├── Makefile ├── migrator ├── test-fixtures │ └── raft │ │ └── mdb │ │ ├── data.mdb │ │ └── lock.mdb ├── migrator.go └── migrator_test.go ├── .gitignore ├── main_test.go ├── README.md ├── main.go └── LICENSE /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test ./... 3 | 4 | build: 5 | go build -o bin/consul-migrate 6 | -------------------------------------------------------------------------------- /migrator/test-fixtures/raft/mdb/data.mdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/consul-migrate/HEAD/migrator/test-fixtures/raft/mdb/data.mdb -------------------------------------------------------------------------------- /migrator/test-fixtures/raft/mdb/lock.mdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/consul-migrate/HEAD/migrator/test-fixtures/raft/mdb/lock.mdb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | bin/* 27 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestMain_fails(t *testing.T) { 11 | // Returns 1 on bad args 12 | if code := realMain([]string{}); code != 1 { 13 | t.Fatalf("bad: %d", code) 14 | } 15 | if code := realMain([]string{"1", "2", "3"}); code != 1 { 16 | t.Fatalf("bad: %d", code) 17 | } 18 | 19 | // Returns 1 on bad data-dir 20 | if code := realMain([]string{"consul-migrate", "/unicorns"}); code != 1 { 21 | t.Fatalf("bad: %d", code) 22 | } 23 | } 24 | 25 | func TestMain_help(t *testing.T) { 26 | fh, err := ioutil.TempFile("", "consul-migrate") 27 | if err != nil { 28 | t.Fatalf("err: %s", err) 29 | } 30 | defer os.Remove(fh.Name()) 31 | 32 | stdoutOrig := *os.Stdout 33 | os.Stdout = fh 34 | defer func() { 35 | os.Stdout = &stdoutOrig 36 | }() 37 | 38 | if code := realMain([]string{"consul-migrate", "-h"}); code != 0 { 39 | t.Fatalf("bad: %d", code) 40 | } 41 | 42 | if _, err := fh.Seek(0, 0); err != nil { 43 | t.Fatalf("err: %s", err) 44 | } 45 | out, err := ioutil.ReadAll(fh) 46 | if !strings.HasPrefix(string(out), "Usage:") { 47 | t.Fatalf("bad: %s", string(out)) 48 | } 49 | } 50 | 51 | func TestMain_noop(t *testing.T) { 52 | fh, err := ioutil.TempFile("", "consul-migrate") 53 | if err != nil { 54 | t.Fatalf("err: %s", err) 55 | } 56 | defer os.Remove(fh.Name()) 57 | 58 | stdoutOrig := *os.Stdout 59 | os.Stdout = fh 60 | defer func() { 61 | os.Stdout = &stdoutOrig 62 | }() 63 | 64 | dir, err := ioutil.TempDir("", "consul-migrate") 65 | if err != nil { 66 | t.Fatalf("err: %s", err) 67 | } 68 | defer os.RemoveAll(dir) 69 | 70 | if code := realMain([]string{"consul-migrate", dir}); code != 0 { 71 | t.Fatalf("bad: %d", code) 72 | } 73 | 74 | if _, err := fh.Seek(0, 0); err != nil { 75 | t.Fatalf("err: %s", err) 76 | } 77 | out, err := ioutil.ReadAll(fh) 78 | if err != nil { 79 | t.Fatalf("err: %s", err) 80 | } 81 | if !strings.HasPrefix(string(out), "Nothing to do") { 82 | t.Fatalf("bad: %s", string(out)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | consul-migrate 2 | ============== 3 | 4 | consul-migrate is a Go package and CLI utility to perform a very specific 5 | data migration for Consul servers nodes. Between Consul versions 0.5.0 6 | and 0.5.1, the backend for storing Raft data was changed from LMDB to 7 | BoltDB. To support seamless upgrades, this library is embedded in Consul 8 | version 0.5.1 to perform the upgrade automatically. 9 | 10 | Supported Upgrade Paths 11 | ======================= 12 | 13 | Following is a table detailing the supported upgrade paths for Consul. 14 | 15 | | From Version | To Version | Procedure | 16 | |--------------|------------|-----------------------------------| 17 | | < 0.5.1 | 0.5.1 | Start Consul v0.5.1 normally. | 18 | | < 0.5.1 | 0.6.0+ | Run `consul-migrate` CLI utility. | 19 | 20 | CLI Usage 21 | ========= 22 | 23 | The consul-migrate CLI is very easy to use. It takes only a single argument: 24 | the path to the consul data-dir. Everything else is handled automatically. 25 | 26 | ``` 27 | Usage: consul-migrate 28 | ``` 29 | 30 | What happens to my data? 31 | ======================== 32 | 33 | The following is the high-level overview of what happens when 34 | consul-migrate is invoked, either as a function or using the CLI: 35 | 36 | 1. The provided data-dir is checked for and `mdb` sub-dir. If it exists, 37 | the migration procedure continues to 2. If it does not exist, the 38 | data-dir is checked for the `raft/raft.db` file. If it exists, this 39 | indicates the migration has already completed, and the program exits 40 | with success. If it does not exist, the data-dir is not a consul 41 | data-dir, and the program exits with an error. 42 | 43 | 2. A new BoltDB file is created in the data-dir, at `raft/raft.db.temp`. 44 | All of the Consul data will be migrated to this new DB file. 45 | 46 | 3. Both LMDB and the BoltDB stores are opened. Data is copied out of LMDB 47 | and into BoltDB. No write operations are performed on LMDB. 48 | 49 | 4. The `raft/raft.db.temp` file is moved to `raft/raft.db`. This is the 50 | location where Consul expects to find the bolt file. 51 | 52 | 5. The `mdb` directory in the data-dir is renamed to `mdb.backup`. This 53 | prevents the migration from re-running. At this point, the data is 54 | successfully migrated and ready to use. 55 | 56 | If any of the above steps encounter errors, the entire process is aborted, 57 | and the temporary BoltDB file is removed. The migration can be retried 58 | without negative consequences. 59 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/hashicorp/consul-migrate/migrator" 9 | ) 10 | 11 | func main() { 12 | os.Exit(realMain(os.Args)) 13 | } 14 | 15 | func realMain(args []string) int { 16 | if len(args) != 2 { 17 | fmt.Println(usage()) 18 | return 1 19 | } 20 | 21 | // Observe the help flags 22 | if args[1] == "-h" || args[1] == "--help" { 23 | fmt.Println(usage()) 24 | return 0 25 | } 26 | 27 | // Create the migrator 28 | m, err := migrator.New(args[1]) 29 | if err != nil { 30 | fmt.Printf("Error creating migrator: %s\n", err) 31 | return 1 32 | } 33 | 34 | // Handle progress output 35 | doneCh := make(chan struct{}) 36 | defer close(doneCh) 37 | go handleProgress(m.ProgressCh, doneCh) 38 | 39 | // Perform the migration 40 | start := time.Now() 41 | migrated, err := m.Migrate() 42 | if err != nil { 43 | fmt.Printf("Migration failed: %s\n", err) 44 | return 1 45 | } 46 | 47 | // Check the result 48 | if migrated { 49 | fmt.Printf("Migration completed in %s\n", time.Now().Sub(start)) 50 | } else { 51 | fmt.Printf("Nothing to do for directory '%s'\n", args[1]) 52 | } 53 | return 0 54 | } 55 | 56 | // handleProgress is used to dump progress information to the console while 57 | // a migration is in flight. This allows the user to monitor a migration. 58 | func handleProgress(ch <-chan *migrator.ProgressUpdate, doneCh <-chan struct{}) { 59 | var lastOp string 60 | var lastProgress float64 61 | lastFlush := time.Now() 62 | for { 63 | select { 64 | case update := <-ch: 65 | switch { 66 | case lastOp != update.Op: 67 | lastProgress = update.Progress 68 | lastOp = update.Op 69 | fmt.Println(update.Op) 70 | fmt.Printf("%.2f%%\n", update.Progress) 71 | 72 | case update.Progress-lastProgress >= 5: 73 | fallthrough 74 | 75 | case time.Now().Sub(lastFlush) > time.Second: 76 | fallthrough 77 | 78 | case update.Progress == 100: 79 | lastFlush = time.Now() 80 | lastProgress = update.Progress 81 | fmt.Printf("%.2f%%\n", update.Progress) 82 | } 83 | case <-doneCh: 84 | return 85 | } 86 | } 87 | } 88 | 89 | func usage() string { 90 | return `Usage: consul-migrate 91 | 92 | Consul-migrate is a tool for moving Consul server data from LMDB to BoltDB. 93 | This is a prerequisite for upgrading to Consul >= 0.5.1. 94 | 95 | This utility will migrate all of the data Consul needs to pick up right where 96 | it left off. The original MDB data folder will *NOT* be modified during the 97 | migration process. If any part of the migration fails, it is safe to try again. 98 | This command is also idempotent, and will not re-attempt a migration which has 99 | already been completed. 100 | 101 | Upon successful migration, the MDB data directory will be renamed so that it 102 | includes the ".backup" extension. Once you have verified Consul is operational 103 | after the migration, and contains all of the expected data, it is safe to 104 | archive the "mdb.backup" directory and remove it from the Consul server. 105 | 106 | Returns 0 on successful migration or no-op, 1 for errors. 107 | ` 108 | } 109 | -------------------------------------------------------------------------------- /migrator/migrator.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | 9 | "github.com/hashicorp/raft" 10 | "github.com/hashicorp/raft-boltdb" 11 | "github.com/hashicorp/raft-mdb" 12 | ) 13 | 14 | const ( 15 | // Path to the raft directory 16 | raftDir = "raft" 17 | 18 | // Path to the legacy MDB data and its backup location 19 | mdbDir = "mdb" 20 | mdbBackupDir = "mdb.backup" 21 | 22 | // The name of the BoltDB file in the raft path 23 | boltFile = "raft.db" 24 | boltTempFile = "raft.db.temp" 25 | 26 | // Maximum log sizes for LMDB. These mirror settings in Consul 27 | // and are automatically set based on the runtime. 28 | maxLogSize32bit uint64 = 8 * 1024 * 1024 * 1024 29 | maxLogSize64bit uint64 = 64 * 1024 * 1024 * 1024 30 | ) 31 | 32 | var ( 33 | // Common error messages from migrator 34 | errFirstIndexZero = fmt.Errorf("No logs found (first index was 0)") 35 | errLastIndexZero = fmt.Errorf("No logs found (last index was 0)") 36 | 37 | // stableStoreKeys are the well-known keys written to the 38 | // stable store, and are used internally by Raft. We hard-code 39 | // them here so that we can copy them explicitly. 40 | stableStoreKeys [][]byte = [][]byte{ 41 | []byte("CurrentTerm"), 42 | []byte("LastVoteTerm"), 43 | []byte("LastVoteCand"), 44 | } 45 | ) 46 | 47 | // Migrator is used to migrate the Consul data storage format on 48 | // servers with versions <= 0.5.0. Consul versions >= 0.5.1 use 49 | // BoltDB internally as the store for the Raft log. During this 50 | // transition, it is necessary to copy data out of our LMDB store 51 | // and create a new BoltStore with the same data. 52 | type Migrator struct { 53 | // Channels used to expose what's happening internally 54 | // during a migration. 55 | ProgressCh chan *ProgressUpdate 56 | 57 | dataDir string // The Consul data-dir 58 | mdbStore *raftmdb.MDBStore // The legacy MDB environment 59 | boltStore *raftboltdb.BoltStore // Handle for the new store 60 | 61 | // Calculated paths based on the data dir 62 | raftPath string 63 | mdbPath string 64 | mdbBackupPath string 65 | boltPath string 66 | boltTempPath string 67 | } 68 | 69 | // New creates a new Migrator given the path to a Consul 70 | // data-dir. Returns the new Migrator and any error. 71 | func New(dataDir string) (*Migrator, error) { 72 | // Check that the directory exists 73 | if _, err := os.Stat(dataDir); err != nil { 74 | return nil, err 75 | } 76 | 77 | // Create the struct 78 | m := &Migrator{ 79 | ProgressCh: make(chan *ProgressUpdate, 128), 80 | dataDir: dataDir, 81 | 82 | raftPath: filepath.Join(dataDir, raftDir), 83 | mdbPath: filepath.Join(dataDir, raftDir, mdbDir), 84 | mdbBackupPath: filepath.Join(dataDir, raftDir, mdbBackupDir), 85 | boltPath: filepath.Join(dataDir, raftDir, boltFile), 86 | boltTempPath: filepath.Join(dataDir, raftDir, boltTempFile), 87 | } 88 | 89 | return m, nil 90 | } 91 | 92 | // mdbConnect is used to open a handle on our LMDB raft backend. This 93 | // is enough to read all of the Consul data we need to migrate. 94 | func (m *Migrator) mdbConnect(dir string) error { 95 | // Calculate and set the max size 96 | size := maxLogSize32bit 97 | if runtime.GOARCH == "amd64" { 98 | size = maxLogSize64bit 99 | } 100 | 101 | // Open the connection 102 | mdb, err := raftmdb.NewMDBStoreWithSize(dir, size) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // Return the new environment 108 | m.mdbStore = mdb 109 | return nil 110 | } 111 | 112 | // boltConnect creates a new BoltStore to copy our data into. We can 113 | // use the BoltStore directly because it provides simple setter 114 | // methods, provided our keys and values are known. 115 | func (m *Migrator) boltConnect(file string) error { 116 | // Connect to the new BoltStore 117 | store, err := raftboltdb.NewBoltStore(file) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | m.boltStore = store 123 | return nil 124 | } 125 | 126 | // migrateStableStore copies values out of the origin StableStore 127 | // and writes them into the destination. There are only a handful 128 | // of keys we need, so we copy them explicitly. 129 | func (m *Migrator) migrateStableStore() error { 130 | op := "Migrating stable store" 131 | m.sendProgress(op, 0, 1) 132 | 133 | total := len(stableStoreKeys) 134 | for i, key := range stableStoreKeys { 135 | val, err := m.mdbStore.Get(key) 136 | if err != nil { 137 | if err.Error() != "not found" { 138 | return fmt.Errorf("Error getting key '%s': %s", string(key), err) 139 | } 140 | m.sendProgress(op, i+1, total) 141 | continue 142 | } 143 | if err := m.boltStore.Set(key, val); err != nil { 144 | return fmt.Errorf("Error storing key '%s': %s", string(key), err) 145 | } 146 | m.sendProgress(op, i+1, total) 147 | } 148 | return nil 149 | } 150 | 151 | // migrateLogStore is like migrateStableStore, but iterates over 152 | // all of our Raft logs and copies them into the new BoltStore. 153 | func (m *Migrator) migrateLogStore() error { 154 | op := "Migrating log store" 155 | m.sendProgress(op, 0, 1) 156 | 157 | first, err := m.mdbStore.FirstIndex() 158 | if err != nil { 159 | return err 160 | } 161 | if first == 0 { 162 | return errFirstIndexZero 163 | } 164 | 165 | last, err := m.mdbStore.LastIndex() 166 | if err != nil { 167 | return err 168 | } 169 | if last == 0 { 170 | return errLastIndexZero 171 | } 172 | total := int(last - first) 173 | 174 | current := 0 175 | for i := first; i <= last; i++ { 176 | log := &raft.Log{} 177 | if err := m.mdbStore.GetLog(i, log); err != nil { 178 | return err 179 | } 180 | if err := m.boltStore.StoreLog(log); err != nil { 181 | return err 182 | } 183 | current++ 184 | m.sendProgress(op, current, total) 185 | } 186 | return nil 187 | } 188 | 189 | // activateBoltStore wraps moving the Bolt file into place after 190 | // a data migration has finished successfully. 191 | func (m *Migrator) activateBoltStore() error { 192 | op := "Moving Bolt file into place" 193 | m.sendProgress(op, 0, 1) 194 | 195 | if err := os.Rename(m.boltTempPath, m.boltPath); err != nil { 196 | return err 197 | } 198 | 199 | m.sendProgress(op, 1, 1) 200 | return nil 201 | } 202 | 203 | // archiveMDBStore is used to move the LMDB data directory to a backup 204 | // location so that it is not used by Consul again. 205 | func (m *Migrator) archiveMDBStore() error { 206 | op := "Archiving LMDB data" 207 | m.sendProgress(op, 0, 1) 208 | 209 | if err := os.Rename(m.mdbPath, m.mdbBackupPath); err != nil { 210 | os.Remove(m.boltPath) 211 | return err 212 | } 213 | 214 | m.sendProgress(op, 1, 1) 215 | return nil 216 | } 217 | 218 | // Migrate is the high-level function we call when we want to attempt 219 | // to migrate all of our LMDB data into BoltDB. If an error is 220 | // encountered, the BoltStore is nuked from disk, since it is useless. 221 | // The migration can be attempted again, as the LMDB data should 222 | // still be intact. Returns a bool indicating whether a migration 223 | // was completed, and any error. 224 | func (m *Migrator) Migrate() (bool, error) { 225 | // Check if we should attempt a migration 226 | if _, err := os.Stat(m.mdbPath); os.IsNotExist(err) { 227 | return false, nil 228 | } 229 | 230 | // Connect the stores 231 | if err := m.mdbConnect(m.raftPath); err != nil { 232 | return false, fmt.Errorf("Failed to connect MDB: %s", err) 233 | } 234 | defer m.mdbStore.Close() 235 | 236 | if err := m.boltConnect(m.boltTempPath); err != nil { 237 | return false, fmt.Errorf("Failed to connect BoltDB: %s", err) 238 | } 239 | defer m.boltStore.Close() 240 | 241 | // Ensure we clean up the temp file during failure cases 242 | defer os.Remove(m.boltTempPath) 243 | 244 | // Migrate the stable store 245 | if err := m.migrateStableStore(); err != nil { 246 | return false, fmt.Errorf("Failed to migrate stable store: %v", err) 247 | } 248 | 249 | // Migrate the log store 250 | if err := m.migrateLogStore(); err != nil { 251 | return false, fmt.Errorf("Failed to migrate log store: %v", err) 252 | } 253 | 254 | // Activate the new BoltDB file 255 | if err := m.activateBoltStore(); err != nil { 256 | return false, fmt.Errorf("Failed to activate Bolt store: %s", err) 257 | } 258 | 259 | // Move the old MDB dir to its backup location 260 | if err := m.archiveMDBStore(); err != nil { 261 | return false, fmt.Errorf("Failed to archive LMDB data: %s", err) 262 | } 263 | 264 | return true, nil 265 | } 266 | 267 | // sendProgress is used to send a progress update message to the progress 268 | // channel. Sending is a non-blocking operation. It is the responsibility 269 | // of the caller to ensure they drain the queue promptly to avoid missing 270 | // progress update messages. 271 | func (m *Migrator) sendProgress(op string, done, total int) { 272 | update := ProgressUpdate{op, (float64(done) / float64(total)) * 100} 273 | select { 274 | case m.ProgressCh <- &update: 275 | default: 276 | } 277 | } 278 | 279 | // ProgressUpdate is used to communicate internal progress data about 280 | // an in-flight migration. The Op is the current operation, and the 281 | // Progress indicates a percentage of migrations completed. 282 | type ProgressUpdate struct { 283 | Op string 284 | Progress float64 285 | } 286 | -------------------------------------------------------------------------------- /migrator/migrator_test.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/hashicorp/raft" 15 | ) 16 | 17 | func testRaftDir(t *testing.T) string { 18 | dir, err := ioutil.TempDir("", "consul-migrate") 19 | if err != nil { 20 | t.Fatalf("err: %s", err) 21 | } 22 | 23 | // Make the mdb sub-dir 24 | if err := os.MkdirAll(filepath.Join(dir, "raft", "mdb"), 0700); err != nil { 25 | t.Fatalf("err: %s", err) 26 | } 27 | 28 | // Copy the MDB files 29 | for _, file := range []string{"data.mdb", "lock.mdb"} { 30 | src, err := os.Open(filepath.Join("test-fixtures", raftDir, mdbDir, file)) 31 | if err != nil { 32 | t.Fatalf("err: %s", err) 33 | } 34 | 35 | dest, err := os.Create(filepath.Join(dir, raftDir, mdbDir, file)) 36 | if err != nil { 37 | t.Fatalf("err: %s", err) 38 | } 39 | if _, err := io.Copy(dest, src); err != nil { 40 | t.Fatalf("err: %s", err) 41 | } 42 | src.Close() 43 | dest.Close() 44 | } 45 | 46 | return dir 47 | } 48 | 49 | // Tests our fixture data to make sure that the other tests in this file 50 | // are properly exercising the migrator utility. 51 | func TestMigrator_data(t *testing.T) { 52 | dir := testRaftDir(t) 53 | defer os.RemoveAll(dir) 54 | 55 | m, err := New(dir) 56 | if err != nil { 57 | t.Fatalf("err: %s", err) 58 | } 59 | 60 | if err := m.mdbConnect(m.raftPath); err != nil { 61 | t.Fatalf("err: %s", err) 62 | } 63 | defer m.mdbStore.Close() 64 | 65 | // Ensure we have at least a few logs in the log store 66 | first, err := m.mdbStore.FirstIndex() 67 | if err != nil { 68 | t.Fatalf("err: %s", err) 69 | } 70 | last, err := m.mdbStore.LastIndex() 71 | if err != nil { 72 | t.Fatalf("err: %s", err) 73 | } 74 | if first < 1 || last < 3 { 75 | t.Fatalf("bad index: first=%d, last=%d", first, last) 76 | } 77 | 78 | // Ensure we have the stable store keys 79 | for _, key := range stableStoreKeys { 80 | if _, err := m.mdbStore.Get(key); err != nil { 81 | t.Fatalf("missing stable store key: %s", string(key)) 82 | } 83 | } 84 | } 85 | 86 | func TestMigrator_new(t *testing.T) { 87 | // Fails on bad data-dir 88 | if _, err := New("/leprechauns"); err == nil { 89 | t.Fatalf("should fail") 90 | } 91 | 92 | // Works with an existing directory 93 | dir := testRaftDir(t) 94 | defer os.RemoveAll(dir) 95 | 96 | m, err := New(dir) 97 | if err != nil { 98 | t.Fatalf("err: %s", err) 99 | } 100 | 101 | // Check the paths 102 | if m.raftPath != filepath.Join(dir, raftDir) { 103 | t.Fatalf("bad: %s", m.raftPath) 104 | } 105 | if m.mdbPath != filepath.Join(dir, raftDir, mdbDir) { 106 | t.Fatalf("bad: %s", m.mdbPath) 107 | } 108 | if m.mdbBackupPath != filepath.Join(dir, raftDir, mdbBackupDir) { 109 | t.Fatalf("bad: %s", m.mdbBackupPath) 110 | } 111 | if m.boltPath != filepath.Join(dir, raftDir, boltFile) { 112 | t.Fatalf("bad: %s", m.boltPath) 113 | } 114 | if m.boltTempPath != filepath.Join(dir, raftDir, boltTempFile) { 115 | t.Fatalf("err: %s", err) 116 | } 117 | } 118 | 119 | func TestMigrator_migrate(t *testing.T) { 120 | dir := testRaftDir(t) 121 | defer os.RemoveAll(dir) 122 | 123 | // Create the migrator 124 | m, err := New(dir) 125 | if err != nil { 126 | t.Fatalf("err: %s", err) 127 | } 128 | 129 | // Perform the migration 130 | if _, err := m.Migrate(); err != nil { 131 | t.Fatalf("err: %s", err) 132 | } 133 | 134 | // Check that the temp bolt file was removed 135 | if _, err := os.Stat(m.boltTempPath); err == nil { 136 | t.Fatalf("did not remove temp bolt file") 137 | } 138 | 139 | // Check that the new BoltStore was created 140 | if _, err := os.Stat(m.boltPath); err != nil { 141 | t.Fatalf("missing bolt file: %s", err) 142 | } 143 | 144 | // Check that the MDB store was backed up 145 | if _, err := os.Stat(m.mdbPath); err == nil { 146 | t.Fatalf("MDB dir was not moved") 147 | } 148 | if _, err := os.Stat(m.mdbBackupPath); err != nil { 149 | t.Fatalf("Missing MDB backup dir") 150 | } 151 | 152 | // Reconnect the data sources. Requires moving the MDB 153 | // store back to its original location. 154 | if err := os.Rename(m.mdbBackupPath, m.mdbPath); err != nil { 155 | t.Fatalf("err: %s", err) 156 | } 157 | 158 | if err := m.mdbConnect(m.raftPath); err != nil { 159 | t.Fatalf("err: %s", err) 160 | } 161 | defer m.mdbStore.Close() 162 | 163 | if err := m.boltConnect(m.boltPath); err != nil { 164 | t.Fatalf("err: %s", err) 165 | } 166 | defer m.boltStore.Close() 167 | 168 | // Check that the BoltStore now has the indexes 169 | mFirst, err := m.mdbStore.FirstIndex() 170 | if err != nil { 171 | t.Fatalf("err: %s", err) 172 | } 173 | mLast, err := m.mdbStore.LastIndex() 174 | if err != nil { 175 | t.Fatalf("err: %s", err) 176 | } 177 | bFirst, err := m.boltStore.FirstIndex() 178 | if err != nil { 179 | t.Fatalf("err: %s", err) 180 | } 181 | if bFirst != mFirst { 182 | t.Fatalf("bad: %d", bFirst) 183 | } 184 | bLast, err := m.boltStore.LastIndex() 185 | if err != nil { 186 | t.Fatalf("err: %s", err) 187 | } 188 | if bLast != mLast { 189 | t.Fatalf("bad: %d", bLast) 190 | } 191 | 192 | // Ensure that the logs were copied properly 193 | for i := mFirst; i <= mLast; i++ { 194 | mLog := &raft.Log{} 195 | if err := m.mdbStore.GetLog(i, mLog); err != nil { 196 | t.Fatalf("err: %s", err) 197 | } 198 | bLog := &raft.Log{} 199 | if err := m.boltStore.GetLog(i, bLog); err != nil { 200 | t.Fatalf("err: %s", err) 201 | } 202 | if !reflect.DeepEqual(mLog, bLog) { 203 | t.Fatalf("bad: %v %v", mLog, bLog) 204 | } 205 | } 206 | 207 | // Ensure the stable store values were copied. 208 | for _, key := range stableStoreKeys { 209 | mVal, err := m.mdbStore.Get(key) 210 | if err != nil { 211 | t.Fatalf("err: %s", err) 212 | } 213 | bVal, err := m.boltStore.Get(key) 214 | if err != nil { 215 | t.Fatalf("err: %s", err) 216 | } 217 | if !bytes.Equal(mVal, bVal) { 218 | t.Fatalf("bad value for key '%s'", key) 219 | } 220 | } 221 | } 222 | 223 | func TestMigrator_migrate_fails(t *testing.T) { 224 | // Create a new temp dir. We will attempt to use this 225 | // as the Raft dir to create errors. 226 | dir, err := ioutil.TempDir("", "consul-migrate") 227 | if err != nil { 228 | t.Fatalf("err: %s", err) 229 | } 230 | defer os.RemoveAll(dir) 231 | 232 | // Create the migrator 233 | m, err := New(dir) 234 | if err != nil { 235 | t.Fatalf("err: %s", err) 236 | } 237 | 238 | // Create the MDB path so that the migrator will run 239 | if err := os.MkdirAll(m.mdbPath, 0700); err != nil { 240 | t.Fatalf("err: %s", err) 241 | } 242 | 243 | // Attempt the migration. This will fail. 244 | migrated, err := m.Migrate() 245 | if !strings.Contains(err.Error(), errFirstIndexZero.Error()) { 246 | t.Fatalf("bad: %s", err) 247 | } 248 | if migrated { 249 | t.Fatalf("should not have migrated") 250 | } 251 | 252 | // Ensure that the MDB dir was not moved 253 | if _, err := os.Stat(m.mdbPath); err != nil { 254 | t.Fatalf("err: %s", err) 255 | } 256 | if _, err := os.Stat(m.mdbBackupPath); !os.IsNotExist(err) { 257 | t.Fatalf("err: %s", err) 258 | } 259 | 260 | // Ensure no BoltDB files were left 261 | if _, err := os.Stat(m.boltPath); !os.IsNotExist(err) { 262 | t.Fatalf("err: %s", err) 263 | } 264 | if _, err := os.Stat(m.boltTempPath); !os.IsNotExist(err) { 265 | t.Fatalf("err: %s", err) 266 | } 267 | } 268 | 269 | func TestMigrator_migrate_noop(t *testing.T) { 270 | dir, err := ioutil.TempDir("", "consul-migrate") 271 | if err != nil { 272 | t.Fatalf("err: %s", err) 273 | } 274 | defer os.RemoveAll(dir) 275 | 276 | // Should return no-op if MDB dir doesn't exist 277 | m, err := New(dir) 278 | if err != nil { 279 | t.Fatalf("err: %s", err) 280 | } 281 | migrated, err := m.Migrate() 282 | if err != nil { 283 | t.Fatalf("err: %s", err) 284 | } 285 | if migrated { 286 | t.Fatalf("should not have migrated") 287 | } 288 | } 289 | 290 | func TestMigrator_sendProgress(t *testing.T) { 291 | dir, err := ioutil.TempDir("", "consul-migrate") 292 | if err != nil { 293 | t.Fatalf("err: %s", err) 294 | } 295 | defer os.RemoveAll(dir) 296 | 297 | // Create the migrator 298 | m, err := New(dir) 299 | if err != nil { 300 | t.Fatalf("err: %s", err) 301 | } 302 | 303 | // Send a progress update 304 | m.sendProgress("doing something", 5, 20) 305 | 306 | // Check the message and format 307 | select { 308 | case update := <-m.ProgressCh: 309 | if update.Op != "doing something" { 310 | t.Fatalf("bad: %#v", update) 311 | } 312 | if update.Progress != 25 { 313 | t.Fatalf("bad: %#v", update) 314 | } 315 | case <-time.After(time.Second): 316 | t.Fatalf("timed out waiting for update") 317 | } 318 | } 319 | 320 | func TestMigrator_progressUpdate(t *testing.T) { 321 | dir := testRaftDir(t) 322 | defer os.RemoveAll(dir) 323 | 324 | // Create the migrator 325 | m, err := New(dir) 326 | if err != nil { 327 | t.Fatalf("err: %s", err) 328 | } 329 | 330 | // Listen for progress updates in the background 331 | recvd := 0 332 | doneCh := make(chan struct{}) 333 | defer close(doneCh) 334 | go func() { 335 | for { 336 | select { 337 | case <-m.ProgressCh: 338 | recvd++ 339 | case <-doneCh: 340 | return 341 | } 342 | } 343 | }() 344 | 345 | // Perform the migration 346 | if _, err := m.Migrate(); err != nil { 347 | t.Fatalf("err: %s", err) 348 | } 349 | 350 | // Check if we got the updates 351 | if recvd == 0 { 352 | t.Fatalf("missing progress updates") 353 | } 354 | } 355 | 356 | func TestMigrator_progressUpdate_nonblocking(t *testing.T) { 357 | dir := testRaftDir(t) 358 | defer os.RemoveAll(dir) 359 | 360 | // Create the migrator 361 | m, err := New(dir) 362 | if err != nil { 363 | t.Fatalf("err: %s", err) 364 | } 365 | 366 | // Replace the progress channel with an unbuffered one 367 | m.ProgressCh = make(chan *ProgressUpdate, 1) 368 | 369 | // Run the migration. This should not block trying to 370 | // push progress messages onto the channel. Simulates 371 | // a consumer not pulling messages off of the channel. 372 | if _, err := m.Migrate(); err != nil { 373 | t.Fatalf("err: %s", err) 374 | } 375 | 376 | // Should have the single update in the channel 377 | if len(m.ProgressCh) != 1 { 378 | t.Fatalf("missing progress update") 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. “Contributor” 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. “Contributor Version” 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor’s Contribution. 14 | 15 | 1.3. “Contribution” 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. “Covered Software” 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. “Incompatible With Secondary Licenses” 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of version 33 | 1.1 or earlier of the License, but not also under the terms of a 34 | Secondary License. 35 | 36 | 1.6. “Executable Form” 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. “Larger Work” 41 | 42 | means a work that combines Covered Software with other material, in a separate 43 | file or files, that is not Covered Software. 44 | 45 | 1.8. “License” 46 | 47 | means this document. 48 | 49 | 1.9. “Licensable” 50 | 51 | means having the right to grant, to the maximum extent possible, whether at the 52 | time of the initial grant or subsequently, any and all of the rights conveyed by 53 | this License. 54 | 55 | 1.10. “Modifications” 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, deletion 60 | from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. “Patent Claims” of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, process, 67 | and apparatus claims, in any patent Licensable by such Contributor that 68 | would be infringed, but for the grant of the License, by the making, 69 | using, selling, offering for sale, having made, import, or transfer of 70 | either its Contributions or its Contributor Version. 71 | 72 | 1.12. “Secondary License” 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. “Source Code Form” 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. “You” (or “Your”) 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, “You” includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, “control” means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or as 104 | part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its Contributions 108 | or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution become 113 | effective for each Contribution on the date the Contributor first distributes 114 | such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under this 119 | License. No additional rights or licenses will be implied from the distribution 120 | or licensing of Covered Software under this License. Notwithstanding Section 121 | 2.1(b) above, no patent license is granted by a Contributor: 122 | 123 | a. for any code that a Contributor has removed from Covered Software; or 124 | 125 | b. for infringements caused by: (i) Your and any other third party’s 126 | modifications of Covered Software, or (ii) the combination of its 127 | Contributions with other software (except as part of its Contributor 128 | Version); or 129 | 130 | c. under Patent Claims infringed by Covered Software in the absence of its 131 | Contributions. 132 | 133 | This License does not grant any rights in the trademarks, service marks, or 134 | logos of any Contributor (except as may be necessary to comply with the 135 | notice requirements in Section 3.4). 136 | 137 | 2.4. Subsequent Licenses 138 | 139 | No Contributor makes additional grants as a result of Your choice to 140 | distribute the Covered Software under a subsequent version of this License 141 | (see Section 10.2) or under the terms of a Secondary License (if permitted 142 | under the terms of Section 3.3). 143 | 144 | 2.5. Representation 145 | 146 | Each Contributor represents that the Contributor believes its Contributions 147 | are its original creation(s) or it has sufficient rights to grant the 148 | rights to its Contributions conveyed by this License. 149 | 150 | 2.6. Fair Use 151 | 152 | This License is not intended to limit any rights You have under applicable 153 | copyright doctrines of fair use, fair dealing, or other equivalents. 154 | 155 | 2.7. Conditions 156 | 157 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 158 | Section 2.1. 159 | 160 | 161 | 3. Responsibilities 162 | 163 | 3.1. Distribution of Source Form 164 | 165 | All distribution of Covered Software in Source Code Form, including any 166 | Modifications that You create or to which You contribute, must be under the 167 | terms of this License. You must inform recipients that the Source Code Form 168 | of the Covered Software is governed by the terms of this License, and how 169 | they can obtain a copy of this License. You may not attempt to alter or 170 | restrict the recipients’ rights in the Source Code Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | a. such Covered Software must also be made available in Source Code Form, 177 | as described in Section 3.1, and You must inform recipients of the 178 | Executable Form how they can obtain a copy of such Source Code Form by 179 | reasonable means in a timely manner, at a charge no more than the cost 180 | of distribution to the recipient; and 181 | 182 | b. You may distribute such Executable Form under the terms of this License, 183 | or sublicense it under different terms, provided that the license for 184 | the Executable Form does not attempt to limit or alter the recipients’ 185 | rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for the 191 | Covered Software. If the Larger Work is a combination of Covered Software 192 | with a work governed by one or more Secondary Licenses, and the Covered 193 | Software is not Incompatible With Secondary Licenses, this License permits 194 | You to additionally distribute such Covered Software under the terms of 195 | such Secondary License(s), so that the recipient of the Larger Work may, at 196 | their option, further distribute the Covered Software under the terms of 197 | either this License or such Secondary License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices (including 202 | copyright notices, patent notices, disclaimers of warranty, or limitations 203 | of liability) contained within the Source Code Form of the Covered 204 | Software, except that You may alter any license notices to the extent 205 | required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on behalf 212 | of any Contributor. You must make it absolutely clear that any such 213 | warranty, support, indemnity, or liability obligation is offered by You 214 | alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | 222 | If it is impossible for You to comply with any of the terms of this License 223 | with respect to some or all of the Covered Software due to statute, judicial 224 | order, or regulation then You must: (a) comply with the terms of this License 225 | to the maximum extent possible; and (b) describe the limitations and the code 226 | they affect. Such description must be placed in a text file included with all 227 | distributions of the Covered Software under this License. Except to the 228 | extent prohibited by statute or regulation, such description must be 229 | sufficiently detailed for a recipient of ordinary skill to be able to 230 | understand it. 231 | 232 | 5. Termination 233 | 234 | 5.1. The rights granted under this License will terminate automatically if You 235 | fail to comply with any of its terms. However, if You become compliant, 236 | then the rights granted under this License from a particular Contributor 237 | are reinstated (a) provisionally, unless and until such Contributor 238 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 239 | if such Contributor fails to notify You of the non-compliance by some 240 | reasonable means prior to 60 days after You have come back into compliance. 241 | Moreover, Your grants from a particular Contributor are reinstated on an 242 | ongoing basis if such Contributor notifies You of the non-compliance by 243 | some reasonable means, this is the first time You have received notice of 244 | non-compliance with this License from such Contributor, and You become 245 | compliant prior to 30 days after Your receipt of the notice. 246 | 247 | 5.2. If You initiate litigation against any entity by asserting a patent 248 | infringement claim (excluding declaratory judgment actions, counter-claims, 249 | and cross-claims) alleging that a Contributor Version directly or 250 | indirectly infringes any patent, then the rights granted to You by any and 251 | all Contributors for the Covered Software under Section 2.1 of this License 252 | shall terminate. 253 | 254 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 255 | license agreements (excluding distributors and resellers) which have been 256 | validly granted by You or Your distributors under this License prior to 257 | termination shall survive termination. 258 | 259 | 6. Disclaimer of Warranty 260 | 261 | Covered Software is provided under this License on an “as is” basis, without 262 | warranty of any kind, either expressed, implied, or statutory, including, 263 | without limitation, warranties that the Covered Software is free of defects, 264 | merchantable, fit for a particular purpose or non-infringing. The entire 265 | risk as to the quality and performance of the Covered Software is with You. 266 | Should any Covered Software prove defective in any respect, You (not any 267 | Contributor) assume the cost of any necessary servicing, repair, or 268 | correction. This disclaimer of warranty constitutes an essential part of this 269 | License. No use of any Covered Software is authorized under this License 270 | except under this disclaimer. 271 | 272 | 7. Limitation of Liability 273 | 274 | Under no circumstances and under no legal theory, whether tort (including 275 | negligence), contract, or otherwise, shall any Contributor, or anyone who 276 | distributes Covered Software as permitted above, be liable to You for any 277 | direct, indirect, special, incidental, or consequential damages of any 278 | character including, without limitation, damages for lost profits, loss of 279 | goodwill, work stoppage, computer failure or malfunction, or any and all 280 | other commercial damages or losses, even if such party shall have been 281 | informed of the possibility of such damages. This limitation of liability 282 | shall not apply to liability for death or personal injury resulting from such 283 | party’s negligence to the extent applicable law prohibits such limitation. 284 | Some jurisdictions do not allow the exclusion or limitation of incidental or 285 | consequential damages, so this exclusion and limitation may not apply to You. 286 | 287 | 8. Litigation 288 | 289 | Any litigation relating to this License may be brought only in the courts of 290 | a jurisdiction where the defendant maintains its principal place of business 291 | and such litigation shall be governed by laws of that jurisdiction, without 292 | reference to its conflict-of-law provisions. Nothing in this Section shall 293 | prevent a party’s ability to bring cross-claims or counter-claims. 294 | 295 | 9. Miscellaneous 296 | 297 | This License represents the complete agreement concerning the subject matter 298 | hereof. If any provision of this License is held to be unenforceable, such 299 | provision shall be reformed only to the extent necessary to make it 300 | enforceable. Any law or regulation which provides that the language of a 301 | contract shall be construed against the drafter shall not be used to construe 302 | this License against a Contributor. 303 | 304 | 305 | 10. Versions of the License 306 | 307 | 10.1. New Versions 308 | 309 | Mozilla Foundation is the license steward. Except as provided in Section 310 | 10.3, no one other than the license steward has the right to modify or 311 | publish new versions of this License. Each version will be given a 312 | distinguishing version number. 313 | 314 | 10.2. Effect of New Versions 315 | 316 | You may distribute the Covered Software under the terms of the version of 317 | the License under which You originally received the Covered Software, or 318 | under the terms of any subsequent version published by the license 319 | steward. 320 | 321 | 10.3. Modified Versions 322 | 323 | If you create software not governed by this License, and you want to 324 | create a new license for such software, you may create and use a modified 325 | version of this License if you rename the license and remove any 326 | references to the name of the license steward (except to note that such 327 | modified license differs from this License). 328 | 329 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 330 | If You choose to distribute Source Code Form that is Incompatible With 331 | Secondary Licenses under the terms of this version of the License, the 332 | notice described in Exhibit B of this License must be attached. 333 | 334 | Exhibit A - Source Code Form License Notice 335 | 336 | This Source Code Form is subject to the 337 | terms of the Mozilla Public License, v. 338 | 2.0. If a copy of the MPL was not 339 | distributed with this file, You can 340 | obtain one at 341 | http://mozilla.org/MPL/2.0/. 342 | 343 | If it is not possible or desirable to put the notice in a particular file, then 344 | You may include the notice in a location (such as a LICENSE file in a relevant 345 | directory) where a recipient would be likely to look for such a notice. 346 | 347 | You may add additional accurate notices of copyright ownership. 348 | 349 | Exhibit B - “Incompatible With Secondary Licenses” Notice 350 | 351 | This Source Code Form is “Incompatible 352 | With Secondary Licenses”, as defined by 353 | the Mozilla Public License, v. 2.0. 354 | 355 | --------------------------------------------------------------------------------