mirror of
https://github.com/kemko/nomad.git
synced 2026-01-02 00:15:43 +03:00
Co-authored-by: Michael Schurter <michael.schurter@gmail.com> Co-authored-by: James Rasell <jrasell@hashicorp.com>
258 lines
5.6 KiB
Go
258 lines
5.6 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
humanize "github.com/dustin/go-humanize"
|
|
"github.com/hashicorp/go-msgpack/v2/codec"
|
|
"github.com/hashicorp/nomad/helper/snapshot"
|
|
"github.com/hashicorp/nomad/nomad"
|
|
"github.com/hashicorp/raft"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
type OperatorSnapshotInspectCommand struct {
|
|
Meta
|
|
}
|
|
|
|
type typeStats struct {
|
|
Name string
|
|
Sum int
|
|
Count int
|
|
}
|
|
|
|
type SnapshotInspectFormat struct {
|
|
Meta *raft.SnapshotMeta
|
|
Stats []typeStats
|
|
}
|
|
|
|
// SnapshotInfo is used for passing snapshot stat
|
|
// information between functions
|
|
type SnapshotInfo struct {
|
|
Stats map[nomad.SnapshotType]typeStats
|
|
TotalSize int
|
|
TotalCount int
|
|
}
|
|
|
|
// countingReader helps keep track of the bytes we have read
|
|
// when reading snapshots
|
|
type countingReader struct {
|
|
wrappedReader io.Reader
|
|
read int
|
|
}
|
|
|
|
func (r *countingReader) Read(p []byte) (n int, err error) {
|
|
n, err = r.wrappedReader.Read(p)
|
|
if err == nil {
|
|
r.read += n
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (c *OperatorSnapshotInspectCommand) Help() string {
|
|
helpText := `
|
|
Usage: nomad operator snapshot inspect [options] <file>
|
|
|
|
Displays information about a snapshot file on disk.
|
|
The output will include all snapshot types and their
|
|
respective sizes, sorted in descending order.
|
|
|
|
To inspect the file "backup.snap":
|
|
$ nomad operator snapshot inspect backup.snap
|
|
|
|
Snapshot Inspect Options:
|
|
|
|
-json
|
|
Output the snapshot inspect in its JSON format.
|
|
`
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *OperatorSnapshotInspectCommand) AutocompleteFlags() complete.Flags {
|
|
return complete.Flags{}
|
|
}
|
|
|
|
func (c *OperatorSnapshotInspectCommand) AutocompleteArgs() complete.Predictor {
|
|
return complete.PredictNothing
|
|
}
|
|
|
|
func (c *OperatorSnapshotInspectCommand) Synopsis() string {
|
|
return "Displays information about a Nomad snapshot file"
|
|
}
|
|
|
|
func (c *OperatorSnapshotInspectCommand) Name() string { return "operator snapshot inspect" }
|
|
|
|
func (c *OperatorSnapshotInspectCommand) Run(args []string) int {
|
|
var json bool
|
|
|
|
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
|
flags.BoolVar(&json, "json", false, "")
|
|
|
|
if err := flags.Parse(args); err != nil {
|
|
return 1
|
|
}
|
|
|
|
// Check that we either got no filename or exactly one.
|
|
args = flags.Args()
|
|
if len(args) != 1 {
|
|
c.Ui.Error("This command takes one argument: <filename>")
|
|
c.Ui.Error(commandErrorText(c))
|
|
return 1
|
|
}
|
|
|
|
path := args[0]
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error opening snapshot file: %s", err))
|
|
return 1
|
|
}
|
|
defer f.Close()
|
|
|
|
meta, info, err := inspect(f)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error inspecting snapshot: %s", err))
|
|
return 1
|
|
}
|
|
stats := generateStats(info)
|
|
|
|
// format as JSON if requested
|
|
if json {
|
|
data := SnapshotInspectFormat{
|
|
Meta: meta,
|
|
Stats: stats,
|
|
}
|
|
out, err := Format(json, "", data)
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
c.Ui.Output(out)
|
|
return 0
|
|
}
|
|
|
|
// print human-readable output
|
|
c.Ui.Output(formatKV([]string{
|
|
fmt.Sprintf("Created|%s", extractTimeFromName(meta.ID)),
|
|
fmt.Sprintf("ID|%s", meta.ID),
|
|
fmt.Sprintf("Size|%s", humanize.IBytes(uint64(meta.Size))),
|
|
fmt.Sprintf("Index|%d", meta.Index),
|
|
fmt.Sprintf("Term|%d", meta.Term),
|
|
fmt.Sprintf("Version|%d", meta.Version),
|
|
}))
|
|
c.Ui.Output("")
|
|
|
|
output := []string{"Type|Count|Size"}
|
|
|
|
for _, stat := range stats {
|
|
output = append(output, fmt.Sprintf("%s|%d|%s", stat.Name, stat.Count, humanize.IBytes(uint64(stat.Sum))))
|
|
}
|
|
output = append(output,
|
|
" | | ",
|
|
fmt.Sprintf("Total|%v|%s", info.TotalCount, humanize.IBytes(uint64(info.TotalSize))),
|
|
)
|
|
|
|
c.Ui.Output(formatListWithSpaces(output))
|
|
return 0
|
|
}
|
|
|
|
func inspect(file io.Reader) (*raft.SnapshotMeta, *SnapshotInfo, error) {
|
|
info := &SnapshotInfo{
|
|
Stats: make(map[nomad.SnapshotType]typeStats),
|
|
TotalSize: 0,
|
|
}
|
|
|
|
// w is closed by CopySnapshot
|
|
r, w := io.Pipe()
|
|
cr := &countingReader{wrappedReader: r}
|
|
errCh := make(chan error)
|
|
metaCh := make(chan *raft.SnapshotMeta)
|
|
|
|
go func() {
|
|
meta, err := snapshot.CopySnapshot(file, w)
|
|
if err != nil {
|
|
errCh <- fmt.Errorf("failed to read snapshot: %w", err)
|
|
} else {
|
|
metaCh <- meta
|
|
}
|
|
}()
|
|
|
|
handler := func(header *nomad.SnapshotHeader, snapType nomad.SnapshotType, dec *codec.Decoder) error {
|
|
name := snapType.String()
|
|
stat := info.Stats[snapType]
|
|
|
|
if stat.Name == "" {
|
|
stat.Name = name
|
|
}
|
|
|
|
var val interface{}
|
|
err := dec.Decode(&val)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode snapshot %q: %v", snapType, err)
|
|
}
|
|
|
|
size := cr.read - info.TotalSize
|
|
stat.Sum += size
|
|
stat.Count++
|
|
info.TotalSize = cr.read
|
|
info.TotalCount++
|
|
info.Stats[snapType] = stat
|
|
|
|
return nil
|
|
}
|
|
|
|
err := nomad.ReadSnapshot(cr, handler)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
return nil, nil, err
|
|
case meta := <-metaCh:
|
|
return meta, info, nil
|
|
}
|
|
}
|
|
|
|
func generateStats(info *SnapshotInfo) []typeStats {
|
|
ss := make([]typeStats, 0, len(info.Stats))
|
|
for _, stat := range info.Stats {
|
|
ss = append(ss, stat)
|
|
}
|
|
|
|
// sort by Sum
|
|
sort.Slice(ss, func(i, j int) bool {
|
|
// sort alphabetically if size is equal
|
|
if ss[i].Sum == ss[j].Sum {
|
|
return ss[i].Name < ss[j].Name
|
|
}
|
|
return ss[i].Sum > ss[j].Sum
|
|
})
|
|
|
|
return ss
|
|
}
|
|
|
|
// Raft snapshot name is in format of <term>-<index>-<time-milliseconds>
|
|
// we will extract the creation time
|
|
func extractTimeFromName(snapshotName string) string {
|
|
parts := strings.Split(snapshotName, "-")
|
|
if len(parts) != 3 {
|
|
return ""
|
|
}
|
|
msec, err := strconv.ParseInt(parts[2], 10, 64)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return formatTime(time.UnixMilli(msec))
|
|
}
|