Skip to content

Commit

Permalink
cli: Add etcdutl snapshot hashkv command
Browse files Browse the repository at this point in the history
Signed-off-by: Cenk Alti <cenkalti@gmail.com>
  • Loading branch information
cenkalti committed May 26, 2023
1 parent 7cc98e6 commit 1873518
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 5 deletions.
38 changes: 38 additions & 0 deletions etcdutl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,44 @@ Prints a line of JSON encoding the database hash, revision, total keys, and size
+----------+----------+------------+------------+
```

### SNAPSHOT HASHKV [options] \<filename\>

SNAPSHOT HASHKV prints hash of keys and values up to given revision.

#### Options

- rev -- Revision number. Default is 0 which means the latest revision.

#### Output

##### Simple format

Prints a humanized table of the KV hash, hash revision and compact revision.

##### JSON format

Prints a line of JSON encoding the KV hash, hash revision and compact revision.

#### Examples
```bash
./etcdutl snapshot hashkv file.db
# 35c86e9b, 214, 150
```

```bash
./etcdutl --write-out=json snapshot hashkv file.db
# {"hash":902327963,"hashRevision":214,"compactRevision":150}
```

```bash
./etcdutl --write-out=table snapshot hashkv file.db
+----------+---------------+------------------+
| HASH | HASH REVISION | COMPACT REVISION |
+----------+---------------+------------------+
| 35c86e9b | 214 | 150 |
+----------+---------------+------------------+
```

### VERSION

Prints the version of etcdutl.
Expand Down
12 changes: 12 additions & 0 deletions etcdutl/etcdutl/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (

type printer interface {
DBStatus(snapshot.Status)
DBHashKV(snapshot.HashKV)
}

func NewPrinter(printerType string) printer {
Expand Down Expand Up @@ -65,6 +66,7 @@ func newPrinterUnsupported(n string) printer {
}

func (p *printerUnsupported) DBStatus(snapshot.Status) { p.p(nil) }
func (p *printerUnsupported) DBHashKV(snapshot.HashKV) { p.p(nil) }

func makeDBStatusTable(ds snapshot.Status) (hdr []string, rows [][]string) {
hdr = []string{"hash", "revision", "total keys", "total size", "version"}
Expand All @@ -78,6 +80,16 @@ func makeDBStatusTable(ds snapshot.Status) (hdr []string, rows [][]string) {
return hdr, rows
}

func makeDBHashKVTable(ds snapshot.HashKV) (hdr []string, rows [][]string) {
hdr = []string{"hash", "hash revision", "compact revision"}
rows = append(rows, []string{
fmt.Sprintf("%x", ds.Hash),
fmt.Sprint(ds.HashRevision),
fmt.Sprint(ds.CompactRevision),
})
return hdr, rows
}

func initPrinterFromCmd(cmd *cobra.Command) (p printer) {
outputType, err := cmd.Flags().GetString("write-out")
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions etcdutl/etcdutl/printer_fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ func (p *fieldsPrinter) DBStatus(r snapshot.Status) {
fmt.Println(`"Size" :`, r.TotalSize)
fmt.Println(`"Version" :`, r.Version)
}

func (p *fieldsPrinter) DBHashKV(r snapshot.HashKV) {
fmt.Println(`"Hash" :`, r.Hash)
fmt.Println(`"Hash revision" :`, r.HashRevision)
fmt.Println(`"Compact revision" :`, r.CompactRevision)
}
1 change: 1 addition & 0 deletions etcdutl/etcdutl/printer_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func newJSONPrinter() printer {
}

func (p *jsonPrinter) DBStatus(r snapshot.Status) { printJSON(r) }
func (p *jsonPrinter) DBHashKV(r snapshot.HashKV) { printJSON(r) }

// !!! Share ??
func printJSON(v interface{}) {
Expand Down
7 changes: 7 additions & 0 deletions etcdutl/etcdutl/printer_simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ func (s *simplePrinter) DBStatus(ds snapshot.Status) {
fmt.Println(strings.Join(row, ", "))
}
}

func (s *simplePrinter) DBHashKV(ds snapshot.HashKV) {
_, rows := makeDBHashKVTable(ds)
for _, row := range rows {
fmt.Println(strings.Join(row, ", "))
}
}
11 changes: 11 additions & 0 deletions etcdutl/etcdutl/printer_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,14 @@ func (tp *tablePrinter) DBStatus(r snapshot.Status) {
table.SetAlignment(tablewriter.ALIGN_RIGHT)
table.Render()
}

func (tp *tablePrinter) DBHashKV(r snapshot.HashKV) {
hdr, rows := makeDBHashKVTable(r)
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(hdr)
for _, row := range rows {
table.Append(row)
}
table.SetAlignment(tablewriter.ALIGN_RIGHT)
table.Render()
}
31 changes: 31 additions & 0 deletions etcdutl/etcdutl/snapshot_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var (
restorePeerURLs string
restoreName string
skipHashCheck bool
hashKVRevision int64
)

// NewSnapshotCommand returns the cobra command for "snapshot".
Expand All @@ -48,6 +49,7 @@ func NewSnapshotCommand() *cobra.Command {
}
cmd.AddCommand(NewSnapshotRestoreCommand())
cmd.AddCommand(newSnapshotStatusCommand())
cmd.AddCommand(newSnapshotHashKVCommand())
return cmd
}

Expand All @@ -62,6 +64,19 @@ The items in the lists are hash, revision, total keys, total size.
}
}

func newSnapshotHashKVCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "hashkv <filename>",
Short: "Prints the KV history hash of a given file",
Long: `When --write-out is set to simple, this command prints out comma-separated status lists for each endpoint.
The items in the lists are hash, hash revision, compact revision.
`,
Run: SnapshotHashKVCommandFunc,
}
cmd.Flags().Int64Var(&hashKVRevision, "rev", 0, "maximum revision to hash (default: all revisions)")
return cmd
}

func NewSnapshotRestoreCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "restore <filename> --data-dir {output dir} [options]",
Expand Down Expand Up @@ -98,6 +113,22 @@ func SnapshotStatusCommandFunc(cmd *cobra.Command, args []string) {
printer.DBStatus(ds)
}

func SnapshotHashKVCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
err := fmt.Errorf("snapshot hashkv requires exactly one argument")
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
}
printer := initPrinterFromCmd(cmd)

lg := GetLogger()
sp := snapshot.NewV3(lg)
ds, err := sp.HashKV(args[0], hashKVRevision)
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitError, err)
}
printer.DBHashKV(ds)
}

func snapshotRestoreCommandFunc(_ *cobra.Command, args []string) {
SnapshotRestoreCommandFunc(restoreCluster, restoreClusterToken, restoreDataDir, restoreWalDir,
restorePeerURLs, restoreName, skipHashCheck, args)
Expand Down
28 changes: 28 additions & 0 deletions etcdutl/snapshot/v3_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"go.etcd.io/etcd/server/v3/etcdserver/api/v2store"
"go.etcd.io/etcd/server/v3/etcdserver/cindex"
"go.etcd.io/etcd/server/v3/storage/backend"
"go.etcd.io/etcd/server/v3/storage/mvcc"
"go.etcd.io/etcd/server/v3/storage/schema"
"go.etcd.io/etcd/server/v3/storage/wal"
"go.etcd.io/etcd/server/v3/storage/wal/walpb"
Expand All @@ -63,6 +64,9 @@ type Manager interface {
// Status returns the snapshot file information.
Status(dbPath string) (Status, error)

// HashKV returns the hash of keys and values up to specified revision (0 means all).
HashKV(dbPath string, rev int64) (HashKV, error)

// Restore restores a new etcd data directory from given snapshot
// file. It returns an error if specified data directory already
// exists, to prevent unintended data directory overwrites.
Expand Down Expand Up @@ -110,6 +114,30 @@ type Status struct {
Version string `json:"version"`
}

type HashKV struct {
Hash uint32 `json:"hash"`
HashRevision int64 `json:"hashRevision"`
CompactRevision int64 `json:"compactRevision"`
}

func (s *v3Manager) HashKV(dbPath string, rev int64) (ds HashKV, err error) {
cfg := backend.DefaultBackendConfig(zap.NewNop())
cfg.Path = dbPath
b := backend.New(cfg)
st := mvcc.NewStore(zap.NewNop(), b, nil, mvcc.StoreConfig{})
hst := mvcc.NewHashStorage(zap.NewNop(), st)

h, _, err := hst.HashByRev(rev)
if err != nil {
return HashKV{}, err
}
return HashKV{
Hash: h.Hash,
HashRevision: h.Revision,
CompactRevision: h.CompactRevision,
}, nil
}

// Status returns the snapshot file information.
func (s *v3Manager) Status(dbPath string) (ds Status, err error) {
if _, err = os.Stat(dbPath); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion server/storage/mvcc/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ type hashStorage struct {
lg *zap.Logger
}

func newHashStorage(lg *zap.Logger, s *store) *hashStorage {
func NewHashStorage(lg *zap.Logger, s *store) *hashStorage {
return &hashStorage{
store: s,
lg: lg,
Expand Down
4 changes: 2 additions & 2 deletions server/storage/mvcc/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (tc hashTestCase) Compact(ctx context.Context, rev int64) error {

func TestHasherStore(t *testing.T) {
lg := zaptest.NewLogger(t)
s := newHashStorage(lg, newFakeStore(lg))
s := NewHashStorage(lg, newFakeStore(lg))
defer s.store.Close()

var hashes []KeyValueHash
Expand Down Expand Up @@ -207,7 +207,7 @@ func TestHasherStore(t *testing.T) {

func TestHasherStoreFull(t *testing.T) {
lg := zaptest.NewLogger(t)
s := newHashStorage(lg, newFakeStore(lg))
s := NewHashStorage(lg, newFakeStore(lg))
defer s.store.Close()

var minRevision int64 = 100
Expand Down
2 changes: 1 addition & 1 deletion server/storage/mvcc/kvstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func NewStore(lg *zap.Logger, b backend.Backend, le lease.Lessor, cfg StoreConfi

lg: lg,
}
s.hashes = newHashStorage(lg, s)
s.hashes = NewHashStorage(lg, s)
s.ReadView = &readView{s}
s.WriteView = &writeView{s}
if s.le != nil {
Expand Down
2 changes: 1 addition & 1 deletion server/storage/mvcc/kvstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ func newFakeStore(lg *zap.Logger) *store {
lg: lg,
}
s.ReadView, s.WriteView = &readView{s}, &writeView{s}
s.hashes = newHashStorage(lg, s)
s.hashes = NewHashStorage(lg, s)
return s
}

Expand Down

0 comments on commit 1873518

Please sign in to comment.