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