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 } */