diff --git a/.changelog/18625.txt b/.changelog/18625.txt new file mode 100644 index 0000000000..8474cac8dc --- /dev/null +++ b/.changelog/18625.txt @@ -0,0 +1,5 @@ +```release-note:improvement +Adds flag -append-filename (which works on values version, dc, node and status) to consul snapshot save command. +Adding the flag -append-filename version,dc,node,status will add consul version, consul datacenter, node name and leader/follower +(status) in the file name given in the snapshot save command before the file extension. +``` diff --git a/command/snapshot/save/snapshot_save.go b/command/snapshot/save/snapshot_save.go index e43dcb6126..8a4c94eb92 100644 --- a/command/snapshot/save/snapshot_save.go +++ b/command/snapshot/save/snapshot_save.go @@ -3,7 +3,10 @@ package save import ( "flag" "fmt" + "golang.org/x/exp/slices" "os" + "path/filepath" + "strings" "github.com/mitchellh/cli" "github.com/rboyer/safeio" @@ -20,10 +23,18 @@ func New(ui cli.Ui) *cmd { } type cmd struct { - UI cli.Ui - flags *flag.FlagSet - http *flags.HTTPFlags - help string + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + appendFileNameFlag flags.StringValue +} + +func (c *cmd) getAppendFileNameFlag() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Var(&c.appendFileNameFlag, "append-filename", "Append filename flag supports the following "+ + "comma-separated arguments. 1. version, 2. dc. 3. node 4. status. It appends these values to the filename provided in the command") + return fs } func (c *cmd) init() { @@ -31,6 +42,7 @@ func (c *cmd) init() { c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.getAppendFileNameFlag()) c.help = flags.Usage(help, c.flags) } @@ -55,6 +67,62 @@ func (c *cmd) Run(args []string) int { // Create and test the HTTP client client, err := c.http.APIClient() + + appendFileNameFlags := strings.Split(c.appendFileNameFlag.String(), ",") + + if len(appendFileNameFlags) != 0 && len(c.appendFileNameFlag.String()) > 0 { + agentSelfResponse, err := client.Agent().Self() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent and fetching datacenter/version: %s", err)) + return 1 + } + + fileExt := filepath.Ext(file) + fileNameWithoutExt := strings.TrimSuffix(file, fileExt) + + if slices.Contains(appendFileNameFlags, "version") { + if config, ok := agentSelfResponse["Config"]; ok { + if version, ok := config["Version"]; ok { + fileNameWithoutExt = fileNameWithoutExt + "-" + version.(string) + } + } + } + + if slices.Contains(appendFileNameFlags, "dc") { + if config, ok := agentSelfResponse["Config"]; ok { + if datacenter, ok := config["Datacenter"]; ok { + fileNameWithoutExt = fileNameWithoutExt + "-" + datacenter.(string) + } + } + } + + if slices.Contains(appendFileNameFlags, "node") { + if config, ok := agentSelfResponse["Config"]; ok { + if nodeName, ok := config["NodeName"]; ok { + fileNameWithoutExt = fileNameWithoutExt + "-" + nodeName.(string) + } + } + } + + if slices.Contains(appendFileNameFlags, "status") { + if status, ok := agentSelfResponse["Stats"]; ok { + if config, ok := status["consul"]; ok { + configMap := config.(map[string]interface{}) + if leader, ok := configMap["leader"]; ok { + if leader == "true" { + fileNameWithoutExt = fileNameWithoutExt + "-" + "leader" + } else { + fileNameWithoutExt = fileNameWithoutExt + "-" + "follower" + } + } + } + } + } + + //adding extension back + file = fileNameWithoutExt + fileExt + } + if err != nil { c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) return 1 diff --git a/command/snapshot/save/snapshot_save_test.go b/command/snapshot/save/snapshot_save_test.go index 10e8abcfea..3ea03a2f98 100644 --- a/command/snapshot/save/snapshot_save_test.go +++ b/command/snapshot/save/snapshot_save_test.go @@ -69,6 +69,58 @@ func TestSnapshotSaveCommand_Validation(t *testing.T) { } } +func TestSnapshotSaveCommandWithAppendFileNameFlag(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := agent.NewTestAgent(t, ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + + dir := testutil.TempDir(t, "snapshot") + file := filepath.Join(dir, "backup.tgz") + args := []string{ + "-append-filename=version,dc,node,status", + "-http-addr=" + a.HTTPAddr(), + file, + } + + stats := a.Stats() + + status := "follower" + + if stats["consul"]["leader"] == "true" { + status = "leader" + } + + newFilePath := filepath.Join(dir, "backup"+"-"+a.Config.Version+"-"+a.Config.Datacenter+ + "-"+a.Config.NodeName+"-"+status+".tgz") + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + fi, err := os.Stat(newFilePath) + require.NoError(t, err) + require.Equal(t, fi.Mode(), os.FileMode(0600)) + + f, err := os.Open(newFilePath) + if err != nil { + t.Fatalf("err: %v", err) + } + defer f.Close() + + if err := client.Snapshot().Restore(nil, f); err != nil { + t.Fatalf("err: %v", err) + } +} + func TestSnapshotSaveCommand(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") diff --git a/website/content/commands/snapshot/save.mdx b/website/content/commands/snapshot/save.mdx index b33df4b9f1..bc80092f75 100644 --- a/website/content/commands/snapshot/save.mdx +++ b/website/content/commands/snapshot/save.mdx @@ -47,6 +47,10 @@ Usage: `consul snapshot save [options] FILE` @include 'http_api_options_server.mdx' +- `-append-filename=` - Value can be - version,dc,node,status +Adds consul version, datacenter name, node name, and status (leader/follower) +to the file name before the extension separated by `-` + ## Examples To create a snapshot from the leader server and save it to "backup.snap": @@ -71,6 +75,17 @@ $ consul snapshot save -stale backup.snap # ... ``` +To create snapshot file with consul version, datacenter, node name and leader/follower info, +run + +```shell-session +$ consul snapshot save -append-filename node,status,version,dc backup.snap +#... +``` + +File name created will be like backup-%CONSUL_VERSION%-%DC_NAME%-%NODE_NAME%-%STATUS.snap +example - backup-1.17.0-dc1-local-machine-leader.tgz + This is useful for situations where a cluster is in a degraded state and no leader is available. To target a specific server for a snapshot, you can run the `consul snapshot save` command on that specific server.