1// Copyright 2021 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 etcdutl
16
17import (
18	"fmt"
19	"strings"
20
21	"go.etcd.io/etcd/etcdutl/v3/snapshot"
22	"go.etcd.io/etcd/pkg/v3/cobrautl"
23	"go.etcd.io/etcd/server/v3/datadir"
24
25	"github.com/spf13/cobra"
26)
27
28const (
29	defaultName                     = "default"
30	defaultInitialAdvertisePeerURLs = "http://localhost:2380"
31)
32
33var (
34	restoreCluster      string
35	restoreClusterToken string
36	restoreDataDir      string
37	restoreWalDir       string
38	restorePeerURLs     string
39	restoreName         string
40	skipHashCheck       bool
41)
42
43// NewSnapshotCommand returns the cobra command for "snapshot".
44func NewSnapshotCommand() *cobra.Command {
45	cmd := &cobra.Command{
46		Use:   "snapshot <subcommand>",
47		Short: "Manages etcd node snapshots",
48	}
49	cmd.AddCommand(NewSnapshotSaveCommand())
50	cmd.AddCommand(NewSnapshotRestoreCommand())
51	cmd.AddCommand(newSnapshotStatusCommand())
52	return cmd
53}
54
55func NewSnapshotSaveCommand() *cobra.Command {
56	return &cobra.Command{
57		Use:                   "save <filename>",
58		Short:                 "Stores an etcd node backend snapshot to a given file",
59		Hidden:                true,
60		DisableFlagsInUseLine: true,
61		Run: func(cmd *cobra.Command, args []string) {
62			cobrautl.ExitWithError(cobrautl.ExitBadArgs,
63				fmt.Errorf("In order to download snapshot use: "+
64					"`etcdctl snapshot save ...`"))
65		},
66		Deprecated: "Use `etcdctl snapshot save` to download snapshot",
67	}
68}
69
70func newSnapshotStatusCommand() *cobra.Command {
71	return &cobra.Command{
72		Use:   "status <filename>",
73		Short: "Gets backend snapshot status of a given file",
74		Long: `When --write-out is set to simple, this command prints out comma-separated status lists for each endpoint.
75The items in the lists are hash, revision, total keys, total size.
76`,
77		Run: SnapshotStatusCommandFunc,
78	}
79}
80
81func NewSnapshotRestoreCommand() *cobra.Command {
82	cmd := &cobra.Command{
83		Use:   "restore <filename> --data-dir {output dir} [options]",
84		Short: "Restores an etcd member snapshot to an etcd directory",
85		Run:   snapshotRestoreCommandFunc,
86	}
87	cmd.Flags().StringVar(&restoreDataDir, "data-dir", "", "Path to the output data directory")
88	cmd.Flags().StringVar(&restoreWalDir, "wal-dir", "", "Path to the WAL directory (use --data-dir if none given)")
89	cmd.Flags().StringVar(&restoreCluster, "initial-cluster", initialClusterFromName(defaultName), "Initial cluster configuration for restore bootstrap")
90	cmd.Flags().StringVar(&restoreClusterToken, "initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during restore bootstrap")
91	cmd.Flags().StringVar(&restorePeerURLs, "initial-advertise-peer-urls", defaultInitialAdvertisePeerURLs, "List of this member's peer URLs to advertise to the rest of the cluster")
92	cmd.Flags().StringVar(&restoreName, "name", defaultName, "Human-readable name for this member")
93	cmd.Flags().BoolVar(&skipHashCheck, "skip-hash-check", false, "Ignore snapshot integrity hash value (required if copied from data directory)")
94
95	cmd.MarkFlagRequired("data-dir")
96
97	return cmd
98}
99
100func SnapshotStatusCommandFunc(cmd *cobra.Command, args []string) {
101	if len(args) != 1 {
102		err := fmt.Errorf("snapshot status requires exactly one argument")
103		cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
104	}
105	printer := initPrinterFromCmd(cmd)
106
107	lg := GetLogger()
108	sp := snapshot.NewV3(lg)
109	ds, err := sp.Status(args[0])
110	if err != nil {
111		cobrautl.ExitWithError(cobrautl.ExitError, err)
112	}
113	printer.DBStatus(ds)
114}
115
116func snapshotRestoreCommandFunc(_ *cobra.Command, args []string) {
117	SnapshotRestoreCommandFunc(restoreCluster, restoreClusterToken, restoreDataDir, restoreWalDir,
118		restorePeerURLs, restoreName, skipHashCheck, args)
119}
120
121func SnapshotRestoreCommandFunc(restoreCluster string,
122	restoreClusterToken string,
123	restoreDataDir string,
124	restoreWalDir string,
125	restorePeerURLs string,
126	restoreName string,
127	skipHashCheck bool,
128	args []string) {
129	if len(args) != 1 {
130		err := fmt.Errorf("snapshot restore requires exactly one argument")
131		cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
132	}
133
134	dataDir := restoreDataDir
135	if dataDir == "" {
136		dataDir = restoreName + ".etcd"
137	}
138
139	walDir := restoreWalDir
140	if walDir == "" {
141		walDir = datadir.ToWalDir(dataDir)
142	}
143
144	lg := GetLogger()
145	sp := snapshot.NewV3(lg)
146
147	if err := sp.Restore(snapshot.RestoreConfig{
148		SnapshotPath:        args[0],
149		Name:                restoreName,
150		OutputDataDir:       dataDir,
151		OutputWALDir:        walDir,
152		PeerURLs:            strings.Split(restorePeerURLs, ","),
153		InitialCluster:      restoreCluster,
154		InitialClusterToken: restoreClusterToken,
155		SkipHashCheck:       skipHashCheck,
156	}); err != nil {
157		cobrautl.ExitWithError(cobrautl.ExitError, err)
158	}
159}
160
161func initialClusterFromName(name string) string {
162	n := name
163	if name == "" {
164		n = defaultName
165	}
166	return fmt.Sprintf("%s=http://localhost:2380", n)
167}
168