Add snapshot inspect subcommand (#2451)

This commit is contained in:
Kyle Havlovitz 2016-10-31 19:37:27 -04:00 committed by GitHub
parent 4be39290e5
commit 606662c502
10 changed files with 306 additions and 17 deletions

View File

@ -20,10 +20,10 @@ func (c *SnapshotCommand) Help() string {
helpText := ` helpText := `
Usage: consul snapshot <subcommand> [options] [args] Usage: consul snapshot <subcommand> [options] [args]
This command has subcommands for saving and restoring the state of the Consul This command has subcommands for saving, restoring, and inspecting the state
servers for disaster recovery. These are atomic, point-in-time snapshots which of the Consul servers for disaster recovery. These are atomic, point-in-time
include key/value entries, service catalog, prepared queries, sessions, and snapshots which include key/value entries, service catalog, prepared queries,
ACLs. sessions, and ACLs.
If ACLs are enabled, a management token must be supplied in order to perform If ACLs are enabled, a management token must be supplied in order to perform
snapshot operations. snapshot operations.
@ -36,6 +36,10 @@ Usage: consul snapshot <subcommand> [options] [args]
$ consul snapshot restore backup.snap $ consul snapshot restore backup.snap
Inspect a snapshot:
$ consul snapshot inspect backup.snap
For more examples, ask for subcommand help or view the documentation. 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 { 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)) c.Ui.Error(fmt.Sprintf("Error opening snapshot file for verify: %s", err))
return 1 return 1
} }
if err := snapshot.Verify(f); err != nil { if _, err := snapshot.Verify(f); err != nil {
f.Close() f.Close()
c.Ui.Error(fmt.Sprintf("Error verifying snapshot file: %s", err)) c.Ui.Error(fmt.Sprintf("Error verifying snapshot file: %s", err))
return 1 return 1

View File

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

View File

@ -125,20 +125,20 @@ func (s *Snapshot) Close() error {
} }
// Verify takes the snapshot from the reader and verifies its contents. // 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. // Wrap the reader in a gzip decompressor.
decomp, err := gzip.NewReader(in) decomp, err := gzip.NewReader(in)
if err != nil { 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() defer decomp.Close()
// Read the archive, throwing away the snapshot data. // Read the archive, throwing away the snapshot data.
var metadata raft.SnapshotMeta var metadata raft.SnapshotMeta
if err := read(decomp, &metadata, ioutil.Discard); err != nil { 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 // 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 // Make a Raft and populate it with some data. We tee everything we
// apply off to a buffer for checking post-snapshot. // apply off to a buffer for checking post-snapshot.
var expected []bytes.Buffer var expected []bytes.Buffer
entries := 64 * 1024
before, _ := makeRaft(t, path.Join(dir, "before")) before, _ := makeRaft(t, path.Join(dir, "before"))
defer before.Shutdown() defer before.Shutdown()
for i := 0; i < 64*1024; i++ { for i := 0; i < entries; i++ {
var log bytes.Buffer var log bytes.Buffer
var copy bytes.Buffer var copy bytes.Buffer
both := io.MultiWriter(&log, &copy) both := io.MultiWriter(&log, &copy)
@ -160,12 +161,22 @@ func TestSnapshot(t *testing.T) {
defer snap.Close() defer snap.Close()
// Verify the snapshot. We have to rewind it after for the restore. // 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) t.Fatalf("err: %v", err)
} }
if _, err := snap.file.Seek(0, 0); err != nil { if _, err := snap.file.Seek(0, 0); err != nil {
t.Fatalf("err: %v", err) 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. // Make a new, independent Raft.
after, fsm := makeRaft(t, path.Join(dir, "after")) after, fsm := makeRaft(t, path.Join(dir, "after"))
@ -220,7 +231,7 @@ func TestSnapshot_Nil(t *testing.T) {
func TestSnapshot_BadVerify(t *testing.T) { func TestSnapshot_BadVerify(t *testing.T) {
buf := bytes.NewBuffer([]byte("nope")) buf := bytes.NewBuffer([]byte("nope"))
err := Verify(buf) _, err := Verify(buf)
if err == nil || !strings.Contains(err.Error(), "unexpected EOF") { if err == nil || !strings.Contains(err.Error(), "unexpected EOF") {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

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