From fa6426f803b8a4f8559519db82596613d36e8575 Mon Sep 17 00:00:00 2001 From: Christian Schwarz Date: Wed, 2 May 2018 21:26:56 +0200 Subject: [PATCH] WIP: zfs: hacky resume token parsing --- zfs/resume_token.go | 105 +++++++++++++++++++++++++++++++++++++++ zfs/resume_token_test.go | 64 ++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 zfs/resume_token.go create mode 100644 zfs/resume_token_test.go diff --git a/zfs/resume_token.go b/zfs/resume_token.go new file mode 100644 index 0000000..4423626 --- /dev/null +++ b/zfs/resume_token.go @@ -0,0 +1,105 @@ +package zfs + +import ( + "context" + "errors" + "os/exec" + "regexp" + "strconv" + "time" +) + +type ResumeToken struct { + HasFromGUID, HasToGUID bool + FromGUID, ToGUID uint64 + // no support for other fields +} + +var resumeTokenNVListRE = regexp.MustCompile(`\t(\S+) = (.*)`) +var resumeTokenContentsRE = regexp.MustCompile(`resume token contents:\nnvlist version: 0`) +var resumeTokenIsCorruptRE = regexp.MustCompile(`resume token is corrupt`) + +var ResumeTokenCorruptError = errors.New("resume token is corrupt") +var ResumeTokenDecodingNotSupported = errors.New("zfs binary does not allow decoding resume token or zrepl cannot scrape zfs output") +var ResumeTokenParsingError = errors.New("zrepl cannot parse resume token values") + +// Abuse 'zfs send' to decode the resume token +// +// FIXME: implement nvlist unpacking in Go and read through libzfs_sendrecv.c +func ParseResumeToken(ctx context.Context, token string) (*ResumeToken, error) { + + // Example resume tokens: + // + // From a non-incremental send + // 1-bf31b879a-b8-789c636064000310a500c4ec50360710e72765a5269740f80cd8e4d3d28a534b18e00024cf86249f5459925acc802a8facbf243fbd3433858161f5ddb9ab1ae7c7466a20c97382e5f312735319180af2f3730cf58166953824c2cc0200cde81651 + + // From an incremental send + // 1-c49b979a2-e0-789c636064000310a501c49c50360710a715e5e7a69766a63040c1eabb735735ce8f8d5400b2d991d4e52765a5269740f82080219f96569c5ac2000720793624f9a4ca92d46206547964fd25f91057f09e37babb88c9bf5503499e132c9f97989bcac050909f9f63a80f34abc421096616007c881d4c + + // Resulting output of zfs send -nvt + // + //resume token contents: + //nvlist version: 0 + // fromguid = 0x595d9f81aa9dddab + // object = 0x1 + // offset = 0x0 + // bytes = 0x0 + // toguid = 0x854f02a2dd32cf0d + // toname = pool1/test@b + //cannot resume send: 'pool1/test@b' used in the initial send no longer exists + + ctx, _ = context.WithTimeout(ctx, 500*time.Millisecond) + cmd := exec.CommandContext(ctx, ZFS_BINARY, "send", "-nvt", string(token)) + output, err := cmd.CombinedOutput() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + if !exitErr.Exited() { + return nil, err + } + // we abuse zfs send for decoding, the exit error may be due to + // a) the token being from a third machine + // b) it no longer exists on the machine where + } else { + return nil, err + } + } + + if !resumeTokenContentsRE.Match(output) { + if resumeTokenIsCorruptRE.Match(output) { + return nil, ResumeTokenCorruptError + } + return nil, ResumeTokenDecodingNotSupported + } + + matches := resumeTokenNVListRE.FindAllStringSubmatch(string(output), -1) + if matches == nil { + return nil, ResumeTokenDecodingNotSupported + } + + rt := &ResumeToken{} + + for _, m := range matches { + attr, val := m[1], m[2] + switch attr { + case "fromguid": + rt.FromGUID, err = strconv.ParseUint(val, 0, 64) + if err != nil { + return nil, ResumeTokenParsingError + } + rt.HasFromGUID = true + case "toguid": + rt.ToGUID, err = strconv.ParseUint(val, 0, 64) + if err != nil { + return nil, ResumeTokenParsingError + } + rt.HasToGUID = true + } + } + + if !rt.HasToGUID { + return nil, ResumeTokenDecodingNotSupported + } + + return rt, nil + +} diff --git a/zfs/resume_token_test.go b/zfs/resume_token_test.go new file mode 100644 index 0000000..33f560a --- /dev/null +++ b/zfs/resume_token_test.go @@ -0,0 +1,64 @@ +package zfs_test + +import ( + "context" + "github.com/stretchr/testify/assert" + "github.com/zrepl/zrepl/zfs" + "testing" +) + +type ResumeTokenTest struct { + Msg string + Token string + ExpectToken *zfs.ResumeToken + ExpectError error +} + +func (rtt *ResumeTokenTest) Test(t *testing.T) { + t.Log(rtt.Msg) + res, err := zfs.ParseResumeToken(context.TODO(), rtt.Token) + + if rtt.ExpectError != nil { + assert.EqualValues(t, rtt.ExpectError, err) + return + } + if rtt.ExpectToken != nil { + assert.Nil(t, err) + assert.EqualValues(t, rtt.ExpectToken, res) + return + } +} + +func TestParseResumeToken(t *testing.T) { + + tbl := []ResumeTokenTest{ + { + Msg: "normal send (non-incremental)", + Token: `1-bf31b879a-b8-789c636064000310a500c4ec50360710e72765a5269740f80cd8e4d3d28a534b18e00024cf86249f5459925acc802a8facbf243fbd3433858161f5ddb9ab1ae7c7466a20c97382e5f312735319180af2f3730cf58166953824c2cc0200cde81651`, + ExpectToken: &zfs.ResumeToken{ + HasToGUID: true, + ToGUID: 0x595d9f81aa9dddab, + }, + }, + { + Msg: "normal send (incremental)", + Token: `1-c49b979a2-e0-789c636064000310a501c49c50360710a715e5e7a69766a63040c1eabb735735ce8f8d5400b2d991d4e52765a5269740f82080219f96569c5ac2000720793624f9a4ca92d46206547964fd25f91057f09e37babb88c9bf5503499e132c9f97989bcac050909f9f63a80f34abc421096616007c881d4c`, + ExpectToken: &zfs.ResumeToken{ + HasToGUID: true, + ToGUID: 0x854f02a2dd32cf0d, + HasFromGUID: true, + FromGUID: 0x595d9f81aa9dddab, + }, + }, + { + Msg: "corrupted token", + Token: `1-bf31b879a-b8-789c636064000310a500c4ec50360710e72765a5269740f80cd8e4d3d28a534b18e00024cf86249f5459925acc802a8facbf243fbd3433858161f5ddb9ab1ae7c7466a20c97382e5f312735319180af2f3730cf58166953824c2cc0200cd12345`, + ExpectError: zfs.ResumeTokenCorruptError, + }, + } + + for _, test := range tbl { + test.Test(t) + } + +}