// Copyright 2015 The etcd Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package command import ( "encoding/binary" "log" "os" "path" "path/filepath" "regexp" "time" "github.com/coreos/etcd/etcdserver/etcdserverpb" "github.com/coreos/etcd/etcdserver/membership" "github.com/coreos/etcd/pkg/fileutil" "github.com/coreos/etcd/pkg/idutil" "github.com/coreos/etcd/pkg/pbutil" "github.com/coreos/etcd/raft/raftpb" "github.com/coreos/etcd/snap" "github.com/coreos/etcd/wal" "github.com/coreos/etcd/wal/walpb" bolt "github.com/coreos/bbolt" "github.com/urfave/cli" ) func NewBackupCommand() cli.Command { return cli.Command{ Name: "backup", Usage: "backup an etcd directory", ArgsUsage: " ", Flags: []cli.Flag{ cli.StringFlag{Name: "data-dir", Value: "", Usage: "Path to the etcd data dir"}, cli.StringFlag{Name: "wal-dir", Value: "", Usage: "Path to the etcd wal dir"}, cli.StringFlag{Name: "backup-dir", Value: "", Usage: "Path to the backup dir"}, cli.StringFlag{Name: "backup-wal-dir", Value: "", Usage: "Path to the backup wal dir"}, cli.BoolFlag{Name: "with-v3", Usage: "Backup v3 backend data"}, }, Action: handleBackup, } } // handleBackup handles a request that intends to do a backup. func handleBackup(c *cli.Context) error { var srcWAL string var destWAL string withV3 := c.Bool("with-v3") srcSnap := filepath.Join(c.String("data-dir"), "member", "snap") destSnap := filepath.Join(c.String("backup-dir"), "member", "snap") if c.String("wal-dir") != "" { srcWAL = c.String("wal-dir") } else { srcWAL = filepath.Join(c.String("data-dir"), "member", "wal") } if c.String("backup-wal-dir") != "" { destWAL = c.String("backup-wal-dir") } else { destWAL = filepath.Join(c.String("backup-dir"), "member", "wal") } if err := fileutil.CreateDirAll(destSnap); err != nil { log.Fatalf("failed creating backup snapshot dir %v: %v", destSnap, err) } walsnap := saveSnap(destSnap, srcSnap) metadata, state, ents := loadWAL(srcWAL, walsnap, withV3) saveDB(filepath.Join(destSnap, "db"), filepath.Join(srcSnap, "db"), state.Commit, withV3) idgen := idutil.NewGenerator(0, time.Now()) metadata.NodeID = idgen.Next() metadata.ClusterID = idgen.Next() neww, err := wal.Create(destWAL, pbutil.MustMarshal(&metadata)) if err != nil { log.Fatal(err) } defer neww.Close() if err := neww.Save(state, ents); err != nil { log.Fatal(err) } if err := neww.SaveSnapshot(walsnap); err != nil { log.Fatal(err) } return nil } func saveSnap(destSnap, srcSnap string) (walsnap walpb.Snapshot) { ss := snap.New(srcSnap) snapshot, err := ss.Load() if err != nil && err != snap.ErrNoSnapshot { log.Fatal(err) } if snapshot != nil { walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term newss := snap.New(destSnap) if err = newss.SaveSnap(*snapshot); err != nil { log.Fatal(err) } } return walsnap } func loadWAL(srcWAL string, walsnap walpb.Snapshot, v3 bool) (etcdserverpb.Metadata, raftpb.HardState, []raftpb.Entry) { w, err := wal.OpenForRead(srcWAL, walsnap) if err != nil { log.Fatal(err) } defer w.Close() wmetadata, state, ents, err := w.ReadAll() switch err { case nil: case wal.ErrSnapshotNotFound: log.Printf("Failed to find the match snapshot record %+v in wal %v.", walsnap, srcWAL) log.Printf("etcdctl will add it back. Start auto fixing...") default: log.Fatal(err) } re := path.Join(membership.StoreMembersPrefix, "[[:xdigit:]]{1,16}", "attributes") memberAttrRE := regexp.MustCompile(re) removed := uint64(0) i := 0 remove := func() { ents = append(ents[:i], ents[i+1:]...) removed++ i-- } for i = 0; i < len(ents); i++ { ents[i].Index -= removed if ents[i].Type == raftpb.EntryConfChange { log.Println("ignoring EntryConfChange raft entry") remove() continue } var raftReq etcdserverpb.InternalRaftRequest var v2Req *etcdserverpb.Request if pbutil.MaybeUnmarshal(&raftReq, ents[i].Data) { v2Req = raftReq.V2 } else { v2Req = &etcdserverpb.Request{} pbutil.MustUnmarshal(v2Req, ents[i].Data) } if v2Req != nil && v2Req.Method == "PUT" && memberAttrRE.MatchString(v2Req.Path) { log.Println("ignoring member attribute update on", v2Req.Path) remove() continue } if v2Req != nil { continue } if v3 || raftReq.Header == nil { continue } log.Println("ignoring v3 raft entry") remove() } state.Commit -= removed var metadata etcdserverpb.Metadata pbutil.MustUnmarshal(&metadata, wmetadata) return metadata, state, ents } // saveDB copies the v3 backend and strips cluster information. func saveDB(destDB, srcDB string, idx uint64, v3 bool) { // open src db to safely copy db state if v3 { var src *bolt.DB ch := make(chan *bolt.DB, 1) go func() { src, err := bolt.Open(srcDB, 0444, &bolt.Options{ReadOnly: true}) if err != nil { log.Fatal(err) } ch <- src }() select { case src = <-ch: case <-time.After(time.Second): log.Println("waiting to acquire lock on", srcDB) src = <-ch } defer src.Close() tx, err := src.Begin(false) if err != nil { log.Fatal(err) } // copy srcDB to destDB dest, err := os.Create(destDB) if err != nil { log.Fatal(err) } if _, err := tx.WriteTo(dest); err != nil { log.Fatal(err) } dest.Close() if err := tx.Rollback(); err != nil { log.Fatal(err) } } db, err := bolt.Open(destDB, 0644, &bolt.Options{}) if err != nil { log.Fatal(err) } tx, err := db.Begin(true) if err != nil { log.Fatal(err) } // remove membership information; should be clobbered by --force-new-cluster for _, bucket := range []string{"members", "members_removed", "cluster"} { tx.DeleteBucket([]byte(bucket)) } // update consistent index to match hard state if !v3 { idxBytes := make([]byte, 8) binary.BigEndian.PutUint64(idxBytes, idx) b, err := tx.CreateBucketIfNotExists([]byte("meta")) if err != nil { log.Fatal(err) } b.Put([]byte("consistent_index"), idxBytes) } if err := tx.Commit(); err != nil { log.Fatal(err) } if err := db.Close(); err != nil { log.Fatal(err) } }