1// Copyright 2016 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package command
16
17import (
18	"context"
19	"fmt"
20	"path/filepath"
21	"strings"
22
23	"go.etcd.io/etcd/etcdctl/v3/snapshot"
24
25	"github.com/spf13/cobra"
26	"go.uber.org/zap"
27)
28
29const (
30	defaultName                     = "default"
31	defaultInitialAdvertisePeerURLs = "http://localhost:2380"
32)
33
34var (
35	restoreCluster      string
36	restoreClusterToken string
37	restoreDataDir      string
38	restoreWalDir       string
39	restorePeerURLs     string
40	restoreName         string
41	skipHashCheck       bool
42)
43
44// NewSnapshotCommand returns the cobra command for "snapshot".
45func NewSnapshotCommand() *cobra.Command {
46	cmd := &cobra.Command{
47		Use:   "snapshot <subcommand>",
48		Short: "Manages etcd node snapshots",
49	}
50	cmd.AddCommand(NewSnapshotSaveCommand())
51	cmd.AddCommand(NewSnapshotRestoreCommand())
52	cmd.AddCommand(newSnapshotStatusCommand())
53	return cmd
54}
55
56func NewSnapshotSaveCommand() *cobra.Command {
57	return &cobra.Command{
58		Use:   "save <filename>",
59		Short: "Stores an etcd node backend snapshot to a given file",
60		Run:   snapshotSaveCommandFunc,
61	}
62}
63
64func newSnapshotStatusCommand() *cobra.Command {
65	return &cobra.Command{
66		Use:   "status <filename>",
67		Short: "Gets backend snapshot status of a given file",
68		Long: `When --write-out is set to simple, this command prints out comma-separated status lists for each endpoint.
69The items in the lists are hash, revision, total keys, total size.
70`,
71		Run: snapshotStatusCommandFunc,
72	}
73}
74
75func NewSnapshotRestoreCommand() *cobra.Command {
76	cmd := &cobra.Command{
77		Use:   "restore <filename> [options]",
78		Short: "Restores an etcd member snapshot to an etcd directory",
79		Run:   snapshotRestoreCommandFunc,
80	}
81	cmd.Flags().StringVar(&restoreDataDir, "data-dir", "", "Path to the data directory")
82	cmd.Flags().StringVar(&restoreWalDir, "wal-dir", "", "Path to the WAL directory (use --data-dir if none given)")
83	cmd.Flags().StringVar(&restoreCluster, "initial-cluster", initialClusterFromName(defaultName), "Initial cluster configuration for restore bootstrap")
84	cmd.Flags().StringVar(&restoreClusterToken, "initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during restore bootstrap")
85	cmd.Flags().StringVar(&restorePeerURLs, "initial-advertise-peer-urls", defaultInitialAdvertisePeerURLs, "List of this member's peer URLs to advertise to the rest of the cluster")
86	cmd.Flags().StringVar(&restoreName, "name", defaultName, "Human-readable name for this member")
87	cmd.Flags().BoolVar(&skipHashCheck, "skip-hash-check", false, "Ignore snapshot integrity hash value (required if copied from data directory)")
88
89	return cmd
90}
91
92func snapshotSaveCommandFunc(cmd *cobra.Command, args []string) {
93	if len(args) != 1 {
94		err := fmt.Errorf("snapshot save expects one argument")
95		ExitWithError(ExitBadArgs, err)
96	}
97
98	lg, err := zap.NewProduction()
99	if err != nil {
100		ExitWithError(ExitError, err)
101	}
102	sp := snapshot.NewV3(lg)
103	cfg := mustClientCfgFromCmd(cmd)
104
105	// if user does not specify "--command-timeout" flag, there will be no timeout for snapshot save command
106	ctx, cancel := context.WithCancel(context.Background())
107	if isCommandTimeoutFlagSet(cmd) {
108		ctx, cancel = commandCtx(cmd)
109	}
110	defer cancel()
111
112	path := args[0]
113	if err := sp.Save(ctx, *cfg, path); err != nil {
114		ExitWithError(ExitInterrupted, err)
115	}
116	fmt.Printf("Snapshot saved at %s\n", path)
117}
118
119func snapshotStatusCommandFunc(cmd *cobra.Command, args []string) {
120	if len(args) != 1 {
121		err := fmt.Errorf("snapshot status requires exactly one argument")
122		ExitWithError(ExitBadArgs, err)
123	}
124	initDisplayFromCmd(cmd)
125
126	lg, err := zap.NewProduction()
127	if err != nil {
128		ExitWithError(ExitError, err)
129	}
130	sp := snapshot.NewV3(lg)
131	ds, err := sp.Status(args[0])
132	if err != nil {
133		ExitWithError(ExitError, err)
134	}
135	display.DBStatus(ds)
136}
137
138func snapshotRestoreCommandFunc(cmd *cobra.Command, args []string) {
139	if len(args) != 1 {
140		err := fmt.Errorf("snapshot restore requires exactly one argument")
141		ExitWithError(ExitBadArgs, err)
142	}
143
144	dataDir := restoreDataDir
145	if dataDir == "" {
146		dataDir = restoreName + ".etcd"
147	}
148
149	walDir := restoreWalDir
150	if walDir == "" {
151		walDir = filepath.Join(dataDir, "member", "wal")
152	}
153
154	lg, err := zap.NewProduction()
155	if err != nil {
156		ExitWithError(ExitError, err)
157	}
158	sp := snapshot.NewV3(lg)
159
160	if err := sp.Restore(snapshot.RestoreConfig{
161		SnapshotPath:        args[0],
162		Name:                restoreName,
163		OutputDataDir:       dataDir,
164		OutputWALDir:        walDir,
165		PeerURLs:            strings.Split(restorePeerURLs, ","),
166		InitialCluster:      restoreCluster,
167		InitialClusterToken: restoreClusterToken,
168		SkipHashCheck:       skipHashCheck,
169	}); err != nil {
170		ExitWithError(ExitError, err)
171	}
172}
173
174func initialClusterFromName(name string) string {
175	n := name
176	if name == "" {
177		n = defaultName
178	}
179	return fmt.Sprintf("%s=http://localhost:2380", n)
180}
181