| // Package zfs provides wrappers around the ZFS command line tools. | |
| package zfs | |
| import ( | |
| "errors" | |
| "fmt" | |
| "io" | |
| "strconv" | |
| "strings" | |
| ) | |
| // ZFS dataset types, which can indicate if a dataset is a filesystem, | |
| // snapshot, or volume. | |
| const ( | |
| DatasetFilesystem = "filesystem" | |
| DatasetSnapshot = "snapshot" | |
| DatasetVolume = "volume" | |
| ) | |
| // Dataset is a ZFS dataset. A dataset could be a clone, filesystem, snapshot, | |
| // or volume. The Type struct member can be used to determine a dataset's type. | |
| // | |
| // The field definitions can be found in the ZFS manual: | |
| // http://www.freebsd.org/cgi/man.cgi?zfs(8). | |
| type Dataset struct { | |
| Name string | |
| Origin string | |
| Used uint64 | |
| Avail uint64 | |
| Mountpoint string | |
| Compression string | |
| Type string | |
| Written uint64 | |
| Volsize uint64 | |
| Logicalused uint64 | |
| Usedbydataset uint64 | |
| Quota uint64 | |
| Referenced uint64 | |
| } | |
| // InodeType is the type of inode as reported by Diff | |
| type InodeType int | |
| // Types of Inodes | |
| const ( | |
| _ = iota // 0 == unknown type | |
| BlockDevice InodeType = iota | |
| CharacterDevice | |
| Directory | |
| Door | |
| NamedPipe | |
| SymbolicLink | |
| EventPort | |
| Socket | |
| File | |
| ) | |
| // ChangeType is the type of inode change as reported by Diff | |
| type ChangeType int | |
| // Types of Changes | |
| const ( | |
| _ = iota // 0 == unknown type | |
| Removed ChangeType = iota | |
| Created | |
| Modified | |
| Renamed | |
| ) | |
| // DestroyFlag is the options flag passed to Destroy | |
| type DestroyFlag int | |
| // Valid destroy options | |
| const ( | |
| DestroyDefault DestroyFlag = 1 << iota | |
| DestroyRecursive = 1 << iota | |
| DestroyRecursiveClones = 1 << iota | |
| DestroyDeferDeletion = 1 << iota | |
| DestroyForceUmount = 1 << iota | |
| ) | |
| // InodeChange represents a change as reported by Diff | |
| type InodeChange struct { | |
| Change ChangeType | |
| Type InodeType | |
| Path string | |
| NewPath string | |
| ReferenceCountChange int | |
| } | |
| // Logger can be used to log commands/actions | |
| type Logger interface { | |
| Log(cmd []string) | |
| } | |
| type defaultLogger struct{} | |
| func (*defaultLogger) Log(cmd []string) { | |
| return | |
| } | |
| var logger Logger = &defaultLogger{} | |
| // SetLogger set a log handler to log all commands including arguments before | |
| // they are executed | |
| func SetLogger(l Logger) { | |
| if l != nil { | |
| logger = l | |
| } | |
| } | |
| // zfs is a helper function to wrap typical calls to zfs. | |
| func zfs(arg ...string) ([][]string, error) { | |
| c := command{Command: "zfs"} | |
| return c.Run(arg...) | |
| } | |
| // Datasets returns a slice of ZFS datasets, regardless of type. | |
| // A filter argument may be passed to select a dataset with the matching name, | |
| // or empty string ("") may be used to select all datasets. | |
| func Datasets(filter string) ([]*Dataset, error) { | |
| return listByType("all", filter) | |
| } | |
| // Snapshots returns a slice of ZFS snapshots. | |
| // A filter argument may be passed to select a snapshot with the matching name, | |
| // or empty string ("") may be used to select all snapshots. | |
| func Snapshots(filter string) ([]*Dataset, error) { | |
| return listByType(DatasetSnapshot, filter) | |
| } | |
| // Filesystems returns a slice of ZFS filesystems. | |
| // A filter argument may be passed to select a filesystem with the matching name, | |
| // or empty string ("") may be used to select all filesystems. | |
| func Filesystems(filter string) ([]*Dataset, error) { | |
| return listByType(DatasetFilesystem, filter) | |
| } | |
| // Volumes returns a slice of ZFS volumes. | |
| // A filter argument may be passed to select a volume with the matching name, | |
| // or empty string ("") may be used to select all volumes. | |
| func Volumes(filter string) ([]*Dataset, error) { | |
| return listByType(DatasetVolume, filter) | |
| } | |
| // GetDataset retrieves a single ZFS dataset by name. This dataset could be | |
| // any valid ZFS dataset type, such as a clone, filesystem, snapshot, or volume. | |
| func GetDataset(name string) (*Dataset, error) { | |
| out, err := zfs("list", "-Hp", "-o", dsPropListOptions, name) | |
| if err != nil { | |
| return nil, err | |
| } | |
| ds := &Dataset{Name: name} | |
| for _, line := range out { | |
| if err := ds.parseLine(line); err != nil { | |
| return nil, err | |
| } | |
| } | |
| return ds, nil | |
| } | |
| // Clone clones a ZFS snapshot and returns a clone dataset. | |
| // An error will be returned if the input dataset is not of snapshot type. | |
| func (d *Dataset) Clone(dest string, properties map[string]string) (*Dataset, error) { | |
| if d.Type != DatasetSnapshot { | |
| return nil, errors.New("can only clone snapshots") | |
| } | |
| args := make([]string, 2, 4) | |
| args[0] = "clone" | |
| args[1] = "-p" | |
| if properties != nil { | |
| args = append(args, propsSlice(properties)...) | |
| } | |
| args = append(args, []string{d.Name, dest}...) | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(dest) | |
| } | |
| // Unmount unmounts currently mounted ZFS file systems. | |
| func (d *Dataset) Unmount(force bool) (*Dataset, error) { | |
| if d.Type == DatasetSnapshot { | |
| return nil, errors.New("cannot unmount snapshots") | |
| } | |
| args := make([]string, 1, 3) | |
| args[0] = "umount" | |
| if force { | |
| args = append(args, "-f") | |
| } | |
| args = append(args, d.Name) | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(d.Name) | |
| } | |
| // Mount mounts ZFS file systems. | |
| func (d *Dataset) Mount(overlay bool, options []string) (*Dataset, error) { | |
| if d.Type == DatasetSnapshot { | |
| return nil, errors.New("cannot mount snapshots") | |
| } | |
| args := make([]string, 1, 5) | |
| args[0] = "mount" | |
| if overlay { | |
| args = append(args, "-O") | |
| } | |
| if options != nil { | |
| args = append(args, "-o") | |
| args = append(args, strings.Join(options, ",")) | |
| } | |
| args = append(args, d.Name) | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(d.Name) | |
| } | |
| // ReceiveSnapshot receives a ZFS stream from the input io.Reader, creates a | |
| // new snapshot with the specified name, and streams the input data into the | |
| // newly-created snapshot. | |
| func ReceiveSnapshot(input io.Reader, name string) (*Dataset, error) { | |
| c := command{Command: "zfs", Stdin: input} | |
| _, err := c.Run("receive", name) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(name) | |
| } | |
| // SendSnapshot sends a ZFS stream of a snapshot to the input io.Writer. | |
| // An error will be returned if the input dataset is not of snapshot type. | |
| func (d *Dataset) SendSnapshot(output io.Writer) error { | |
| if d.Type != DatasetSnapshot { | |
| return errors.New("can only send snapshots") | |
| } | |
| c := command{Command: "zfs", Stdout: output} | |
| _, err := c.Run("send", d.Name) | |
| return err | |
| } | |
| // CreateVolume creates a new ZFS volume with the specified name, size, and | |
| // properties. | |
| // A full list of available ZFS properties may be found here: | |
| // https://www.freebsd.org/cgi/man.cgi?zfs(8). | |
| func CreateVolume(name string, size uint64, properties map[string]string) (*Dataset, error) { | |
| args := make([]string, 4, 5) | |
| args[0] = "create" | |
| args[1] = "-p" | |
| args[2] = "-V" | |
| args[3] = strconv.FormatUint(size, 10) | |
| if properties != nil { | |
| args = append(args, propsSlice(properties)...) | |
| } | |
| args = append(args, name) | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(name) | |
| } | |
| // Destroy destroys a ZFS dataset. If the destroy bit flag is set, any | |
| // descendents of the dataset will be recursively destroyed, including snapshots. | |
| // If the deferred bit flag is set, the snapshot is marked for deferred | |
| // deletion. | |
| func (d *Dataset) Destroy(flags DestroyFlag) error { | |
| args := make([]string, 1, 3) | |
| args[0] = "destroy" | |
| if flags&DestroyRecursive != 0 { | |
| args = append(args, "-r") | |
| } | |
| if flags&DestroyRecursiveClones != 0 { | |
| args = append(args, "-R") | |
| } | |
| if flags&DestroyDeferDeletion != 0 { | |
| args = append(args, "-d") | |
| } | |
| if flags&DestroyForceUmount != 0 { | |
| args = append(args, "-f") | |
| } | |
| args = append(args, d.Name) | |
| _, err := zfs(args...) | |
| return err | |
| } | |
| // SetProperty sets a ZFS property on the receiving dataset. | |
| // A full list of available ZFS properties may be found here: | |
| // https://www.freebsd.org/cgi/man.cgi?zfs(8). | |
| func (d *Dataset) SetProperty(key, val string) error { | |
| prop := strings.Join([]string{key, val}, "=") | |
| _, err := zfs("set", prop, d.Name) | |
| return err | |
| } | |
| // GetProperty returns the current value of a ZFS property from the | |
| // receiving dataset. | |
| // A full list of available ZFS properties may be found here: | |
| // https://www.freebsd.org/cgi/man.cgi?zfs(8). | |
| func (d *Dataset) GetProperty(key string) (string, error) { | |
| out, err := zfs("get", "-H", key, d.Name) | |
| if err != nil { | |
| return "", err | |
| } | |
| return out[0][2], nil | |
| } | |
| // Rename renames a dataset. | |
| func (d *Dataset) Rename(name string, createParent bool, recursiveRenameSnapshots bool) (*Dataset, error) { | |
| args := make([]string, 3, 5) | |
| args[0] = "rename" | |
| args[1] = d.Name | |
| args[2] = name | |
| if createParent { | |
| args = append(args, "-p") | |
| } | |
| if recursiveRenameSnapshots { | |
| args = append(args, "-r") | |
| } | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return d, err | |
| } | |
| return GetDataset(name) | |
| } | |
| // Snapshots returns a slice of all ZFS snapshots of a given dataset. | |
| func (d *Dataset) Snapshots() ([]*Dataset, error) { | |
| return Snapshots(d.Name) | |
| } | |
| // CreateFilesystem creates a new ZFS filesystem with the specified name and | |
| // properties. | |
| // A full list of available ZFS properties may be found here: | |
| // https://www.freebsd.org/cgi/man.cgi?zfs(8). | |
| func CreateFilesystem(name string, properties map[string]string) (*Dataset, error) { | |
| args := make([]string, 1, 4) | |
| args[0] = "create" | |
| if properties != nil { | |
| args = append(args, propsSlice(properties)...) | |
| } | |
| args = append(args, name) | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(name) | |
| } | |
| // Snapshot creates a new ZFS snapshot of the receiving dataset, using the | |
| // specified name. Optionally, the snapshot can be taken recursively, creating | |
| // snapshots of all descendent filesystems in a single, atomic operation. | |
| func (d *Dataset) Snapshot(name string, recursive bool) (*Dataset, error) { | |
| args := make([]string, 1, 4) | |
| args[0] = "snapshot" | |
| if recursive { | |
| args = append(args, "-r") | |
| } | |
| snapName := fmt.Sprintf("%s@%s", d.Name, name) | |
| args = append(args, snapName) | |
| _, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return GetDataset(snapName) | |
| } | |
| // Rollback rolls back the receiving ZFS dataset to a previous snapshot. | |
| // Optionally, intermediate snapshots can be destroyed. A ZFS snapshot | |
| // rollback cannot be completed without this option, if more recent | |
| // snapshots exist. | |
| // An error will be returned if the input dataset is not of snapshot type. | |
| func (d *Dataset) Rollback(destroyMoreRecent bool) error { | |
| if d.Type != DatasetSnapshot { | |
| return errors.New("can only rollback snapshots") | |
| } | |
| args := make([]string, 1, 3) | |
| args[0] = "rollback" | |
| if destroyMoreRecent { | |
| args = append(args, "-r") | |
| } | |
| args = append(args, d.Name) | |
| _, err := zfs(args...) | |
| return err | |
| } | |
| // Children returns a slice of children of the receiving ZFS dataset. | |
| // A recursion depth may be specified, or a depth of 0 allows unlimited | |
| // recursion. | |
| func (d *Dataset) Children(depth uint64) ([]*Dataset, error) { | |
| args := []string{"list"} | |
| if depth > 0 { | |
| args = append(args, "-d") | |
| args = append(args, strconv.FormatUint(depth, 10)) | |
| } else { | |
| args = append(args, "-r") | |
| } | |
| args = append(args, "-t", "all", "-Hp", "-o", dsPropListOptions) | |
| args = append(args, d.Name) | |
| out, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var datasets []*Dataset | |
| name := "" | |
| var ds *Dataset | |
| for _, line := range out { | |
| if name != line[0] { | |
| name = line[0] | |
| ds = &Dataset{Name: name} | |
| datasets = append(datasets, ds) | |
| } | |
| if err := ds.parseLine(line); err != nil { | |
| return nil, err | |
| } | |
| } | |
| return datasets[1:], nil | |
| } | |
| // Diff returns changes between a snapshot and the given ZFS dataset. | |
| // The snapshot name must include the filesystem part as it is possible to | |
| // compare clones with their origin snapshots. | |
| func (d *Dataset) Diff(snapshot string) ([]*InodeChange, error) { | |
| args := []string{"diff", "-FH", snapshot, d.Name}[:] | |
| out, err := zfs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| inodeChanges, err := parseInodeChanges(out) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return inodeChanges, nil | |
| } |