422 lines
9.6 KiB
Go
422 lines
9.6 KiB
Go
package zfs
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"os/user"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/bitfield/script"
|
|
"github.com/go-errors/errors"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"gensokyo.cafe/xmnt/cfg"
|
|
"gensokyo.cafe/xmnt/mnt"
|
|
"gensokyo.cafe/xmnt/msg"
|
|
"gensokyo.cafe/xmnt/util"
|
|
)
|
|
|
|
const ZfsBin = "/usr/bin/zfs"
|
|
|
|
type Permissions struct {
|
|
Mount bool
|
|
LoadKey bool
|
|
}
|
|
|
|
var allPermission = Permissions{Mount: true, LoadKey: true}
|
|
|
|
type KeyStatus string
|
|
|
|
const (
|
|
KeyStatusAvailable KeyStatus = "available"
|
|
KeyStatusUnavailable KeyStatus = "unavailable"
|
|
)
|
|
|
|
type CanMount string
|
|
|
|
const (
|
|
CanMountOn CanMount = "on"
|
|
CanMountOff CanMount = "off"
|
|
CanMountNoAuto CanMount = "noauto"
|
|
)
|
|
|
|
type Dataset struct {
|
|
Name string
|
|
GUID string
|
|
CanMount CanMount
|
|
MountPoint string
|
|
KeyStatus KeyStatus
|
|
EncryptionRoot string
|
|
Mounted bool
|
|
|
|
permissions *Permissions
|
|
}
|
|
|
|
func (d *Dataset) loadPermissions() error {
|
|
if d.permissions != nil {
|
|
return nil
|
|
}
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "cannot obtain current user", 0)
|
|
}
|
|
if currentUser.Uid == "0" {
|
|
d.permissions = &allPermission
|
|
return nil
|
|
}
|
|
|
|
permissions := &Permissions{}
|
|
|
|
output, err := util.RunCommand(ZfsBin, nil, "allow", d.Name)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("cannot obtain permission list for zfs dataset %s", d.Name), 0)
|
|
}
|
|
pattern := regexp.MustCompile(`^\s*user ` + currentUser.Username + ` ([\w-]+(,[\w-]+)*)$`)
|
|
script.Echo(string(output)).FilterLine(func(line string) string {
|
|
match := pattern.FindStringSubmatch(line)
|
|
if match != nil {
|
|
for _, perm := range strings.Split(match[1], ",") {
|
|
switch perm {
|
|
case "mount":
|
|
permissions.Mount = true
|
|
case "load-key":
|
|
permissions.LoadKey = true
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}).Wait()
|
|
d.permissions = permissions
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Dataset) Permissions() (*Permissions, error) {
|
|
if err := d.loadPermissions(); err != nil {
|
|
return nil, err
|
|
}
|
|
perm := *d.permissions
|
|
return &perm, nil
|
|
}
|
|
|
|
func listCmd(name string, recursive bool) ([]byte, error) {
|
|
zfsArgs := []string{"get", "-Ht", "filesystem", "guid,canmount,mountpoint,encryptionroot,keystatus,mounted"}
|
|
if recursive {
|
|
zfsArgs = append(zfsArgs, "-r")
|
|
}
|
|
if name != "" {
|
|
zfsArgs = append(zfsArgs, name)
|
|
}
|
|
|
|
return util.RunCommand(ZfsBin, nil, zfsArgs...)
|
|
}
|
|
|
|
func ParseListOutput(output []byte) ([]*Dataset, error) {
|
|
idx := map[string]*Dataset{}
|
|
|
|
sc := bufio.NewScanner(bytes.NewReader(output))
|
|
for sc.Scan() {
|
|
fields := strings.Split(sc.Text(), "\t")
|
|
if len(fields) != 4 {
|
|
return nil, errors.Errorf("invalid zfs list output: %q", sc.Text())
|
|
}
|
|
|
|
name := fields[0]
|
|
key := fields[1]
|
|
value := fields[2]
|
|
|
|
dataset, ok := idx[name]
|
|
if !ok {
|
|
dataset = &Dataset{Name: name}
|
|
idx[name] = dataset
|
|
}
|
|
switch key {
|
|
case "guid":
|
|
dataset.GUID = value
|
|
case "canmount":
|
|
dataset.CanMount = CanMount(value)
|
|
case "mountpoint":
|
|
dataset.MountPoint = value
|
|
case "encryptionroot":
|
|
dataset.EncryptionRoot = value
|
|
case "keystatus":
|
|
dataset.KeyStatus = KeyStatus(value)
|
|
case "mounted":
|
|
dataset.Mounted = value == "yes"
|
|
}
|
|
}
|
|
|
|
return maps.Values(idx), nil
|
|
}
|
|
|
|
func List(name string, recursive bool) ([]*Dataset, error) {
|
|
listOutput, err := listCmd(name, recursive)
|
|
if err != nil {
|
|
return nil, errors.WrapPrefix(err, "cannot obtain list of zfs datasets", 0)
|
|
}
|
|
|
|
datasets, err := ParseListOutput(listOutput)
|
|
if err != nil {
|
|
return nil, errors.WrapPrefix(err, "cannot parse zfs list output", 0)
|
|
}
|
|
return datasets, nil
|
|
}
|
|
|
|
type Mounter struct {
|
|
preset *mnt.Preset
|
|
dataset *Dataset
|
|
}
|
|
|
|
func NewMounterFromPreset(p *mnt.Preset) (mnt.Mounter, error) {
|
|
preset := *p
|
|
if preset.Path == "" {
|
|
return nil, errors.New("preset path is empty")
|
|
}
|
|
m := &Mounter{
|
|
preset: &preset,
|
|
}
|
|
|
|
// find the dataset
|
|
if err := m.refresh(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (m *Mounter) refresh() error {
|
|
datasets, err := List(m.preset.Path, false)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "cannot obtain list of zfs datasets", 0)
|
|
}
|
|
var newDataset *Dataset
|
|
for _, d := range datasets {
|
|
if d.Name == m.preset.Path {
|
|
newDataset = d
|
|
}
|
|
}
|
|
if newDataset == nil {
|
|
return errors.Errorf("cannot find zfs dataset %q", m.preset.Path)
|
|
}
|
|
m.dataset = newDataset
|
|
return nil
|
|
}
|
|
|
|
func (m *Mounter) loadKey() error {
|
|
if m.dataset.KeyStatus != KeyStatusUnavailable {
|
|
return nil
|
|
}
|
|
if m.dataset.Name != m.dataset.EncryptionRoot {
|
|
return errors.Errorf("cannot load key for zfs dataset %q: not an encryption root", m.dataset.Name)
|
|
}
|
|
|
|
key, err := util.ReadCredentialFile(m.dataset.GUID, cfg.Cfg.CredentialStore)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "cannot load zfs key", 0)
|
|
}
|
|
|
|
perm, err := m.dataset.Permissions()
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "failed to load zfs key", 0)
|
|
}
|
|
run := util.RunPrivilegedCommand
|
|
if perm.LoadKey {
|
|
run = util.RunCommand
|
|
}
|
|
|
|
msg.Infof("zfs load-key %q", m.dataset.Name)
|
|
_, err = run(ZfsBin, strings.NewReader(key), "load-key", m.dataset.Name)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "failed to load zfs key", 0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mounter) mount() error {
|
|
if m.dataset.Mounted {
|
|
return nil
|
|
}
|
|
if !slices.Contains([]CanMount{CanMountNoAuto, CanMountOn}, m.dataset.CanMount) {
|
|
return errors.Errorf("cannot mount zfs dataset %q: canmount is %q", m.dataset.Name, m.dataset.CanMount)
|
|
}
|
|
if m.dataset.KeyStatus != KeyStatusAvailable {
|
|
return errors.Errorf("cannot mount zfs dataset %q: not unlocked", m.dataset.Name)
|
|
}
|
|
|
|
mountPoint := m.preset.MountPoint
|
|
if mountPoint == "" {
|
|
mountPoint = m.dataset.MountPoint
|
|
}
|
|
if !util.IsValidMountPoint(mountPoint) {
|
|
return errors.Errorf(
|
|
"cannot mount zfs dataset %q: invalid mount point %q",
|
|
m.dataset.Name, mountPoint,
|
|
)
|
|
}
|
|
|
|
// If systemd mount unit exists, use it.
|
|
// In this case, if we do not use systemd for mounting, systemd will mess with the mounting process, and the zfs
|
|
// dataset will get unmounted immediately after we mounted it. See https://github.com/openzfs/zfs/issues/11248
|
|
if err := util.SystemdMount(mountPoint); err == nil {
|
|
return nil
|
|
} else if !util.ShouldSkipSdMount(err) {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("failed to mount zfs dataset %q", m.dataset.Name), 0)
|
|
}
|
|
|
|
// mount using zfs command
|
|
mountArgs := []string{"mount"}
|
|
if m.preset.MountPoint != "" {
|
|
// user specified the mount point
|
|
mountArgs = append(mountArgs, "-o", "mountpoint="+mountPoint)
|
|
}
|
|
mountArgs = append(mountArgs, m.dataset.Name)
|
|
|
|
perm, err := m.dataset.Permissions()
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("failed to mount zfs dataset %q", m.dataset.Name), 0)
|
|
}
|
|
run := util.RunPrivilegedCommand
|
|
if perm.Mount {
|
|
run = util.RunCommand
|
|
}
|
|
|
|
_, err = run(ZfsBin, nil, mountArgs...)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("failed to mount zfs dataset %q", m.dataset.Name), 0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mounter) Mount() error {
|
|
if err := m.loadKey(); err != nil {
|
|
return err
|
|
}
|
|
if err := m.refresh(); err != nil {
|
|
return err
|
|
}
|
|
if err := m.mount(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// check
|
|
if err := m.refresh(); err != nil {
|
|
return err
|
|
}
|
|
if !m.dataset.Mounted {
|
|
return errors.Errorf("zfs dataset %q is not mounted", m.dataset.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Mounter) unmount() error {
|
|
if !m.dataset.Mounted {
|
|
return nil
|
|
}
|
|
|
|
// try to unmount with systemd
|
|
mp := m.dataset.MountPoint
|
|
if util.IsValidMountPoint(mp) {
|
|
if err := util.SystemdUnmount(mp); err == nil {
|
|
return nil
|
|
} else if !util.ShouldSkipSdMount(err) {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("failed to unmount zfs dataset %q", m.dataset.Name), 0)
|
|
}
|
|
}
|
|
|
|
// try to unmount with zfs command.
|
|
perm, err := m.dataset.Permissions()
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("failed to unmount zfs dataset %q", m.dataset.Name), 0)
|
|
}
|
|
run := util.RunPrivilegedCommand
|
|
if perm.Mount {
|
|
run = util.RunCommand
|
|
}
|
|
|
|
_, err = run(ZfsBin, nil, "unmount", "-u", m.dataset.Name)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, fmt.Sprintf("failed to unmount zfs dataset %q", m.dataset.Name), 0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mounter) unloadKey() error {
|
|
if m.dataset.KeyStatus != KeyStatusAvailable || m.dataset.Name != m.dataset.EncryptionRoot {
|
|
return nil
|
|
}
|
|
|
|
perm, err := m.dataset.Permissions()
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "failed to unload zfs key", 0)
|
|
}
|
|
run := util.RunPrivilegedCommand
|
|
if perm.LoadKey {
|
|
run = util.RunCommand
|
|
}
|
|
|
|
msg.Infof("zfs unload-key %q", m.dataset.Name)
|
|
_, err = run(ZfsBin, nil, "unload-key", m.dataset.Name)
|
|
if err != nil {
|
|
return errors.WrapPrefix(err, "failed to unload zfs key", 0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mounter) Unmount() error {
|
|
if err := m.unmount(); err != nil {
|
|
return errors.Wrap(err, 0)
|
|
}
|
|
|
|
// check
|
|
if err := m.refresh(); err != nil {
|
|
return errors.WrapPrefix(err, "failed to check for result of unmounting", 0)
|
|
}
|
|
if m.dataset.Mounted {
|
|
return errors.Errorf("zfs dataset %q is still mounted", m.dataset.Name)
|
|
}
|
|
|
|
if err := m.unloadKey(); err != nil {
|
|
return errors.Wrap(err, 0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func match(s string) ([]*mnt.Preset, error) {
|
|
datasets, err := List("", true)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, 0)
|
|
}
|
|
|
|
var partialMatch []*Dataset
|
|
for _, d := range datasets {
|
|
if d.Name == s {
|
|
return []*mnt.Preset{{
|
|
Name: s,
|
|
Type: "zfs",
|
|
Path: d.Name,
|
|
}}, nil
|
|
}
|
|
if strings.HasSuffix(d.Name, "/"+s) {
|
|
partialMatch = append(partialMatch, d)
|
|
}
|
|
}
|
|
var ret []*mnt.Preset
|
|
for _, d := range partialMatch {
|
|
ret = append(ret, &mnt.Preset{
|
|
Name: s,
|
|
Type: "zfs",
|
|
Path: d.Name,
|
|
})
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func init() {
|
|
mnt.RegisterMounter("zfs", NewMounterFromPreset)
|
|
mnt.RegisterMatcher(match)
|
|
}
|