package zfs import ( "bytes" "errors" "fmt" "sort" "strconv" "strings" ) type VersionType string const ( Bookmark VersionType = "bookmark" Snapshot = "snapshot" ) func (t VersionType) DelimiterChar() string { switch t { case Bookmark: return "#" case Snapshot: return "@" default: panic(fmt.Sprintf("unexpected VersionType %#v", t)) } return "" } type FilesystemVersion struct { Type VersionType // Display name. Should not be used for identification, only for user output Name string // GUID as exported by ZFS. Uniquely identifies a snapshot across pools Guid uint64 // The TXG in which the snapshot was created. For bookmarks, // this is the GUID of the snapshot it was initially tied to. CreateTXG uint64 } func (v FilesystemVersion) ToAbsPath(p DatasetPath) string { var b bytes.Buffer b.WriteString(p.ToString()) b.WriteString(v.Type.DelimiterChar()) b.WriteString(v.Name) return b.String() } type fsbyCreateTXG []FilesystemVersion func (l fsbyCreateTXG) Len() int { return len(l) } func (l fsbyCreateTXG) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l fsbyCreateTXG) Less(i, j int) bool { return l[i].CreateTXG < l[j].CreateTXG } /* The sender (left) wants to know if the receiver (right) has more recent versions Left : | C | Right: | A | B | C | D | E | => : | C | D | E | Left: | C | Right: | D | E | => : , no common ancestor Left : | C | D | E | Right: | A | B | C | => : , the left has newer versions Left : | A | B | C | | F | Right: | C | D | E | => : | C | | F | => diverged => IMPORTANT: since ZFS currently does not export dataset UUIDs, the best heuristic to identify a filesystem version is the tuple (name,creation) */ type FilesystemDiff struct { // The increments required to get left up to right's most recent version // 0th element is the common ancestor, ordered by birthtime, oldest first // If len() < 2, left and right are at same most recent version // If nil, there is no incremental path for left to get to right's most recent version // This means either (check Diverged field to determine which case we are in) // a) no common ancestor (left deleted all the snapshots it previously transferred to right) // => consult MRCAPathRight and request initial retransfer after prep on left side // b) divergence bewteen left and right (left made snapshots that right doesn't have) // => check MRCAPathLeft and MRCAPathRight and decide what to do based on that IncrementalPath []FilesystemVersion // true if left and right diverged, false otherwise Diverged bool // If Diverged, contains path from left most recent common ancestor (mrca) // to most recent version on left // Otherwise: nil MRCAPathLeft []FilesystemVersion // If Diverged, contains path from right most recent common ancestor (mrca) // to most recent version on right // If there is no common ancestor (i.e. not diverged), contains entire list of // versions on right MRCAPathRight []FilesystemVersion } func ZFSListFilesystemVersions(fs DatasetPath) (res []FilesystemVersion, err error) { var fieldLines [][]string fieldLines, err = ZFSList( []string{"name", "guid", "createtxg"}, "-r", "-d", "1", "-t", "bookmark,snapshot", "-s", "createtxg", fs.ToString()) if err != nil { return } res = make([]FilesystemVersion, len(fieldLines)) for i, line := range fieldLines { if len(line[0]) < 3 { err = errors.New(fmt.Sprintf("snapshot or bookmark name implausibly short: %s", line[0])) return } snapSplit := strings.SplitN(line[0], "@", 2) bookmarkSplit := strings.SplitN(line[0], "#", 2) if len(snapSplit)*len(bookmarkSplit) != 2 { err = errors.New(fmt.Sprintf("dataset cannot be snapshot and bookmark at the same time: %s", line[0])) return } var v FilesystemVersion if len(snapSplit) == 2 { v.Name = snapSplit[1] v.Type = Snapshot } else { v.Name = bookmarkSplit[1] v.Type = Bookmark } if v.Guid, err = strconv.ParseUint(line[1], 10, 64); err != nil { err = errors.New(fmt.Sprintf("cannot parse GUID: %s", err.Error())) return } if v.CreateTXG, err = strconv.ParseUint(line[2], 10, 64); err != nil { err = errors.New(fmt.Sprintf("cannot parse CreateTXG: %s", err.Error())) return } res[i] = v } return } // we must assume left and right are ordered ascendingly by ZFS_PROP_CREATETXG and that // names are unique (bas ZFS_PROP_GUID replacement) func MakeFilesystemDiff(left, right []FilesystemVersion) (diff FilesystemDiff) { if right == nil { panic("right must not be nil") } if left == nil { // treat like no common ancestor diff = FilesystemDiff{ IncrementalPath: nil, Diverged: false, MRCAPathRight: right, } } // Assert both left and right are sorted by createtxg { var leftSorted, rightSorted fsbyCreateTXG leftSorted = left rightSorted = right if !sort.IsSorted(leftSorted) { panic("cannot make filesystem diff: unsorted left") } if !sort.IsSorted(rightSorted) { panic("cannot make filesystem diff: unsorted right") } } // Find most recent common ancestor by name, preferring snapshots over bookmarks mrcaLeft := len(left) - 1 var mrcaRight int outer: for ; mrcaLeft >= 0; mrcaLeft-- { for i := len(right) - 1; i >= 0; i-- { if left[mrcaLeft].Guid == right[i].Guid { mrcaRight = i if i-1 >= 0 && right[i-1].Guid == right[i].Guid && right[i-1].Type == Snapshot { // prefer snapshots over bookmarks mrcaRight = i - 1 } break outer } } } // no common ancestor? if mrcaLeft == -1 { diff = FilesystemDiff{ IncrementalPath: nil, Diverged: false, MRCAPathRight: right, } return } // diverged? if mrcaLeft != len(left)-1 { diff = FilesystemDiff{ IncrementalPath: nil, Diverged: true, MRCAPathLeft: left[mrcaLeft:], MRCAPathRight: right[mrcaRight:], } return } if mrcaLeft != len(left)-1 { panic("invariant violated: mrca on left must be the last item in the left list") } // incPath must not contain bookmarks except initial one, // and only if that initial bookmark's snapshot is gone incPath := make([]FilesystemVersion, 0, len(right)) incPath = append(incPath, right[mrcaRight]) // right[mrcaRight] may be a bookmark if there's no equally named snapshot for i := mrcaRight + 1; i < len(right); i++ { if right[i].Type != Bookmark { incPath = append(incPath, right[i]) } } diff = FilesystemDiff{ IncrementalPath: incPath, } return } // A somewhat efficient way to determine if a filesystem exists on this host. // Particularly useful if exists is called more than once (will only fork exec once and cache the result) func ZFSListFilesystemExists() (exists func(p DatasetPath) bool, err error) { var actual [][]string if actual, err = ZFSList([]string{"name"}, "-t", "filesystem,volume"); err != nil { return } filesystems := make(map[string]bool, len(actual)) for _, e := range actual { filesystems[e[0]] = true } exists = func(p DatasetPath) bool { return filesystems[p.ToString()] } return }