Skip to content

Commit 1180f3a

Browse files
committed
Create a v2 snapshot when running etcdutl migrate command
Also added test to cover the etcdutl migrate command Signed-off-by: Benjamin Wang <[email protected]>
1 parent 2dbb689 commit 1180f3a

File tree

5 files changed

+377
-29
lines changed

5 files changed

+377
-29
lines changed

etcdutl/etcdutl/common.go

+90
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ package etcdutl
1616

1717
import (
1818
"errors"
19+
"fmt"
1920

2021
"go.uber.org/zap"
2122
"go.uber.org/zap/zapcore"
2223

2324
"go.etcd.io/etcd/client/pkg/v3/logutil"
2425
"go.etcd.io/etcd/pkg/v3/cobrautl"
26+
"go.etcd.io/etcd/server/v3/etcdserver"
27+
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
2528
"go.etcd.io/etcd/server/v3/etcdserver/api/snap"
29+
"go.etcd.io/etcd/server/v3/storage/backend"
2630
"go.etcd.io/etcd/server/v3/storage/datadir"
31+
"go.etcd.io/etcd/server/v3/storage/schema"
2732
"go.etcd.io/etcd/server/v3/storage/wal"
2833
"go.etcd.io/etcd/server/v3/storage/wal/walpb"
2934
"go.etcd.io/raft/v3/raftpb"
@@ -68,3 +73,88 @@ func getLatestV2Snapshot(lg *zap.Logger, dataDir string) (*raftpb.Snapshot, erro
6873

6974
return snapshot, nil
7075
}
76+
77+
func createV2SnapshotFromV3Store(dataDir string, be backend.Backend) error {
78+
var (
79+
lg = GetLogger()
80+
81+
snapDir = datadir.ToSnapDir(dataDir)
82+
walDir = datadir.ToWALDir(dataDir)
83+
)
84+
85+
ci, term := schema.ReadConsistentIndex(be.ReadTx())
86+
87+
cl := membership.NewCluster(lg)
88+
cl.SetBackend(schema.NewMembershipBackend(lg, be))
89+
cl.UnsafeLoad()
90+
91+
latestWALSnap, err := getLatestWALSnap(lg, dataDir)
92+
if err != nil {
93+
return err
94+
}
95+
96+
// Each time before creating the v2 snapshot, etcdserve always flush
97+
// the backend storage (bbolt db), so the consistent index should never
98+
// less than the Index or term of the latest snapshot.
99+
if ci < latestWALSnap.Index || term < latestWALSnap.Term {
100+
// This should never happen
101+
return fmt.Errorf("consistent_index [Index: %d, Term: %d] is less than the latest snapshot [Index: %d, Term: %d]", ci, term, latestWALSnap.Index, latestWALSnap.Term)
102+
}
103+
104+
voters, learners := getVotersAndLearners(cl)
105+
confState := raftpb.ConfState{
106+
Voters: voters,
107+
Learners: learners,
108+
}
109+
110+
// create the v2 snaspshot file
111+
raftSnap := raftpb.Snapshot{
112+
Data: etcdserver.GetMembershipInfoInV2Format(lg, cl),
113+
Metadata: raftpb.SnapshotMetadata{
114+
Index: ci,
115+
Term: term,
116+
ConfState: confState,
117+
},
118+
}
119+
sn := snap.New(lg, snapDir)
120+
if err = sn.SaveSnap(raftSnap); err != nil {
121+
return err
122+
}
123+
124+
// save WAL snapshot record
125+
w, err := wal.Open(lg, walDir, latestWALSnap)
126+
if err != nil {
127+
return err
128+
}
129+
defer w.Close()
130+
// We must read all records to locate the tail of the last valid WAL file.
131+
_, st, _, err := w.ReadAll()
132+
if err != nil {
133+
return err
134+
}
135+
136+
if err := w.SaveSnapshot(walpb.Snapshot{Index: ci, Term: term, ConfState: &confState}); err != nil {
137+
return err
138+
}
139+
if err := w.Save(raftpb.HardState{Term: term, Commit: ci, Vote: st.Vote}, nil); err != nil {
140+
return err
141+
}
142+
return w.Sync()
143+
}
144+
145+
func getVotersAndLearners(cl *membership.RaftCluster) ([]uint64, []uint64) {
146+
var (
147+
voters []uint64
148+
learners []uint64
149+
)
150+
for _, m := range cl.Members() {
151+
if m.IsLearner {
152+
learners = append(learners, uint64(m.ID))
153+
continue
154+
}
155+
156+
voters = append(voters, uint64(m.ID))
157+
}
158+
159+
return voters, learners
160+
}

etcdutl/etcdutl/common_test.go

+191
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,26 @@
1515
package etcdutl
1616

1717
import (
18+
"path"
19+
"path/filepath"
1820
"testing"
1921

22+
"github.com/coreos/go-semver/semver"
2023
"github.com/stretchr/testify/require"
2124
"go.uber.org/zap"
2225

2326
"go.etcd.io/etcd/api/v3/etcdserverpb"
2427
"go.etcd.io/etcd/client/pkg/v3/fileutil"
28+
"go.etcd.io/etcd/client/pkg/v3/types"
2529
"go.etcd.io/etcd/pkg/v3/pbutil"
30+
"go.etcd.io/etcd/server/v3/etcdserver"
31+
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
2632
"go.etcd.io/etcd/server/v3/etcdserver/api/snap"
33+
"go.etcd.io/etcd/server/v3/etcdserver/api/v2store"
34+
"go.etcd.io/etcd/server/v3/etcdserver/cindex"
35+
"go.etcd.io/etcd/server/v3/storage/backend"
2736
"go.etcd.io/etcd/server/v3/storage/datadir"
37+
"go.etcd.io/etcd/server/v3/storage/schema"
2838
"go.etcd.io/etcd/server/v3/storage/wal"
2939
"go.etcd.io/etcd/server/v3/storage/wal/walpb"
3040
"go.etcd.io/raft/v3/raftpb"
@@ -141,3 +151,184 @@ func TestGetLatestWalSnap(t *testing.T) {
141151
})
142152
}
143153
}
154+
155+
func TestCreateV2SnapshotFromV3Store(t *testing.T) {
156+
testCases := []struct {
157+
name string
158+
consistentIndex uint64
159+
term uint64
160+
clusterVersion string
161+
members []uint64
162+
learners []uint64
163+
removedMembers []uint64
164+
expectedErrMsg string
165+
}{
166+
{
167+
name: "unexpected term: less than the last snapshot.term",
168+
consistentIndex: 3,
169+
term: 1,
170+
expectedErrMsg: "less than the latest snapshot",
171+
},
172+
{
173+
name: "unexpected consistent index: less than the last snapshot.index",
174+
consistentIndex: 1,
175+
term: 3,
176+
expectedErrMsg: "less than the latest snapshot",
177+
},
178+
{
179+
name: "normal case",
180+
consistentIndex: 32,
181+
term: 4,
182+
clusterVersion: "3.5.0",
183+
members: []uint64{100, 200},
184+
learners: []uint64{300},
185+
removedMembers: []uint64{400, 500},
186+
},
187+
{
188+
name: "empty cluster version",
189+
consistentIndex: 45,
190+
term: 4,
191+
clusterVersion: "",
192+
members: []uint64{110, 200},
193+
learners: []uint64{350},
194+
removedMembers: []uint64{450, 500},
195+
},
196+
{
197+
name: "no learner",
198+
consistentIndex: 7,
199+
term: 5,
200+
clusterVersion: "3.5.0",
201+
members: []uint64{150, 200},
202+
removedMembers: []uint64{450, 550},
203+
},
204+
{
205+
name: "no removed members",
206+
consistentIndex: 34,
207+
term: 6,
208+
clusterVersion: "3.7.0",
209+
members: []uint64{160, 200},
210+
learners: []uint64{300},
211+
},
212+
{
213+
name: "no learner and removed members",
214+
consistentIndex: 19,
215+
term: 5,
216+
clusterVersion: "3.6.0",
217+
members: []uint64{120, 220},
218+
},
219+
}
220+
221+
for _, tc := range testCases {
222+
t.Run(tc.name, func(t *testing.T) {
223+
dataDir := t.TempDir()
224+
lg := zap.NewNop()
225+
226+
require.NoError(t, fileutil.TouchDirAll(lg, datadir.ToMemberDir(dataDir)))
227+
require.NoError(t, fileutil.TouchDirAll(lg, datadir.ToWALDir(dataDir)))
228+
require.NoError(t, fileutil.TouchDirAll(lg, datadir.ToSnapDir(dataDir)))
229+
230+
t.Log("Populate the wal file")
231+
w, err := wal.Create(lg, datadir.ToWALDir(dataDir), pbutil.MustMarshal(
232+
&etcdserverpb.Metadata{
233+
NodeID: 1,
234+
ClusterID: 2,
235+
},
236+
))
237+
require.NoError(t, err)
238+
err = w.SaveSnapshot(walpb.Snapshot{Index: 2, Term: 2, ConfState: &raftpb.ConfState{Voters: []uint64{1}}})
239+
require.NoError(t, err)
240+
err = w.Save(raftpb.HardState{Term: 2, Commit: 2, Vote: 1}, nil)
241+
require.NoError(t, err)
242+
err = w.Close()
243+
require.NoError(t, err)
244+
245+
t.Log("Generate a v2 snapshot file")
246+
ss := snap.New(lg, datadir.ToSnapDir(dataDir))
247+
err = ss.SaveSnap(raftpb.Snapshot{Metadata: raftpb.SnapshotMetadata{Index: 2, Term: 2, ConfState: raftpb.ConfState{Voters: []uint64{1}}}})
248+
require.NoError(t, err)
249+
250+
t.Log("Load and verify the latest v2 snapshot file")
251+
oldV2Snap, err := getLatestV2Snapshot(lg, dataDir)
252+
require.NoError(t, err)
253+
require.Equal(t, raftpb.SnapshotMetadata{Index: 2, Term: 2, ConfState: raftpb.ConfState{Voters: []uint64{1}}}, oldV2Snap.Metadata)
254+
255+
t.Log("Prepare the bbolt db")
256+
be := backend.NewDefaultBackend(lg, filepath.Join(dataDir, "member/snap/db"))
257+
schema.CreateMetaBucket(be.BatchTx())
258+
schema.NewMembershipBackend(lg, be).MustCreateBackendBuckets()
259+
260+
if len(tc.clusterVersion) > 0 {
261+
t.Logf("Populate the cluster version: %s", tc.clusterVersion)
262+
schema.NewMembershipBackend(lg, be).MustSaveClusterVersionToBackend(semver.New(tc.clusterVersion))
263+
} else {
264+
t.Log("Skip populating cluster version due to not provided")
265+
}
266+
267+
tx := be.BatchTx()
268+
tx.LockOutsideApply()
269+
t.Log("Populate the consistent index and term")
270+
ci := cindex.NewConsistentIndex(be)
271+
ci.SetConsistentIndex(tc.consistentIndex, tc.term)
272+
ci.UnsafeSave(tx)
273+
tx.Unlock()
274+
275+
t.Logf("Populate members: %d", len(tc.members))
276+
memberBackend := schema.NewMembershipBackend(lg, be)
277+
for _, mID := range tc.members {
278+
memberBackend.MustSaveMemberToBackend(&membership.Member{ID: types.ID(mID)})
279+
}
280+
281+
t.Logf("Populate learner: %d", len(tc.learners))
282+
for _, mID := range tc.learners {
283+
memberBackend.MustSaveMemberToBackend(&membership.Member{ID: types.ID(mID), RaftAttributes: membership.RaftAttributes{IsLearner: true}})
284+
}
285+
286+
t.Logf("Populate removed members: %d", len(tc.removedMembers))
287+
for _, mID := range tc.removedMembers {
288+
memberBackend.MustDeleteMemberFromBackend(types.ID(mID))
289+
}
290+
291+
t.Log("Committing bbolt db")
292+
be.ForceCommit()
293+
require.NoError(t, be.Close())
294+
295+
t.Log("Creating a new v2 snapshot file based on the v3 store")
296+
err = createV2SnapshotFromV3Store(dataDir, backend.NewDefaultBackend(lg, filepath.Join(dataDir, "member/snap/db")))
297+
if len(tc.expectedErrMsg) > 0 {
298+
require.ErrorContains(t, err, tc.expectedErrMsg)
299+
return
300+
}
301+
require.NoError(t, err)
302+
303+
t.Log("Loading & verifying the new latest v2 snapshot file")
304+
newV2Snap, err := getLatestV2Snapshot(lg, dataDir)
305+
require.NoError(t, err)
306+
require.Equal(t, raftpb.SnapshotMetadata{Index: tc.consistentIndex, Term: tc.term, ConfState: raftpb.ConfState{Voters: tc.members, Learners: tc.learners}}, newV2Snap.Metadata)
307+
308+
st := v2store.New(etcdserver.StoreClusterPrefix, etcdserver.StoreKeysPrefix)
309+
require.NoError(t, st.Recovery(newV2Snap.Data))
310+
311+
cv, err := st.Get(path.Join(etcdserver.StoreClusterPrefix, "version"), false, false)
312+
if len(tc.clusterVersion) > 0 {
313+
require.NoError(t, err)
314+
if !semver.New(*cv.Node.Value).Equal(*semver.New(tc.clusterVersion)) {
315+
t.Fatalf("Unexpected cluster version, got %s, want %s", semver.New(*cv.Node.Value).String(), tc.clusterVersion)
316+
}
317+
} else {
318+
require.ErrorContains(t, err, "Key not found")
319+
}
320+
321+
members, err := st.Get(path.Join(etcdserver.StoreClusterPrefix, "members"), true, true)
322+
require.NoError(t, err)
323+
require.Len(t, members.Node.Nodes, len(tc.members)+len(tc.learners))
324+
325+
removedMembers, err := st.Get(path.Join(etcdserver.StoreClusterPrefix, "removed_members"), true, true)
326+
if len(tc.removedMembers) > 0 {
327+
require.NoError(t, err)
328+
require.Equal(t, len(tc.removedMembers), len(removedMembers.Node.Nodes))
329+
} else {
330+
require.ErrorContains(t, err, "Key not found")
331+
}
332+
})
333+
}
334+
}

0 commit comments

Comments
 (0)