From 440611f9f7b0e68ea9cf0397abeb8d789c7d48ca Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Mon, 31 Oct 2016 19:37:27 -0400 Subject: [PATCH] Add snapshot inspect subcommand (#2451) --- command/snapshot_command.go | 14 ++- command/snapshot_inspect.go | 89 ++++++++++++++ command/snapshot_inspect_test.go | 116 ++++++++++++++++++ command/snapshot_save.go | 2 +- commands.go | 6 + consul/snapshot/snapshot.go | 8 +- consul/snapshot/snapshot_test.go | 17 ++- .../docs/commands/snapshot.html.markdown | 21 +++- .../snapshot/inspect.html.markdown.erb | 47 +++++++ website/source/layouts/docs.erb | 3 + 10 files changed, 306 insertions(+), 17 deletions(-) create mode 100644 command/snapshot_inspect.go create mode 100644 command/snapshot_inspect_test.go create mode 100644 website/source/docs/commands/snapshot/inspect.html.markdown.erb diff --git a/command/snapshot_command.go b/command/snapshot_command.go index 1d1837243..fca39bfa3 100644 --- a/command/snapshot_command.go +++ b/command/snapshot_command.go @@ -20,10 +20,10 @@ func (c *SnapshotCommand) Help() string { helpText := ` Usage: consul snapshot [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 [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 [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" } diff --git a/command/snapshot_inspect.go b/command/snapshot_inspect.go new file mode 100644 index 000000000..8efb8d521 --- /dev/null +++ b/command/snapshot_inspect.go @@ -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" +} diff --git a/command/snapshot_inspect_test.go b/command/snapshot_inspect_test.go new file mode 100644 index 000000000..557d3ba82 --- /dev/null +++ b/command/snapshot_inspect_test.go @@ -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) + } + } +} diff --git a/command/snapshot_save.go b/command/snapshot_save.go index ed7bc0324..5efa51311 100644 --- a/command/snapshot_save.go +++ b/command/snapshot_save.go @@ -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 diff --git a/commands.go b/commands.go index 8e45c2254..99bebff45 100644 --- a/commands.go +++ b/commands.go @@ -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(), diff --git a/consul/snapshot/snapshot.go b/consul/snapshot/snapshot.go index 6f7ee21c1..e39cb12a1 100644 --- a/consul/snapshot/snapshot.go +++ b/consul/snapshot/snapshot.go @@ -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 diff --git a/consul/snapshot/snapshot_test.go b/consul/snapshot/snapshot_test.go index 406c29a1e..2a64343b4 100644 --- a/consul/snapshot/snapshot_test.go +++ b/consul/snapshot/snapshot_test.go @@ -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, ©) @@ -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) } diff --git a/website/source/docs/commands/snapshot.html.markdown b/website/source/docs/commands/snapshot.html.markdown index 9f9ece331..1c8f689ac 100644 --- a/website/source/docs/commands/snapshot.html.markdown +++ b/website/source/docs/commands/snapshot.html.markdown @@ -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 [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. diff --git a/website/source/docs/commands/snapshot/inspect.html.markdown.erb b/website/source/docs/commands/snapshot/inspect.html.markdown.erb new file mode 100644 index 000000000..d249c06f5 --- /dev/null +++ b/website/source/docs/commands/snapshot/inspect.html.markdown.erb @@ -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. \ No newline at end of file diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 0b7a00ec5..726ce44f0 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -152,6 +152,9 @@ > snapshot