not tracked not covered covered

    package config

    import (
            "os"

            "github.com/k0kubun/pp"
            "github.com/ubuntu/zsys/internal/log"
    )

    // TEXTDOMAIN is the message domain used by snappy; see dgettext(3)
    // for more information.
    const TEXTDOMAIN = "zsys"

    // ErrorFormat switch between "%v" and "%+v" depending if we want more verbose info
    var ErrorFormat = "%v"

    func init() {
            pp.SetDefaultOutput(os.Stderr)
    }

    // SetVerboseMode change ErrorFormat and logs between very, middly and non verbose
    func SetVerboseMode(level int) {
            if level > 2 {
                    level = 2
            }
            switch level {
            default:
                    ErrorFormat = "%v"
                    log.SetLevel(log.DefaultLevel)
            case 1:
                    ErrorFormat = "%+v"
                    log.SetLevel(log.InfoLevel)
            case 2:
                    ErrorFormat = "%+v"
                    log.SetLevel(log.DebugLevel)
            }
    }

    package i18n

    /*
     * This package is inspired from https://github.com/snapcore/snapd/blob/master/i18n, with other snap dependecies removed
     * and adapted to follow common go best practices.
     */

    //go:generate go run generate-locales.go update-po ../../po
    //go:generate go run generate-locales.go generate-mo ../../po ../../generated

    import (
            "fmt"
            "os"
            "path/filepath"
            "strings"

            "github.com/snapcore/go-gettext"
    )

    type i18n struct {
            domain    string
            localeDir string
            loc       string

            gettext.Catalog
            translations gettext.Translations
    }

    var (
            locale i18n

            // G is the shorthand for Gettext
            G = func(msgid string) string { return msgid }
            // NG is the shorthand for NGettext
            NG = func(msgid string, msgidPlural string, n uint32) string { return msgid }
    )

    // InitI18nDomain calls bind + set locale to system values
    func InitI18nDomain(domain string, options ...func(l *i18n)) {
            locale = i18n{
                    domain:    domain,
                    localeDir: "/usr/share/locale",
            }
            for _, option := range options {
                    option(&locale)
            }

            locale.bindTextDomain(locale.domain, locale.localeDir)
            locale.setLocale(locale.loc)

            G = locale.Gettext
            NG = locale.NGettext
    }

    // langpackResolver tries to fetch locale mo file path.
    // It first checks for the real locale (e.g. de_DE) and then
    // tries to simplify the locale (e.g. de_DE -> de)
    func langpackResolver(root string, locale string, domain string) string {
            for _, locale := range []string{locale, strings.SplitN(locale, "_", 2)[0]} {
                    r := filepath.Join(locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain))

                    // look into the generated mo files path first for translations, then the system
                    var candidateDirs []string
                    // Ubuntu uses /usr/share/locale-langpack and patches the glibc gettext implementation
                    candidateDirs = append(candidateDirs, filepath.Join(root, "..", "locale-langpack"))
                    candidateDirs = append(candidateDirs, root)

                    for _, dir := range candidateDirs {
                            candidateMo := filepath.Join(dir, r)
                            // Only load valid candidates, if we can't access it or have perm issues, ignore
                            if _, err := os.Stat(candidateMo); err != nil {
                                    continue
                            }
                            return candidateMo
                    }
            }

            return ""
    }

    func (l *i18n) bindTextDomain(domain, dir string) {
            l.translations = gettext.NewTranslations(dir, domain, langpackResolver)
    }

    // setLocale initializes the locale name and simplify it.
    // If empty, it defaults to system ones set in LC_MESSAGES and LANG.
    func (l *i18n) setLocale(loc string) {
            if loc == "" {
                    loc = os.Getenv("LC_MESSAGES")
                    if loc == "" {
                            loc = os.Getenv("LANG")
                    }
            }
            // de_DE.UTF-8, de_DE@euro all need to get simplified
            loc = strings.Split(loc, "@")[0]
            loc = strings.Split(loc, ".")[0]

            l.Catalog = l.translations.Locale(loc)
    }

    package log

    import (
            "context"
            "crypto/rand"
            "errors"
            "fmt"
            "io"

            "github.com/sirupsen/logrus"
            "github.com/ubuntu/zsys/internal/i18n"
    )

    const (
            // DefaultLevel only prints warning and errors.
            DefaultLevel = logrus.WarnLevel
            // InfoLevel is signaling system information like global calls.
            InfoLevel = logrus.InfoLevel
            // DebugLevel gives fine-grained details about executions.
            DebugLevel = logrus.DebugLevel
    )

    const (
            defaultRequestID = "unknown"
    )

    // ContextWithLogger returns a context which will log to the writer.
    // Level is based on metadata information from the ctx request.
    // A generated request ID is added to a requester ID and attached to the context
    func ContextWithLogger(ctx context.Context, requesterID, level string, w io.Writer) (newCtx context.Context, err error) {
            // Generate a request id.
            requestID := defaultRequestID
            b := make([]byte, 4)
            if _, err := rand.Read(b); err != nil {
                    logrus.Warningf(i18n.G("couldn't generate request id, setting to %q: %v"), defaultRequestID, err)
            } else {
                    requestID = fmt.Sprintf("%x", b[0:])
            }
            id := fmt.Sprintf("%s:%s", requesterID, requestID)

            // Get logging level.
            var logLevel logrus.Level
            if logLevel, err = logrus.ParseLevel(level); err != nil {
                    logrus.Warningf(i18n.G("invalid log level requested. Using default: %v"), err)
            }

            // Associate the context with a new logger, for which output is the io.Writer.
            logger := logrus.New()
            logger.SetOutput(w)
            // ignore the TTY check in logrus and force color mode and not systemd printing format.
            setLevelLogger(logger, logLevel, true)

            return context.WithValue(ctx, requestInfoKey, &requestInfo{
                    id:     id,
                    logger: logger,
            }), nil
    }

    // IDFromContext returns current request log id from context
    func IDFromContext(ctx context.Context) (string, error) {
            info, ok := ctx.Value(requestInfoKey).(*requestInfo)
            if !ok {
                    return "", errors.New(i18n.G("no request ID attached to this context"))
            }

            return info.id, nil
    }

    /*

    Package log proxy logs to logrus logger and an optional io.Writer.
    Both can have independent logging levels.

    */
    package log

    import (
            "context"
            "fmt"

            "github.com/sirupsen/logrus"
    )

    type requestInfoKeyType string

    const (
            requestInfoKey requestInfoKeyType = "logrequestinfo"
            reqIDFormat                       = "[[%s]] %s"
    )

    type requestInfo struct {
            id     string
            logger *logrus.Logger
    }

    // SetLevel sets default logger
    func SetLevel(l logrus.Level) {
            setLevelLogger(logrus.StandardLogger(), l, false)
    }

    // GetLevel gets default logger level
    func GetLevel() logrus.Level {
            return logrus.GetLevel()
    }

    // setLevelLogger sets given logger to level.
    // "simplified" enables to ignore the TTY check in logrus and force color mode and not systemd printing.
    func setLevelLogger(logger *logrus.Logger, l logrus.Level, simplified bool) {
            f := &logrus.TextFormatter{
                    DisableLevelTruncation: true,
                    DisableTimestamp:       true,
            }
            if simplified {
                    f.ForceColors = true
            }

            logger.SetLevel(l)
            logger.SetFormatter(f)
    }

    // Debug logs a message at level Debug on the standard logger
    // and may push to the stream referenced by ctx.
    func Debug(ctx context.Context, args ...interface{}) {
            Debugf(ctx, "%s", args...)
    }

    // Debugf logs a message at level Debug on the standard logger
    // and may push to the stream may push to the stream referenced by ctx.
    func Debugf(ctx context.Context, format string, args ...interface{}) {
            if info, ok := ctx.Value(requestInfoKey).(*requestInfo); ok {
                    info.logger.Debugf(format, args...)
                    // for standard logger, save the id
                    format = fmt.Sprintf(reqIDFormat, info.id, format)
            }
            logrus.Debugf(format, args...)
    }

    // Info logs a message at level Info on the standard logger
    // and may push to the stream referenced by ctx.
    func Info(ctx context.Context, args ...interface{}) {
            Infof(ctx, "%s", args...)
    }

    // Infof logs a message at level Info on the standard logger
    // and may push to the stream may push to the stream referenced by ctx.
    func Infof(ctx context.Context, format string, args ...interface{}) {
            if info, ok := ctx.Value(requestInfoKey).(*requestInfo); ok {
                    info.logger.Infof(format, args...)
                    // for standard logger, save the id
                    format = fmt.Sprintf(reqIDFormat, info.id, format)
            }
            logrus.Infof(format, args...)
    }

    // Warning logs a message at level Warning on the standard logger
    // and may push to the stream referenced by ctx.
    func Warning(ctx context.Context, args ...interface{}) {
            Warningf(ctx, "%s", args...)
    }

    // Warningf logs a message at level Warning on the standard logger
    // and may push to the stream may push to the stream referenced by ctx.
    func Warningf(ctx context.Context, format string, args ...interface{}) {
            if info, ok := ctx.Value(requestInfoKey).(*requestInfo); ok {
                    info.logger.Warningf(format, args...)
                    // for standard logger, save the id
                    format = fmt.Sprintf(reqIDFormat, info.id, format)
            }
            logrus.Warningf(format, args...)
    }

    // Error logs a message at level Error on the standard logger
    // and may push to the stream referenced by ctx.
    func Error(ctx context.Context, args ...interface{}) {
            Errorf(ctx, "%s", args...)
    }

    // Errorf logs a message at level Error on the standard logger
    // and may push to the stream may push to the stream referenced by ctx.
    func Errorf(ctx context.Context, format string, args ...interface{}) {
            if info, ok := ctx.Value(requestInfoKey).(*requestInfo); ok {
                    info.logger.Errorf(format, args...)
                    // for standard logger, save the id
                    format = fmt.Sprintf(reqIDFormat, info.id, format)
            }
            logrus.Errorf(format, args...)
    }

    package testutils

    import (
            "encoding/json"
            "flag"
            "fmt"
            "io/ioutil"
            "path/filepath"
            "strings"
            "sync"
            "testing"
    )

    var update *bool
    var updateFlagOnce = sync.Once{}

    // InstallUpdateFlag adds the update golden files flag
    func InstallUpdateFlag() {
            updateFlagOnce.Do(func() { update = flag.Bool("update", false, "update golden files") })
    }

    // LoadFromGoldenFile loads expected content to "want", after optionally refreshing it
    // from "got" if udpate flag is passed.
    func LoadFromGoldenFile(t *testing.T, got interface{}, want interface{}) {
            t.Helper()

            goldenFile := filepath.Join("testdata", testNameToPath(t)+".golden")
            if update != nil && *update {
                    b, err := json.MarshalIndent(got, "", "   ")
                    if err != nil {
                            t.Fatal("couldn't convert to json:", err)
                    }
                    if err := ioutil.WriteFile(goldenFile, b, 0644); err != nil {
                            t.Fatal("couldn't save golden file:", err)
                    }
                    if p, err := filepath.Abs(goldenFile); err == nil {
                            fmt.Println("Updated", p)
                    }
            }
            b, err := ioutil.ReadFile(goldenFile)
            if err != nil {
                    t.Fatal("couldn't read golden file")
            }

            if err := json.Unmarshal(b, &want); err != nil {
                    t.Fatal("couldn't convert golden file content to structure:", err)
            }
    }

    // testNameToPath transform the test path official name to a [subdirectory/]*test_base_name
    // for golden files generations
    func testNameToPath(t *testing.T) string {
            t.Helper()

            testDirname := strings.Split(t.Name(), "/")[0]
            nparts := strings.Split(t.Name(), "/")
            name := strings.ToLower(nparts[len(nparts)-1])

            var elems []string
            for _, e := range []string{testDirname, name} {
                    for _, k := range []string{"/", " ", ",", "=", "'"} {
                            e = strings.Replace(e, k, "_", -1)
                    }
                    elems = append(elems, strings.ToLower(strings.Replace(e, "__", "_", -1)))
            }

            return strings.Join(elems, "/")
    }

    package testutils

    import (
            "io/ioutil"
            "os"
            "testing"
    )

    // TempDir creates a temporary directory and returns the created directory and a teardown removal function to defer
    func TempDir(t *testing.T) (string, func()) {
            t.Helper()

            dir, err := ioutil.TempDir("", "zsystest-")
            if err != nil {
                    t.Fatal("can't create temporary directory", err)
            }
            return dir, func() {
                    if err = os.RemoveAll(dir); err != nil {
                            t.Error("can't clean temporary directory", err)
                    }
            }
    }

    package zfs

    import (
            "context"
            "fmt"
            "strconv"
            "strings"

            libzfs "github.com/bicomsystems/go-libzfs"
            "github.com/ubuntu/zsys/internal/config"
            "github.com/ubuntu/zsys/internal/i18n"
            "github.com/ubuntu/zsys/internal/log"
    )

    // RefreshProperties refreshes all the properties for a given dataset and the source of them.
    // for snapshots, we'll take the parent dataset for the mount properties.
    func (dataset *Dataset) RefreshProperties(ctx context.Context, d *libzfs.Dataset) error {
            sources := datasetSources{}
            isSnapshot := d.IsSnapshot()
            name := d.Properties[libzfs.DatasetPropName].Value

            var mounted bool
            var mountpoint, canMount string

            // On snapshots, take mount* properties from stored user property on dataset
            if isSnapshot {
                    var srcMountpoint, srcCanMount string
                    var err error

                    mountpoint, srcMountpoint, err = getUserPropertyFromSys(ctx, SnapshotMountpointProp, d)
                    if err != nil {
                            log.Debugf(ctx, i18n.G("%q isn't a zsys snapshot with a valid %q property: %v"), name, SnapshotMountpointProp, err)
                    }
                    sources.Mountpoint = srcMountpoint

                    canMount, srcCanMount, err = getUserPropertyFromSys(ctx, SnapshotCanmountProp, d)
                    if err != nil {
                            log.Debugf(ctx, i18n.G("%q isn't a zsys snapshot with a valid  %q property: %v"), name, SnapshotCanmountProp, err)
                    }
                    sources.CanMount = srcCanMount
            } else {
                    mp := d.Properties[libzfs.DatasetPropMountpoint]

                    p, err := d.Pool()
                    if err != nil {
                            return fmt.Errorf(i18n.G("can't get associated pool: ")+config.ErrorFormat, err)
                    }
                    poolRoot := p.Properties[libzfs.PoolPropAltroot].Value
                    mountpoint = strings.TrimPrefix(mp.Value, poolRoot)
                    if mountpoint == "" {
                            mountpoint = "/"
                    }
                    srcMountpoint := "local"
                    if mp.Source != "local" {
                            srcMountpoint = "inherited"
                    }
                    sources.Mountpoint = srcMountpoint

                    cm := d.Properties[libzfs.DatasetPropCanmount]
                    canMount = cm.Value
                    srcCanMount := "local"
                    if cm.Source != "local" {
                            srcCanMount = "inherited"
                    }
                    sources.CanMount = srcCanMount

                    mountedp := d.Properties[libzfs.DatasetPropMounted]
                    if mountedp.Value == "yes" {
                            mounted = true
                    }
            }

            origin := d.Properties[libzfs.DatasetPropOrigin].Value

            bfs, srcBootFS, err := getUserPropertyFromSys(ctx, BootfsProp, d)
            if err != nil {
                    return err
            }
            var bootFS bool
            if bfs == "yes" {
                    bootFS = true
            }
            sources.BootFS = srcBootFS

            var lu, srcLastUsed string
            if !isSnapshot {
                    lu, srcLastUsed, err = getUserPropertyFromSys(ctx, LastUsedProp, d)
                    if err != nil {
                            return err
                    }
            } else {
                    lu = d.Properties[libzfs.DatasetPropCreation].Value
            }
            if lu == "" {
                    lu = "0"
            }
            lastUsed, err := strconv.Atoi(lu)
            if err != nil {
                    return fmt.Errorf(i18n.G("%q property isn't an int: ")+config.ErrorFormat, LastUsedProp, err)
            }
            sources.LastUsed = srcLastUsed

            lastBootedKernel, srcLastBootedKernel, err := getUserPropertyFromSys(ctx, LastBootedKernelProp, d)
            if err != nil {
                    return err
            }
            sources.LastBootedKernel = srcLastBootedKernel

            bootfsDatasets, srcBootfsDatasets, err := getUserPropertyFromSys(ctx, BootfsDatasetsProp, d)
            if err != nil {
                    return err
            }
            sources.BootfsDatasets = srcBootfsDatasets

            dataset.DatasetProp = DatasetProp{
                    Mountpoint:       mountpoint,
                    CanMount:         canMount,
                    Mounted:          mounted,
                    BootFS:           bootFS,
                    LastUsed:         lastUsed,
                    LastBootedKernel: lastBootedKernel,
                    BootfsDatasets:   bootfsDatasets,
                    Origin:           origin,
                    sources:          sources,
            }
            return nil
    }

    // getUserPropertyFromSys returns the value of a user property and its source from the underlying
    // ZFS system dataset state.
    // It also sanitize the sources to only return "local" or "inherited".
    func getUserPropertyFromSys(ctx context.Context, prop string, d *libzfs.Dataset) (value, source string, err error) {
            name := d.Properties[libzfs.DatasetPropName].Value

            p, err := d.GetUserProperty(prop)
            if err != nil {
                    return "", "", fmt.Errorf(i18n.G("can't get %q property: ")+config.ErrorFormat, prop, err)
            }

            // User property doesn't exist for this dataset
            // On undefined user property sources, ZFS returns "-" but the API returns "none" check both for safety
            if p.Value == "-" && (p.Source == "-" || p.Source == "none") {
                    return "", "", nil
            }
            // The user property isn't set explicitely on the snapshot (inherited from non snapshot parent): ignore it.
            if d.IsSnapshot() && p.Source != "local" {
                    return "", "", nil
            }

            if d.IsSnapshot() {
                    log.Debugf(ctx, "property %q on snapshot %q: %q", prop, name, value)
                    idx := strings.LastIndex(p.Value, ":")
                    if idx < 0 {
                            log.Warningf(ctx, i18n.G("%q isn't a 'value:source' format type for %q"), prop, name)
                            return
                    }
                    value = p.Value[:idx]
                    source = p.Value[idx+1:]
            } else {
                    value = p.Value
                    source = p.Source
                    log.Debugf(ctx, "property %q on dataset %q: value: %q source: %q", prop, name, value, source)
            }

            if source != "local" {
                    source = "inherited"
            }

            return value, source, nil
    }

    // newDatasetTree returns a Dataset and a populated tree of all its children
    func newDatasetTree(ctx context.Context, d *libzfs.Dataset, allDatasets *map[string]*Dataset) (*Dataset, error) {
            // Skip non file system or snapshot datasets
            if d.Type == libzfs.DatasetTypeVolume || d.Type == libzfs.DatasetTypeBookmark {
                    return nil, nil
            }

            name := d.Properties[libzfs.DatasetPropName].Value
            log.Debugf(ctx, i18n.G("New dataNew dataset found: %q"), name)
            node := Dataset{
                    Name:       name,
                    IsSnapshot: d.IsSnapshot(),
            }
            if err := node.RefreshProperties(ctx, d); err != nil {
                    return nil, fmt.Errorf("couldn't refresh properties of %q: %v", node.Name, err)
            }

            var children []*Dataset
            for _, dc := range d.Children {
                    c, err := newDatasetTree(ctx, &dc, allDatasets)
                    if err != nil {
                            return nil, fmt.Errorf("couldn't scan dataset: %v", err)
                    }
                    if c == nil {
                            continue
                    }
                    children = append(children, c)
            }
            node.children = children

            // Populate direct access map
            (*allDatasets)[node.Name] = &node

            return &node, nil
    }

    // splitSnapshotName return base and trailing names
    func splitSnapshotName(name string) (string, string) {
            i := strings.LastIndex(name, "@")
            if i < 0 {
                    return name, ""
            }
            return name[:i], name[i+1:]
    }

    // checkSnapshotHierarchyIntegrity checks that the hierarchy follow the correct rules.
    // There are multiple cases:
    // - All children datasets with a snapshot with the same name exists -> OK, nothing in particular to deal with
    // - One dataset doesn't have a snapshot with the same name:
    //   * If no of its children of this dataset has a snapshot with the same name:
    //     * the dataset (and its children) has been created after the snapshot was taken -> OK
    //     * the dataset snapshot (and all its children snapshots) have been removed entirely: no way to detect the difference from above -> consider OK
    //   * If one of its children has a snapshot with the same name: clearly a case where something went wrong during snapshot -> error OUT
    // Said differently:
    // if a dataset has a snapshot with a given, all its parents should have a snapshot with the same name (up to base snapshotName)
    func checkSnapshotHierarchyIntegrity(d libzfs.Dataset, snapshotName string, snapshotExpected bool) error {
            found, _ := d.FindSnapshotName("@" + snapshotName)

            // No more snapshot was expected for children (parent dataset didn't have a snapshot, so all children shouldn't have them)
            if found && !snapshotExpected {
                    name := d.Properties[libzfs.DatasetPropName].Value
                    return fmt.Errorf(i18n.G("parent of %q doesn't have a snapshot named %q. Every of its children shouldn't have a snapshot. However %q exists"),
                            name, snapshotName, name+"@"+snapshotName)
            }

            for _, cd := range d.Children {
                    if err := checkSnapshotHierarchyIntegrity(cd, snapshotName, found); err != nil {
                            return err
                    }
            }
            return nil
    }

    // checkNoClone checks that the hierarchy has no clone.
    func checkNoClone(d *libzfs.Dataset) error {
            name := d.Properties[libzfs.DatasetPropName].Value

            clones, err := d.Clones()
            if err != nil {
                    return fmt.Errorf(i18n.G("couldn't scan %q for clones"), name)
            }
            if len(clones) > 0 {
                    return fmt.Errorf(i18n.G("%q has some clones when it shouldn't"), name)
            }

            for _, cd := range d.Children {
                    if err := checkNoClone(&cd); err != nil {
                            return err
                    }
            }
            return nil
    }

    // getProperty abstracts getting from a zfs or user property. It returns the property object.
    func getProperty(d libzfs.Dataset, name string) (libzfs.Property, error) {
            // TODO: or use getDatasetProp() and cache on Scan() to always have "none" checked.
            var prop libzfs.Property
            if !strings.Contains(name, ":") {
                    propName, err := stringToProp(name)
                    if err != nil {
                            return prop, err
                    }
                    return d.GetProperty(propName)
            }

            return d.GetUserProperty(name)
    }

    // setProperty abstracts setting  value to a zfs or user property from a zfs or user property.
    func setProperty(d libzfs.Dataset, name, value string) error {
            if !strings.Contains(name, ":") {
                    propName, err := stringToProp(name)
                    if err != nil {
                            return err
                    }
                    return d.SetProperty(propName, value)
            }
            return d.SetUserProperty(name, value)
    }

    // stringToProp converts a string to a validated zfs property (user properties aren't supported here).
    func stringToProp(name string) (libzfs.Prop, error) {
            var prop libzfs.Prop
            switch name {
            case CanmountProp:
                    prop = libzfs.DatasetPropCanmount
            case MountPointProp:
                    prop = libzfs.DatasetPropMountpoint
            default:
                    return prop, fmt.Errorf(i18n.G("unsupported property %q"), name)
            }
            return prop, nil
    }

    type datasetFuncRecursive func(d libzfs.Dataset) error

    // recurseFileSystemDatasets takes all children of d, and if it's not a snpashot, run f() over there while
    // returning an error if raised on any children.
    func recurseFileSystemDatasets(d libzfs.Dataset, f datasetFuncRecursive) error {
            for _, cd := range d.Children {
                    if cd.IsSnapshot() {
                            continue
                    }

                    if err := f(cd); err != nil {
                            return err
                    }
            }
            return nil
    }

    package zfs

    import (
            "context"
            "fmt"

            libzfs "github.com/bicomsystems/go-libzfs"
            "github.com/ubuntu/zsys/internal/config"
            "github.com/ubuntu/zsys/internal/i18n"
            "github.com/ubuntu/zsys/internal/log"
    )

    const (
            zsysPrefix = "com.ubuntu.zsys:"
            // BootfsProp string value
            BootfsProp = zsysPrefix + "bootfs"
            // LastUsedProp string value
            LastUsedProp = zsysPrefix + "last-used"
            // BootfsDatasetsProp string value
            BootfsDatasetsProp = zsysPrefix + "bootfs-datasets"
            // LastBootedKernelProp string value
            LastBootedKernelProp = zsysPrefix + "last-booted-kernel"
            // CanmountProp string value
            CanmountProp = "canmount"
            // SnapshotCanmountProp is the equivalent to CanmountProp, but as a user property to store on zsys snapshot
            SnapshotCanmountProp = zsysPrefix + CanmountProp
            // MountPointProp string value
            MountPointProp = "mountpoint"
            // SnapshotMountpointProp is the equivalent to MountPointProp, but as a user property to store on zsys snapshot
            SnapshotMountpointProp = zsysPrefix + MountPointProp
    )

    // Dataset is the abstraction of a physical dataset and exposes only properties that must are accessible by the user.
    type Dataset struct {
            // Name of the dataset.
            Name       string
            IsSnapshot bool `json:",omitempty"`
            DatasetProp

            children []*Dataset
            d        *libzfs.Dataset
    }

    // DatasetProp abstracts some properties for a given dataset
    type DatasetProp struct {
            // Mountpoint where the dataset will be mounted (without alt-root).
            Mountpoint string `json:",omitempty"`
            // CanMount state of the dataset.
            CanMount string `json:",omitempty"`
            // Mounted report if dataset is mounted
            Mounted bool `json:",omitempty"`
            // BootFS is a user property stating if the dataset should be mounted in the initramfs.
            BootFS bool `json:",omitempty"`
            // LastUsed is a user property that store the last time a dataset was used.
            LastUsed int `json:",omitempty"`
            // LastBootedKernel is a user property storing what latest kernel was a root dataset successfully boot with.
            LastBootedKernel string `json:",omitempty"`
            // BootfsDatasets is a user property for user datasets, linking them to relevant system bootfs datasets.
            BootfsDatasets string `json:",omitempty"`
            // Origin points to the dataset snapshot this one was clone from.
            Origin string `json:",omitempty"`

            // Here are the sources (not exposed to the public API) for each property
            // Used mostly for tests
            sources datasetSources
    }

    // datasetSources list sources some properties for a given dataset
    type datasetSources struct {
            Mountpoint       string `json:",omitempty"`
            CanMount         string `json:",omitempty"`
            BootFS           string `json:",omitempty"`
            LastUsed         string `json:",omitempty"`
            LastBootedKernel string `json:",omitempty"`
            BootfsDatasets   string `json:",omitempty"`
    }

    // Zfs is a system handler talking to zfs linux module.
    // It contains a local cache and dataset structures of underlying system.
    type Zfs struct {
            // root is a virtual dataset to which all top dataset of all pools are attached
            root        *Dataset
            allDatasets map[string]*Dataset
    }

    // New returns a new zfs system handler.
    func New(ctx context.Context, options ...func(*Zfs)) (*Zfs, error) {
            log.Debug(ctx, i18n.G("ZFS: new scan"))

            z := Zfs{
                    root:        &Dataset{Name: "/"},
                    allDatasets: make(map[string]*Dataset),
            }
            for _, options := range options {
                    options(&z)
            }

            // scan all datasets that are currently imported on the system
            ds, err := libzfs.DatasetOpenAll()
            if err != nil {
                    return nil, fmt.Errorf(i18n.G("can't list datasets: %v"), err)
            }
            defer libzfs.DatasetCloseAll(ds)

            var children []*Dataset
            for _, d := range ds {
                    c, err := newDatasetTree(ctx, &d, &z.allDatasets)
                    if err != nil {
                            return nil, fmt.Errorf("couldn't scan all datasets: %v", err)
                    }
                    if c == nil {
                            continue
                    }
                    children = append(children, c)
            }
            z.root.children = children

            return &z, nil
    }

    // Datasets returns all datasets on the system, where parent will always be before children.
    func (z Zfs) Datasets() []Dataset {
            ds := make(chan *Dataset)

            var collectChildren func(d *Dataset)
            collectChildren = func(d *Dataset) {
                    if d != z.root {
                            ds <- d
                    }
                    for _, n := range d.children {
                            collectChildren(n)
                    }
                    if d == z.root {
                            close(ds)
                    }
            }
            go collectChildren(z.root)

            r := make([]Dataset, 0, len(z.allDatasets))
            for d := range ds {
                    r = append(r, *d)
            }
            return r
    }

    // Transaction is a particular transaction on a Zfs state
    type Transaction struct {
            *Zfs
            ctx    context.Context
            cancel context.CancelFunc
            done   chan struct{}

            reverts []func() error

            // lastNestedTransaction will help ensuring that it's fully done before reverting this parent one.
            lastNestedTransaction *Transaction
    }

    // autoTransaction is a particular transaction on a Zfs state
    type nestedTransaction struct {
            *Transaction

            parent *Transaction
    }

    // NewTransaction create a new Zfs handler for a transaction, based on a Zfs object.
    // It returns a cancelFunc that is automatically purged on <Transaction>.Done().
    // If ctx is a cancellable or if CancelFunc is called before <Transaction>.Done(),
    // the transaction will revert any in progress zfs changes.
    func (z *Zfs) NewTransaction(ctx context.Context) (*Transaction, context.CancelFunc) {
            ctx, cancel := context.WithCancel(ctx)

            t := Transaction{
                    Zfs:    z,
                    ctx:    ctx,
                    cancel: cancel,
                    done:   make(chan struct{}),
            }

            go func() {
                    <-ctx.Done()

                    // check that any potential lastNestedTransaction has fully processed its reverted if it wasn't ended
                    if t.lastNestedTransaction != nil {
                            t.lastNestedTransaction.Done()
                    }

                    if len(t.reverts) > 0 {
                            log.Debugf(t.ctx, i18n.G("ZFS: reverting all in progress zfs transactions"))
                    }
                    for i := len(t.reverts) - 1; i >= 0; i-- {
                            if err := t.reverts[i](); err != nil {
                                    log.Warningf(t.ctx, i18n.G("An error occurred when reverting a Zfs transaction: ")+config.ErrorFormat, err)
                            }
                    }
                    t.reverts = nil
                    close(t.done)
            }()

            return &t, cancel
    }

    // Done signal that the transaction has ended and the object can't be reused.
    // This should be called to release underlying resources.
    func (t *Transaction) Done() {
            log.Debugf(t.ctx, i18n.G("ZFS: committing transaction"))

            // If cancel() was called before Done(), ensure we have proceeded the revert functions.
            select {
            case <-t.ctx.Done():
                    <-t.done
                    return
            default:
            }

            t.reverts = nil
            t.cancel() // Purge ctx goroutine
            <-t.done
    }

    // registerRevert is a helper for defer() setting error value
    func (t *Transaction) registerRevert(f func() error) {
            t.reverts = append(t.reverts, f)
    }

    // checkValid verifies if the transaction object is still valid and panics if not.
    func (t *Transaction) checkValid() {
            select {
            case <-t.done:
                    panic(i18n.G("The ZFS transaction object has already been used and Done() was called. It can't be reused"))
            default:
            }
    }

    // newNestedTransaction creates a sub transaction from an in progress transaction, reusing the parent transaction
    // context.
    // You should call Done(&err). If the given error it points at is not nil, it will cancel the nested transaction
    // automatically
    func (t *Transaction) newNestedTransaction() *nestedTransaction {
            nested, _ := t.Zfs.NewTransaction(t.ctx)
            t.lastNestedTransaction = nested
            return &nestedTransaction{
                    Transaction: nested,
                    parent:      t,
            }
    }

    // Done either commit a nested transaction or cancel it if an error occured
    func (t *nestedTransaction) Done(err *error) {
            defer t.Transaction.Done()
            if *err != nil {
                    // revert all in progress transactions
                    log.Debugf(t.ctx, i18n.G("ZFS: an error occured, cancelling nested transaction"))
                    t.cancel()
                    return
            }
            // append to parents current in progress transactions
            t.parent.reverts = append(t.parent.reverts, t.reverts...)
    }

    // Create creates a dataset for that path.
    func (t *Transaction) Create(path, mountpoint, canmount string) error {
            log.Debugf(t.ctx, i18n.G("ZFS: trying to Create %q with mountpoint %q"), path, mountpoint)

            props := make(map[libzfs.Prop]libzfs.Property)
            if mountpoint != "" {
                    props[libzfs.DatasetPropMountpoint] = libzfs.Property{Value: mountpoint}
            }
            props[libzfs.DatasetPropCanmount] = libzfs.Property{Value: canmount}

            d, err := libzfs.DatasetCreate(path, libzfs.DatasetTypeFilesystem, props)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't create %q: %v"), path, err)
            }
            defer d.Close()

            //TODO: update t.z.allDataset + tree Dataset
            //Maybe not close the created dataset?

            t.registerRevert(func() error {
                    d, err := libzfs.DatasetOpen(path)
                    if err != nil {
                            return fmt.Errorf(i18n.G("couldn't open %q for cleanup: %v"), path, err)
                    }
                    defer d.Close()
                    if err := d.Destroy(false); err != nil {
                            return fmt.Errorf(i18n.G("couldn't destroy %q for cleanup: %v"), path, err)
                    }
                    return nil
            })

            return nil
    }

    /*
    // Snapshot creates a new snapshot for dataset (and children if recursive is true) with the given name.
    func (t *Transaction) Snapshot(snapName, datasetName string, recursive bool) (errSnapshot error) {
            t.checkValid()

            log.Debugf(t.ctx, i18n.G("ZFS: trying to snapshot %q, recursive: %v"), datasetName, recursive)

            /*
                    API call:
                            t, cancel := {zfs}.Transaction(ctx)
                                    defer t.Done()

                                ...
                                    if err := t.Snapshot(); err != nil {
                                            cancel()
                                    }
                                    if err := t.Clone(); err != nil {
                                            cancel()
                                    }*/

    ////

    /*        d, err := libzfs.DatasetOpen(datasetName)
            if err != nil {
                    return fmt.Errorf(i18n.G("couldn't open %q: %v"), datasetName, err)
            }
            defer d.Close()

            nestedT := t.newNestedTransaction()
            defer nestedT.Done(&errSnapshot)

            // We can't use the recursive version of snapshotting, as we want to track user properties and
            // set them explicitly as needed
            return nestedT.snapshotRecursive(d, snapName, recursive)
    }

    // snapshotRecursive recursively try snapshotting all children and store "revert" operations by cleaning newly
    // created datasets.
    func (t *nestedTransaction) snapshotRecursive(d libzfs.Dataset, snapName string, recursive bool) error {
            datasetName := d.Properties[libzfs.DatasetPropName].Value

            // Get properties from parent snapshot.
            srcProps, err := getDatasetProp(d)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get dataset properties for %q: ")+config.ErrorFormat, datasetName, err)
            }

            props := make(map[libzfs.Prop]libzfs.Property)
            n := datasetName + "@" + snapName
            ds, err := libzfs.DatasetSnapshot(n, false, props)
            if err != nil {
                    return fmt.Errorf(i18n.G("couldn't snapshot %q: %v"), datasetName, err)
            }
            defer ds.Close()
            t.registerRevert(func() error {
                    d, err := libzfs.DatasetOpen(n)
                    if err != nil {
                            return fmt.Errorf(i18n.G("couldn't open %q for cleanup: %v"), n, err)
                    }
                    defer d.Close()
                    if err := d.Destroy(false); err != nil {
                            return fmt.Errorf(i18n.G("couldn't destroy %q for cleanup: %v"), n, err)
                    }
                    return nil
            })

            // Set user properties that we couldn't set before creating the snapshot dataset.
            // We don't set LastUsed here as Creation time will be used.
            if srcProps.sources.BootFS == "local" {
                    bootfsValue := "no"
                    if srcProps.BootFS {
                            bootfsValue = "yes"
                    }
                    if err := ds.SetUserProperty(BootfsProp, bootfsValue); err != nil {
                            return fmt.Errorf(i18n.G("couldn't set user property %q to %q: ")+config.ErrorFormat, BootfsProp, n, err)
                    }
            }
            if srcProps.sources.BootfsDatasets == "local" {
                    if err := ds.SetUserProperty(BootfsDatasetsProp, srcProps.BootfsDatasets); err != nil {
                            return fmt.Errorf(i18n.G("couldn't set user property %q to %q: ")+config.ErrorFormat, BootfsDatasetsProp, n, err)
                    }
            }
            if srcProps.sources.LastBootedKernel == "local" {
                    if err := ds.SetUserProperty(LastBootedKernelProp, srcProps.LastBootedKernel); err != nil {
                            return fmt.Errorf(i18n.G("couldn't set user property %q to %q: ")+config.ErrorFormat, LastBootedKernelProp, n, err)
                    }
            }

            if !recursive {
                    return nil
            }

            // Take snapshots on non snapshot dataset children
            return recurseFileSystemDatasets(d,
                    func(next libzfs.Dataset) error {
                            return t.snapshotRecursive(next, snapName, true)
                    })
    }

    // Clone creates a new dataset from a snapshot (and children if recursive is true) with a given suffix,
    // stripping older _<suffix> if any.
    func (z *Zfs) Clone(name, suffix string, skipBootfs, recursive bool) (errClone error) {
            log.Debugf(z.ctx, i18n.G("ZFS: trying to clone %q"), name)
            if suffix == "" {
                    return fmt.Errorf(i18n.G("no suffix was provided for cloning"))
            }

            d, err := libzfs.DatasetOpen(name)
            if err != nil {
                    return fmt.Errorf(i18n.G("%q doesn't exist: %v"), name, err)
            }
            defer d.Close()

            if !d.IsSnapshot() {
                    return fmt.Errorf(i18n.G("%q isn't a snapshot"), name)
            }

            subz, done := z.newTransaction()
            defer done(&errClone)

            rootName, snapshotName := splitSnapshotName(name)

            // Reformat the name with the new uuid and clone now the dataset.
            newRootName := rootName
            suffixIndex := strings.LastIndex(newRootName, "_")
            if suffixIndex != -1 {
                    newRootName = newRootName[:suffixIndex]
            }
            newRootName += "_" + suffix

            parent, err := libzfs.DatasetOpen(d.Properties[libzfs.DatasetPropName].Value[:strings.LastIndex(name, "@")])
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get parent dataset of %q: ")+config.ErrorFormat, name, err)
            }
            defer parent.Close()
            if recursive {
                    if err := checkSnapshotHierarchyIntegrity(parent, snapshotName, true); err != nil {
                            return fmt.Errorf(i18n.G("integrity check failed: %v"), err)
                    }
            }

            return subz.cloneRecursive(d, snapshotName, rootName, newRootName, skipBootfs, recursive)
    }

    // cloneRecursive recursively clones all children and store "revert" operations by cleaning newly
    // created datasets.
    func (z *Zfs) cloneRecursive(d libzfs.Dataset, snapshotName, rootName, newRootName string, skipBootfs, recursive bool) error {
            name := d.Properties[libzfs.DatasetPropName].Value
            parentName := name[:strings.LastIndex(name, "@")]

            /* FIXME: this is taken from getDatasetProp().
             * We will access directly the parent properties we are interested in here.
             *

                            parentName = name[:strings.LastIndex(name, "@")]
                            p, ok := datasetPropertiesCache[parentName]
                            if ok != true {
                                    return nil, fmt.Errorf(i18n.G("couldn't find %q in cache for getting properties of snapshot %q"), parentName, name)
                            }
                            mountpoint = p.Mountpoint
                            sources.Mountpoint = p.sources.Mountpoint
                            canMount = p.CanMount

    */
    /*
            // Get properties from snapshot and parents.
            srcProps, err := getDatasetProp(d)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get dataset properties for %q: ")+config.ErrorFormat, name, err)
            }

            datasetRelPath := strings.TrimPrefix(strings.TrimSuffix(name, "@"+snapshotName), rootName)
            if (!skipBootfs && srcProps.BootFS) || !srcProps.BootFS {
                    if err := z.cloneDataset(d, newRootName+datasetRelPath, *srcProps, parentName); err != nil {
                            return err
                    }
            }

            if !recursive {
                    return nil
            }

            // Handle other datasets (children of parent) which may have snapshots
            parent, err := libzfs.DatasetOpen(parentName)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get parent dataset of %q: ")+config.ErrorFormat, name, err)
            }
            defer parent.Close()

            return recurseFileSystemDatasets(parent,
                    func(next libzfs.Dataset) error {
                            // Look for childrens filesystem datasets having a corresponding snapshot
                            found, snapD := next.FindSnapshotName("@" + snapshotName)
                            if !found {
                                    return nil
                            }

                            return z.cloneRecursive(snapD, snapshotName, rootName, newRootName, skipBootfs, true)
                    })
    }

    func (z *Zfs) cloneDataset(d libzfs.Dataset, target string, srcProps DatasetProp, parentName string) error {
            props := make(map[libzfs.Prop]libzfs.Property)
            if srcProps.sources.Mountpoint == "local" {
                    props[libzfs.DatasetPropMountpoint] = libzfs.Property{
                            Value:  srcProps.Mountpoint,
                            Source: srcProps.sources.Mountpoint,
                    }
            }

            // CanMount is always local
            if srcProps.CanMount == "on" {
                    // don't mount new cloned dataset on top of parent.
                    srcProps.CanMount = "noauto"
            }
            props[libzfs.DatasetPropCanmount] = libzfs.Property{
                    Value:  srcProps.CanMount,
                    Source: "local",
            }

            cd, err := d.Clone(target, props)
            if err != nil {
                    name := d.Properties[libzfs.DatasetPropName].Value
                    return fmt.Errorf(i18n.G("couldn't clone %q to %q: ")+config.ErrorFormat, name, target, err)
            }
            defer cd.Close()
            z.registerRevert(func() error {
                    d, err := libzfs.DatasetOpen(target)
                    if err != nil {
                            return fmt.Errorf(i18n.G("couldn't open %q for cleanup: %v"), target, err)
                    }
                    defer d.Close()
                    if err := d.Destroy(false); err != nil {
                            return fmt.Errorf(i18n.G("couldn't destroy %q for cleanup: %v"), target, err)
                    }
                    return nil
            })

            // Set user properties that we couldn't set before creating the dataset. Based this for local
            // or source == parentName (as it will be local)
            if srcProps.sources.BootFS == "local" || srcProps.sources.BootFS == parentName {
                    bootfsValue := "no"
                    if srcProps.BootFS {
                            bootfsValue = "yes"
                    }
                    if err := cd.SetUserProperty(BootfsProp, bootfsValue); err != nil {
                            return fmt.Errorf(i18n.G("couldn't set user property %q to %q: ")+config.ErrorFormat, BootfsProp, target, err)
                    }
            }
            // We don't set BootfsDatasets as this property can't be translated to new datasets
            // We don't set LastUsed on purpose as the dataset isn't used yet

            return nil
    }

    // Promote recursively all children, including dataset named "name".
    // If the hierarchy is partially promoted, promote the missing one and be no-op for the rest.
    func (z *Zfs) Promote(name string) (errPromote error) {
            log.Debugf(z.ctx, i18n.G("ZFS: trying to promote %q"), name)
            d, err := libzfs.DatasetOpen(name)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get dataset %q: ")+config.ErrorFormat, name, err)
            }
            defer d.Close()

            if d.IsSnapshot() {
                    return fmt.Errorf(i18n.G("can't promote %q: it's a snapshot"), name)
            }

            subz, done := z.newTransaction()
            defer done(&errPromote)

            originParent, snapshotName := splitSnapshotName(d.Properties[libzfs.DatasetPropOrigin].Value)
            // Only check integrity for non promoted elements
            // Otherwise, promoting is a no-op or will repromote children
            if len(originParent) > 0 {
                    parent, err := libzfs.DatasetOpen(originParent)
                    if err != nil {
                            return fmt.Errorf(i18n.G("can't get parent dataset of %q: ")+config.ErrorFormat, name, err)
                    }
                    defer parent.Close()
                    if err := checkSnapshotHierarchyIntegrity(parent, snapshotName, true); err != nil {
                            return fmt.Errorf(i18n.G("integrity check failed: %v"), err)
                    }
            }

            return subz.promoteRecursive(d)
    }

    func (z *Zfs) promoteRecursive(d libzfs.Dataset) error {
            name := d.Properties[libzfs.DatasetPropName].Value

            origin, _ := splitSnapshotName(d.Properties[libzfs.DatasetPropOrigin].Value)
            // Only promote if not promoted yet.
            if len(origin) > 0 {
                    if err := d.Promote(); err != nil {
                            return fmt.Errorf(i18n.G("couldn't promote %q: ")+config.ErrorFormat, name, err)
                    }
                    z.registerRevert(func() error {
                            origD, err := libzfs.DatasetOpen(origin)
                            if err != nil {
                                    return fmt.Errorf(i18n.G("couldn't open %q for cleanup: %v"), origin, err)
                            }
                            defer origD.Close()
                            if err := origD.Promote(); err != nil {
                                    return fmt.Errorf(i18n.G("couldn't promote %q for cleanup: %v"), origin, err)
                            }
                            return nil
                    })
            }

            return recurseFileSystemDatasets(d,
                    func(next libzfs.Dataset) error {
                            return z.promoteRecursive(next)
                    })
    }

    // Destroy recursively all children, including dataset named "name".
    // If the dataset is a snapshot, navigate through the hierarchy to delete all dataset with the same snapshot name.
    // Note that destruction can't be rollbacked as filesystem content can't be recreated, so we don't accept them
    // in a transactional Zfs element.
    func (z *Zfs) Destroy(name string) error {
            log.Debugf(z.ctx, i18n.G("ZFS: trying to destroy %q"), name)
            if z.ctx.Done() != nil {
                    return fmt.Errorf(i18n.G("couldn't call Destroy in a transactional context"))
            }

            d, err := libzfs.DatasetOpen(name)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get dataset %q: ")+config.ErrorFormat, name, err)
            }
            defer d.Close()

            if err := checkNoClone(&d); err != nil {
                    return fmt.Errorf(i18n.G("couldn't destroy %q due to clones: %v"), name, err)
            }

            return d.DestroyRecursive()
    }

    // SetProperty to given dataset if it was a local/none/snapshot directly inheriting from parent value.
    // force does it even if the property was inherited.
    // For zfs properties, only a fix set is supported. Right now: "canmount"
    func (z *Zfs) SetProperty(name, value, datasetName string, force bool) error {
            log.Debugf(z.ctx, i18n.G("ZFS: trying to set %q=%q on %q"), name, value, datasetName)
            d, err := libzfs.DatasetOpen(datasetName)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get dataset %q: ")+config.ErrorFormat, datasetName, err)
            }
            defer d.Close()

            if d.IsSnapshot() {
                    return fmt.Errorf(i18n.G("can't set a property %q on %q: the dataset a snapshot"), name, datasetName)
            }

            prop, err := getProperty(d, name)
            if err != nil {
                    return fmt.Errorf(i18n.G("can't get dataset property %q for %q: ")+config.ErrorFormat, name, datasetName, err)
            }
            if !force && prop.Source != "local" && prop.Source != "default" && prop.Source != "none" && prop.Source != "" {
                    log.Debugf(z.ctx, i18n.G("ZFS: can't set property %q=%q for %q as not a local property (%q)"), name, value, datasetName, prop.Source)
                    return nil
            }
            if err = setProperty(d, name, value); err != nil {
                    return fmt.Errorf(i18n.G("can't set dataset property %q=%q for %q: ")+config.ErrorFormat, name, value, datasetName, err)
            }
            z.registerRevert(func() error { return z.SetProperty(name, prop.Value, datasetName, force) })

            return nil
    }
    */