453 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			453 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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
 | |
| }
 | 
