212 lines
8.2 KiB
Go
212 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fake is a machine for a test: a tree standing in for /etc, /proc and /var/lib, and a runner
|
|
// answering from a table.
|
|
type fake struct {
|
|
t *testing.T
|
|
root string
|
|
answers map[string]string
|
|
zfs bool
|
|
}
|
|
|
|
func newFake(t *testing.T) *fake {
|
|
t.Helper()
|
|
return &fake{t: t, root: t.TempDir(), answers: map[string]string{}}
|
|
}
|
|
|
|
func (f *fake) file(path, text string) {
|
|
f.t.Helper()
|
|
p := filepath.Join(f.root, path)
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (f *fake) machine() *Machine {
|
|
return &Machine{
|
|
Root: f.root,
|
|
Run: func(_ context.Context, name string, args ...string) (string, error) {
|
|
line := strings.TrimSpace(name + " " + strings.Join(args, " "))
|
|
if out, ok := f.answers[line]; ok {
|
|
return out, nil
|
|
}
|
|
return "", fmt.Errorf("%s: not found", line)
|
|
},
|
|
LookPath: func(n string) bool { return n == "zfs" && f.zfs },
|
|
}
|
|
}
|
|
|
|
func TestRedactOptions(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{"", ""},
|
|
{"defaults,_netdev", "defaults,_netdev"},
|
|
{"username=root,password=hunter2,iocharset=utf8", "username=root,password=(redacted),iocharset=utf8"},
|
|
{"user=a,pass=x,PASSWD=y", "user=a,pass=(redacted),PASSWD=(redacted)"},
|
|
{"credentials=/etc/samba/creds,sec=ntlmssp", "credentials=/etc/samba/creds,sec=ntlmssp"},
|
|
{"password=", "password=(redacted)"},
|
|
{"password2=z,passwordless", "password2=(redacted),passwordless"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := RedactOptions(c.in); got != c.want {
|
|
t.Errorf("RedactOptions(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
const sampleFstab = `# /etc/fstab
|
|
UUID=aaaa / ext4 rw,relatime 0 1
|
|
UUID=bbbb none swap defaults 0 0
|
|
192.0.2.10:/storage/media /storage/media nfs defaults,_netdev 0 0
|
|
//192.0.2.20/Share /mnt/my\040share cifs username=root,password=s3cret,iocharset=utf8 0 0
|
|
192.0.2.10:/backup /mnt/backup nfs4 nofail,x-systemd.automount 0 0
|
|
`
|
|
|
|
func TestParseFstab(t *testing.T) {
|
|
got := ParseFstab(sampleFstab)
|
|
if len(got) != 5 {
|
|
t.Fatalf("got %d entries, want 5: %+v", len(got), got)
|
|
}
|
|
cases := []struct {
|
|
i int
|
|
point string
|
|
network bool
|
|
line int
|
|
}{
|
|
{0, "/", false, 2}, {2, "/storage/media", true, 4}, {3, "/mnt/my share", true, 5}, {4, "/mnt/backup", true, 6},
|
|
}
|
|
for _, c := range cases {
|
|
e := got[c.i]
|
|
if e.Mountpoint != c.point || e.Network != c.network || e.Line != c.line {
|
|
t.Errorf("entry %d = %+v, want point %q network %v line %d", c.i, e, c.point, c.network, c.line)
|
|
}
|
|
}
|
|
if strings.Contains(got[3].Options, "s3cret") {
|
|
t.Errorf("a password was answered: %q", got[3].Options)
|
|
}
|
|
}
|
|
|
|
func TestIsNetwork(t *testing.T) {
|
|
cases := []struct {
|
|
typ, src string
|
|
want bool
|
|
}{
|
|
{"nfs", "h:/x", true}, {"cifs", "//h/s", true}, {"fuse.sshfs", "u@h:/x", true}, {"ext4", "/dev/sda1", false},
|
|
{"fuseblk", "/dev/sda2", false}, {"fuse.rclone", "remote:", true}, {"fuse.portal", "portal", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsNetwork(c.typ, c.src); got != c.want {
|
|
t.Errorf("IsNetwork(%q, %q) = %v, want %v", c.typ, c.src, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
const sampleMountinfo = `22 1 259:3 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p3 rw
|
|
40 22 0:50 / /mnt/backup rw,relatime shared:30 - autofs systemd-1 rw,fd=50
|
|
61 40 0:60 / /mnt/backup rw,relatime shared:31 - nfs4 192.0.2.10:/backup rw,vers=4.2
|
|
70 22 0:70 / /mnt/my\040share rw - cifs //192.0.2.20/Share rw,username=root,password=leaked
|
|
`
|
|
|
|
func TestParseMountinfo(t *testing.T) {
|
|
got := ParseMountinfo(sampleMountinfo)
|
|
if got["/mnt/backup"].Type != "nfs4" {
|
|
t.Errorf("the last mount at a point must win: %+v", got["/mnt/backup"])
|
|
}
|
|
share, ok := got["/mnt/my share"]
|
|
if !ok {
|
|
t.Fatalf("an escaped mount point was not decoded: %+v", got)
|
|
}
|
|
if strings.Contains(share.Options, "leaked") {
|
|
t.Errorf("a password in mountinfo was answered: %q", share.Options)
|
|
}
|
|
}
|
|
|
|
const sampleListUnits = `-.mount loaded active mounted Root Mount
|
|
mnt-backup.automount loaded active waiting mnt-backup.automount
|
|
mnt-backup.mount loaded active mounted /mnt/backup
|
|
mnt-my\x20share.mount loaded failed failed /mnt/my share
|
|
storage-media.mount loaded failed failed /storage/media
|
|
dev-mqueue.mount loaded active mounted POSIX Message Queue File System
|
|
`
|
|
|
|
var sampleShow = strings.Join([]string{
|
|
"Id=-.mount\nWhere=/\nWhat=/dev/nvme0n1p3\nType=ext4\nOptions=rw\nResult=success\nActiveState=active\nSubState=mounted\nFragmentPath=/run/systemd/generator/-.mount\nSourcePath=/etc/fstab\nTimeoutUSec=1min 30s\nUnitFileState=generated",
|
|
"Id=mnt-backup.automount\nWhere=/mnt/backup\nResult=success\nActiveState=active\nSubState=waiting\nFragmentPath=/run/systemd/generator/mnt-backup.automount\nSourcePath=/etc/fstab\nUnitFileState=generated",
|
|
"Id=mnt-backup.mount\nWhere=/mnt/backup\nWhat=192.0.2.10:/backup\nType=nfs4\nOptions=rw,vers=4.2\nResult=success\nActiveState=active\nSubState=mounted\nFragmentPath=/run/systemd/generator/mnt-backup.mount\nSourcePath=/etc/fstab\nTimeoutUSec=1min 30s\nUnitFileState=generated",
|
|
"Id=mnt-my\\x20share.mount\nWhere=/mnt/my share\nWhat=//192.0.2.20/Share\nType=cifs\nOptions=username=root,password=s3cret\nResult=exit-code\nActiveState=failed\nSubState=failed\nFragmentPath=/run/systemd/generator/mnt-my\\x20share.mount\nSourcePath=/etc/fstab\nUnitFileState=generated",
|
|
"Id=storage-media.mount\nWhere=/storage/media\nWhat=192.0.2.10:/storage/media\nType=nfs\nOptions=defaults,_netdev\nResult=timeout\nActiveState=failed\nSubState=failed\nFragmentPath=/run/systemd/generator/storage-media.mount\nSourcePath=/etc/fstab\nTimeoutUSec=1min 30s\nUnitFileState=generated",
|
|
"Id=dev-mqueue.mount\nWhere=/dev/mqueue\nWhat=mqueue\nType=mqueue\nActiveState=active\nSubState=mounted\nFragmentPath=/usr/lib/systemd/system/dev-mqueue.mount\nUnitFileState=static",
|
|
}, "\n\n") + "\n"
|
|
|
|
func listFake(t *testing.T) *fake {
|
|
f := newFake(t)
|
|
f.file("/etc/fstab", sampleFstab)
|
|
f.file("/proc/self/mountinfo", sampleMountinfo)
|
|
f.answers["systemctl list-units --type=mount,automount --all --no-legend --plain --no-pager"] = sampleListUnits
|
|
names := strings.Join(ParseListUnits(sampleListUnits), " ")
|
|
f.answers["systemctl show -p "+ShowProperties+" --no-pager "+names] = sampleShow
|
|
return f
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
r := listFake(t).machine().List(false, "")
|
|
said := strings.Join(r.Said, "\n")
|
|
for _, want := range []string{
|
|
"/storage/media (nfs from 192.0.2.10:/storage/media): failed, timeout; from /etc/fstab line 4, no automount, no nofail",
|
|
"/mnt/my share (cifs from //192.0.2.20/Share): failed, exit-code; from /etc/fstab line 5, no automount, no nofail",
|
|
"/mnt/backup (nfs4 from 192.0.2.10:/backup): mounted; from /etc/fstab line 6",
|
|
"/ (ext4 from UUID=aaaa): mounted; from /etc/fstab line 2",
|
|
} {
|
|
if !strings.Contains(said, want) {
|
|
t.Errorf("said lacks %q:\n%s", want, said)
|
|
}
|
|
}
|
|
if strings.Contains(said, "mqueue") {
|
|
t.Errorf("a kernel filesystem was said:\n%s", said)
|
|
}
|
|
for _, mt := range r.Mounts {
|
|
if mt.Mountpoint == "/mnt/backup" && (mt.Automount == nil || mt.MountUnit == nil) {
|
|
t.Errorf("/mnt/backup must carry both units: %+v", mt)
|
|
}
|
|
if mt.Mountpoint == "/storage/media" && mt.MountUnit.From != "generated from /etc/fstab" {
|
|
t.Errorf("from = %q", mt.MountUnit.From)
|
|
}
|
|
}
|
|
all, _ := json.Marshal(r)
|
|
if strings.Contains(string(all), "s3cret") || strings.Contains(string(all), "leaked") {
|
|
t.Errorf("a credential was answered: %s", all)
|
|
}
|
|
}
|
|
|
|
func TestListNarrowed(t *testing.T) {
|
|
m := listFake(t).machine()
|
|
if r := m.List(true, ""); len(r.Mounts) != 3 {
|
|
t.Errorf("network only: got %d mounts, want 3: %v", len(r.Mounts), r.Said)
|
|
}
|
|
if r := m.List(false, "192.0.2.20"); len(r.Mounts) != 1 || r.Mounts[0].Mountpoint != "/mnt/my share" {
|
|
t.Errorf("match on source: %v", r.Said)
|
|
}
|
|
}
|
|
|
|
func TestListWithoutFstabSaysSo(t *testing.T) {
|
|
f := newFake(t)
|
|
f.file("/proc/self/mountinfo", sampleMountinfo)
|
|
r := f.machine().List(false, "")
|
|
notes := strings.Join(r.Notes, "\n")
|
|
if !strings.Contains(notes, "/etc/fstab: not present") || !strings.Contains(notes, "units were not read") {
|
|
t.Errorf("a file or command that could not be read must be said: %q", notes)
|
|
}
|
|
}
|