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