Add snapshot inspect subcommand (#2451)

This commit is contained in:
Kyle Havlovitz 2016-10-31 19:37:27 -04:00 committed by GitHub
parent 72412237fd
commit 440611f9f7
10 changed files with 306 additions and 17 deletions

View File

@ -20,10 +20,10 @@ func (c *SnapshotCommand) Help() string {
helpText := `
Usage: consul snapshot <subcommand> [options] [args]
This command has subcommands for saving and restoring the state of the Consul
servers for disaster recovery. These are atomic, point-in-time snapshots which
include key/value entries, service catalog, prepared queries, sessions, and
ACLs.
This command has subcommands for saving, restoring, and inspecting the state
of the Consul servers for disaster recovery. These are atomic, point-in-time
snapshots which include key/value entries, service catalog, prepared queries,
sessions, and ACLs.
If ACLs are enabled, a management token must be supplied in order to perform
snapshot operations.
@ -36,6 +36,10 @@ Usage: consul snapshot <subcommand> [options] [args]
$ consul snapshot restore backup.snap
Inspect a snapshot:
$ consul snapshot inspect backup.snap
For more examples, ask for subcommand help or view the documentation.
@ -44,5 +48,5 @@ Usage: consul snapshot <subcommand> [options] [args]
}
func (c *SnapshotCommand) Synopsis() string {
return "Saves and restores snapshots of Consul server state"
return "Saves, restores and inspects snapshots of Consul server state"
}

View File

@ -0,0 +1,89 @@
package command
import (
"bytes"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/hashicorp/consul/consul/snapshot"
"github.com/mitchellh/cli"
)
// SnapshotInspectCommand is a Command implementation that is used to display
// metadata about a snapshot file
type SnapshotInspectCommand struct {
Ui cli.Ui
}
func (c *SnapshotInspectCommand) Help() string {
helpText := `
Usage: consul snapshot inspect [options] FILE
Displays information about a snapshot file on disk.
To inspect the file "backup.snap":
$ consul snapshot inspect backup.snap
For a full list of options and examples, please see the Consul documentation.
`
return strings.TrimSpace(helpText)
}
func (c *SnapshotInspectCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var file string
args = cmdFlags.Args()
switch len(args) {
case 0:
c.Ui.Error("Missing FILE argument")
return 1
case 1:
file = args[0]
default:
c.Ui.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
// Open the file.
f, err := os.Open(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error opening snapshot file: %s", err))
return 1
}
defer f.Close()
meta, err := snapshot.Verify(f)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error verifying snapshot: %s", err))
}
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
fmt.Fprintf(tw, "ID\t%s\n", meta.ID)
fmt.Fprintf(tw, "Size\t%d\n", meta.Size)
fmt.Fprintf(tw, "Index\t%d\n", meta.Index)
fmt.Fprintf(tw, "Term\t%d\n", meta.Term)
fmt.Fprintf(tw, "Version\t%d\n", meta.Version)
if err = tw.Flush(); err != nil {
c.Ui.Error(fmt.Sprintf("Error rendering snapshot info: %s", err))
}
c.Ui.Info(b.String())
return 0
}
func (c *SnapshotInspectCommand) Synopsis() string {
return "Displays information about a Consul snapshot file"
}

View File

@ -0,0 +1,116 @@
package command
import (
"io"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestSnapshotInspectCommand_implements(t *testing.T) {
var _ cli.Command = &SnapshotInspectCommand{}
}
func TestSnapshotInspectCommand_noTabs(t *testing.T) {
assertNoTabs(t, new(SnapshotInspectCommand))
}
func TestSnapshotInspectCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &SnapshotInspectCommand{Ui: ui}
cases := map[string]struct {
args []string
output string
}{
"no file": {
[]string{},
"Missing FILE argument",
},
"extra args": {
[]string{"foo", "bar", "baz"},
"Too many arguments",
},
}
for name, tc := range cases {
// Ensure our buffer is always clear
if ui.ErrorWriter != nil {
ui.ErrorWriter.Reset()
}
if ui.OutputWriter != nil {
ui.OutputWriter.Reset()
}
code := c.Run(tc.args)
if code == 0 {
t.Errorf("%s: expected non-zero exit", name)
}
output := ui.ErrorWriter.String()
if !strings.Contains(output, tc.output) {
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
}
}
}
func TestSnapshotInspectCommand_Run(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
dir, err := ioutil.TempDir("", "snapshot")
if err != nil {
t.Fatalf("err: %v", err)
}
defer os.RemoveAll(dir)
file := path.Join(dir, "backup.tgz")
// Save a snapshot of the current Consul state
f, err := os.Create(file)
if err != nil {
t.Fatalf("err: %v", err)
}
snap, _, err := client.Snapshot().Save(nil)
if err != nil {
f.Close()
t.Fatalf("err: %v", err)
}
if _, err := io.Copy(f, snap); err != nil {
f.Close()
t.Fatalf("err: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("err: %v", err)
}
// Inspect the snapshot
inspect := &SnapshotInspectCommand{Ui: ui}
args := []string{file}
code := inspect.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
for _, key := range []string{
"ID",
"Size",
"Index",
"Term",
"Version",
} {
if !strings.Contains(output, key) {
t.Fatalf("bad %#v, missing %q", output, key)
}
}
}

View File

@ -113,7 +113,7 @@ func (c *SnapshotSaveCommand) Run(args []string) int {
c.Ui.Error(fmt.Sprintf("Error opening snapshot file for verify: %s", err))
return 1
}
if err := snapshot.Verify(f); err != nil {
if _, err := snapshot.Verify(f); err != nil {
f.Close()
c.Ui.Error(fmt.Sprintf("Error verifying snapshot file: %s", err))
return 1

View File

@ -169,6 +169,12 @@ func init() {
}, nil
},
"snapshot inspect": func() (cli.Command, error) {
return &command.SnapshotInspectCommand{
Ui: ui,
}, nil
},
"version": func() (cli.Command, error) {
return &command.VersionCommand{
HumanVersion: GetHumanVersion(),

View File

@ -125,20 +125,20 @@ func (s *Snapshot) Close() error {
}
// Verify takes the snapshot from the reader and verifies its contents.
func Verify(in io.Reader) error {
func Verify(in io.Reader) (*raft.SnapshotMeta, error) {
// Wrap the reader in a gzip decompressor.
decomp, err := gzip.NewReader(in)
if err != nil {
return fmt.Errorf("failed to decompress snapshot: %v", err)
return nil, fmt.Errorf("failed to decompress snapshot: %v", err)
}
defer decomp.Close()
// Read the archive, throwing away the snapshot data.
var metadata raft.SnapshotMeta
if err := read(decomp, &metadata, ioutil.Discard); err != nil {
return fmt.Errorf("failed to read snapshot file: %v", err)
return nil, fmt.Errorf("failed to read snapshot file: %v", err)
}
return nil
return &metadata, nil
}
// Restore takes the snapshot from the reader and attempts to apply it to the

View File

@ -135,9 +135,10 @@ func TestSnapshot(t *testing.T) {
// Make a Raft and populate it with some data. We tee everything we
// apply off to a buffer for checking post-snapshot.
var expected []bytes.Buffer
entries := 64 * 1024
before, _ := makeRaft(t, path.Join(dir, "before"))
defer before.Shutdown()
for i := 0; i < 64*1024; i++ {
for i := 0; i < entries; i++ {
var log bytes.Buffer
var copy bytes.Buffer
both := io.MultiWriter(&log, &copy)
@ -160,12 +161,22 @@ func TestSnapshot(t *testing.T) {
defer snap.Close()
// Verify the snapshot. We have to rewind it after for the restore.
if err := Verify(snap); err != nil {
metadata, err := Verify(snap)
if err != nil {
t.Fatalf("err: %v", err)
}
if _, err := snap.file.Seek(0, 0); err != nil {
t.Fatalf("err: %v", err)
}
if int(metadata.Index) != entries+2 {
t.Fatalf("bad: %d", metadata.Index)
}
if metadata.Term != 2 {
t.Fatalf("bad: %d", metadata.Index)
}
if metadata.Version != raft.SnapshotVersionMax {
t.Fatalf("bad: %d", metadata.Version)
}
// Make a new, independent Raft.
after, fsm := makeRaft(t, path.Join(dir, "after"))
@ -220,7 +231,7 @@ func TestSnapshot_Nil(t *testing.T) {
func TestSnapshot_BadVerify(t *testing.T) {
buf := bytes.NewBuffer([]byte("nope"))
err := Verify(buf)
_, err := Verify(buf)
if err == nil || !strings.Contains(err.Error(), "unexpected EOF") {
t.Fatalf("err: %v", err)
}

View File

@ -8,10 +8,10 @@ sidebar_current: "docs-commands-snapshot"
Command: `consul snapshot`
The `snapshot` command has subcommands for saving and restoring the state of the
Consul servers for disaster recovery. These are atomic, point-in-time snapshots
which include key/value entries, service catalog, prepared queries, sessions, and
ACLs. This command is available in Consul 0.7.1 and later.
The `snapshot` command has subcommands for saving, restoring, and inspecting the
state of the Consul servers for disaster recovery. These are atomic, point-in-time
snapshots which include key/value entries, service catalog, prepared queries,
sessions, and ACLs. This command is available in Consul 0.7.1 and later.
Snapshots are also accessible via the [HTTP API](/docs/agent/http/snapshot.html).
@ -29,6 +29,7 @@ Usage: consul snapshot <subcommand> [options] [args]
Subcommands:
inspect Displays information about a Consul snapshot file
restore Restores snapshot of Consul server state
save Saves snapshot of Consul server state
```
@ -36,6 +37,7 @@ Subcommands:
For more information, examples, and usage about a subcommand, click on the name
of the subcommand in the sidebar or one of the links below:
- [inspect] (/docs/commands/snapshot/inspect.html)
- [restore](/docs/commands/snapshot/restore.html)
- [save](/docs/commands/snapshot/save.html)
@ -55,5 +57,16 @@ $ consul snapshot restore backup.snap
Restored snapshot
```
To inspect a snapshot from the file "backup.snap":
```text
$ consul snapshot inspect backup.snap
ID 2-5-1477944140022
Size 667
Index 5
Term 2
Version 1
```
For more examples, ask for subcommand help or view the subcommand documentation
by clicking on one of the links in the sidebar.

View File

@ -0,0 +1,47 @@
---
layout: "docs"
page_title: "Commands: Snapshot Inspect"
sidebar_current: "docs-commands-snapshot-inspect"
---
# Consul Snapshot Inspect
Command: `consul snapshot inspect`
The `snapshot inspect` command is used to inspect an atomic, point-in-time
snapshot of the state of the Consul servers which includes key/value entries,
service catalog, prepared queries, sessions, and ACLs. The snapshot is read
from the given file.
The following fields are displayed when inspecting a snapshot:
* `ID` - A unique ID for the snapshot, only used for differentiation purposes.
* `Size` - The size of the snapshot, in bytes.
* `Index` - The Raft index of the latest log entry in the snapshot.
* `Term` - The Raft term of the latest log entry in the snapshot.
* `Version` - The snapshot format version. This only refers to the structure of
the snapshot, not the data contained within.
## Usage
Usage: `consul snapshot inspect [options] FILE`
## Examples
To inspect a snapshot from the file "backup.snap":
```text
$ consul snapshot inspect backup.snap
ID 2-5-1477944140022
Size 667
Index 5
Term 2
Version 1
```
Please see the [HTTP API](/docs/agent/http/snapshot.html) documentation for
more details about snapshot internals.

View File

@ -152,6 +152,9 @@
<li<%= sidebar_current("docs-commands-snapshot") %>>
<a href="/docs/commands/snapshot.html">snapshot</a>
<ul class="subnav">
<li<%= sidebar_current("docs-commands-snapshot-inspect") %>>
<a href="/docs/commands/snapshot/inspect.html">inspect</a>
</li>
<li<%= sidebar_current("docs-commands-snapshot-restore") %>>
<a href="/docs/commands/snapshot/restore.html">restore</a>
</li>