Automatic snapshot mounting and destroying. Upgrade to Objectivefs 7.2.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-03-19 18:03:29 +01:00
parent a8823ac60a
commit ca04d2d9e2
3 changed files with 263 additions and 13 deletions

Binary file not shown.

View File

@@ -92,6 +92,13 @@
"settable": [
"value"
]
},
{
"name": "OBJECTIVEFS_MOUNT_SNAPSHOTS",
"value": "no",
"settable": [
"value"
]
}
],
"network": {

269
main.go
View File

@@ -22,6 +22,7 @@ import (
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strconv"
"strings"
@@ -34,11 +35,12 @@ import (
)
type ofsVolume struct {
Volume *volume.Volume
Fs string
Opts string
Env []string
Asap bool
Volume *volume.Volume
Fs string
Opts string
Env []string
Asap bool
MountSnapshots bool
}
type ofsVolumeRt struct {
@@ -48,16 +50,23 @@ type ofsVolumeRt struct {
type ofsDriver struct {
sync.RWMutex
volumedb *bolt.DB
volumeRt map[string]*ofsVolumeRt
defEnv map[string]string
defMountOpt string
volumedb *bolt.DB
volumeRt map[string]*ofsVolumeRt
defEnv map[string]string
defMountOpt string
defMountSnapshots bool
}
type snapshotRule struct {
count int
period time.Duration
}
var version = "1.0"
const (
volumeBucket = "volumes"
volumeBucket = "volumes"
snapshotsDirectory = "snapshots"
)
func (d *ofsDriver) Create(r *volume.CreateRequest) error {
@@ -80,6 +89,7 @@ func (d *ofsDriver) Create(r *volume.CreateRequest) error {
v.Volume = &volume.Volume{Name: r.Name, Mountpoint: filepath.Join(volume.DefaultDockerRootDirectory, "objectivefs", r.Name), CreatedAt: time.Now().Format(time.RFC3339Nano)}
v.Opts = d.defMountOpt
v.Fs = r.Name
v.MountSnapshots = d.defMountSnapshots
env := make(map[string]string)
for id, val := range d.defEnv {
env[id] = val
@@ -96,6 +106,8 @@ func (d *ofsDriver) Create(r *volume.CreateRequest) error {
}
case "asap":
v.Asap = true
case "mountSnapshots":
v.MountSnapshots = val == "yes"
default:
env[key] = val
}
@@ -289,14 +301,79 @@ func (d *ofsDriver) Mount(r *volume.MountRequest) (*volume.MountResponse, error)
}
// Check for mount
mount := exec.Command("df", "--output=fstype", v.Volume.Mountpoint)
if out, err := mount.CombinedOutput(); err == nil && strings.Index(string(out), "fuse.objectivefs") >= 0 {
if isObjfs, err := isObjectiveFsMount(v.Volume.Mountpoint); err == nil && isObjfs {
break
}
}
log.WithFields(log.Fields{"name": r.Name}).Info("Volume mounted")
rt.mounted = true
if v.MountSnapshots {
go func() {
snapshotsPath := path.Join(v.Volume.Mountpoint, snapshotsDirectory)
log.WithFields(log.Fields{"name": r.Name, "path": snapshotsPath}).Info("Snapshot auto-mount is starting")
if _, err := os.Stat(snapshotsPath); os.IsNotExist(err) {
log.WithFields(log.Fields{"name": r.Name, "directory": snapshotsDirectory}).Info("Creating the snapshots mount directory")
if err := os.Mkdir(snapshotsPath, os.ModePerm); err != nil {
log.WithFields(log.Fields{"name": r.Name}).Error("Failed to create the snapshots mount directory. Snapshot mounting will be disabled.")
return
}
}
for cmd.ProcessState == nil {
if snapshotRulesB, err := applyEnv(exec.Command("/sbin/mount.objectivefs", "snapshot", "-l", v.Fs), v.Env).Output(); err == nil {
if expectedSnapshots, err := generateSnapshotsFromRulesForNow(string(snapshotRulesB)); err == nil {
if existingSnapshotsB, err := applyEnv(exec.Command("/sbin/mount.objectivefs", "list", "-sz", v.Fs), v.Env).Output(); err == nil {
if existingSnapshots, err := parseExistingSnapshots(string(existingSnapshotsB), v.Fs); err == nil {
if existingMounts, err := getMountedSnaphots(snapshotsPath); err == nil {
// Destroy old snapshots
for i, name := range setMinus(existingSnapshots, expectedSnapshots) {
expectedOutput := "Snapshot '" + name + "' destroyed."
if output, err := applyEnv(exec.Command("/sbin/mount.objectivefs", "destroy", name, "-f"), v.Env).Output(); err != nil || string(output) != expectedOutput {
log.WithFields(log.Fields{"name": r.Name, "snapshot": i}).Warn("Failed to destroy an expired snapshot.")
}
}
// Remove mounts of expired snapshots
for i, path := range setMinus(existingMounts, expectedSnapshots) {
if err := exec.Command("umount", path).Run(); err != nil {
log.WithFields(log.Fields{"name": r.Name, "snapshot": i}).Warn("Failed to unmount an expired snapshot.")
}
if err := os.Remove(path); err != nil {
log.WithFields(log.Fields{"name": r.Name, "snapshot": i}).Warn("Failed to remove directory of an expired snapshot.")
}
}
// Add new mounts
for i, name := range setMinus(setIntersect(existingSnapshots, expectedSnapshots), existingMounts) {
dest := filepath.Join(snapshotsPath, i)
if err := applyEnv(exec.Command("/sbin/mount.objectivefs", name, dest), v.Env).Run(); err != nil {
log.WithFields(log.Fields{"name": r.Name, "snapshot": i}).Warn("Failed to destroy an expired snapshot.")
}
}
} else {
log.WithFields(log.Fields{"name": r.Name, "Error": err}).Warn("Cannot determine existing snapshot mounts")
}
} else {
log.WithFields(log.Fields{"name": r.Name, "Error": err}).Warn("Cannot parse existing snapshot names")
}
} else {
log.WithFields(log.Fields{"name": r.Name, "Error": err}).Warn("Cannot list existing snapshot names")
}
} else {
log.WithFields(log.Fields{"name": r.Name, "Error": err}).Warn("Cannot determine expected snapshot names")
}
} else {
log.WithFields(log.Fields{"name": r.Name, "Error": err}).Warn("Cannot detect snapshot frequency")
}
// Cycle periodically
time.Sleep(5 * time.Minute)
}
log.WithFields(log.Fields{"name": r.Name}).Info("Completed snapshot auto-mounting")
}()
}
}
rt.use[r.ID] = true
@@ -381,6 +458,8 @@ func main() {
defMountOpt := os.Getenv("OBJECTIVEFS_MOUNT_OPTIONS")
defMountSnapshots := os.Getenv("OBJECTIVEFS_MOUNT_SNAPSHOTS") == "yes"
db, err := bolt.Open("objectivefs.db", 0600, nil)
if err != nil {
log.Fatal(err)
@@ -394,7 +473,7 @@ func main() {
return nil
})
d := &ofsDriver{volumedb: db, volumeRt: make(map[string]*ofsVolumeRt), defEnv: defEnv, defMountOpt: defMountOpt}
d := &ofsDriver{volumedb: db, volumeRt: make(map[string]*ofsVolumeRt), defEnv: defEnv, defMountOpt: defMountOpt, defMountSnapshots: defMountSnapshots}
h := volume.NewHandler(d)
u, _ := user.Lookup("root")
gid, _ := strconv.Atoi(u.Gid)
@@ -470,3 +549,167 @@ func (p *ofsDriver) removeVolumeInfo(tx *bolt.Tx, volumeName string) error {
bucket := tx.Bucket([]byte(volumeBucket))
return bucket.Delete([]byte(volumeName))
}
/*
func parseSnapshotRules(rulesS string) ([]snapshotRule, error) {
var result []snapshotRule
rules := strings.Split(rulesS, " ")
for _, i := range rules {
var rule snapshotRule
countAndRest := strings.SplitN(i, "@", 2)
if len(countAndRest) != 2 || len(countAndRest[1]) < 2 {
return nil, fmt.Errorf("Failed to parse snapshot rule '" + i + "'")
}
if count, err := strconv.ParseInt(countAndRest[0], 10, 32); err != nil {
return nil, fmt.Errorf("Failed to parse snapshot rule '" + i + "'")
} else {
rule.count = int(count)
}
period := countAndRest[1]
var multiplier time.Duration
switch period[len(period)-1] {
// see https://objectivefs.com/howto/snapshots
case 'm':
multiplier = time.Minute
case 'h':
multiplier = time.Hour
case 'd':
multiplier = 24 * time.Hour
case 'w':
multiplier = 7 * 24 * time.Hour
case 'n':
multiplier = 4 * 7 * 24 * time.Hour // Simple month (4 weeks)
case 'q':
multiplier = 12 * 7 * 24 * time.Hour // Simple quarter (12 weeks)
case 'y':
multiplier = 48 * 7 * 24 * time.Hour // Simple year (48 weeks)
default:
return nil, fmt.Errorf("Failed to parse snapshot period '" + period + "'")
}
if periodCount, err := strconv.ParseInt(period[:len(period)-1], 10, 32); err != nil {
return nil, fmt.Errorf("Failed to parse snapshot rule '" + i + "'")
} else {
rule.period = multiplier * time.Duration(periodCount)
}
result = append(result, rule)
}
return result, nil
}*/
func applyEnv(cmd *exec.Cmd, env []string) *exec.Cmd {
cmd.Env = env
return cmd
}
func generateSnapshotsFromRulesForNow(rules string) (map[string]bool, error) {
if timesB, err := exec.Command("/sbin/mount.objectivefs", "snapshot", "-vs", rules).Output(); err == nil {
var result map[string]bool
scanner := bufio.NewScanner(strings.NewReader(string(timesB)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "Number of automatic snapshots:") || strings.HasPrefix(line, "Automatic schedule:") {
continue
}
if _, err := time.Parse("2006-01-02T15:04:05Z", line); err != nil {
return nil, err
} else {
result[line] = false
}
}
return result, nil
} else {
return nil, err
}
}
func parseExistingSnapshots(data string, expectedPrefix string) (map[string]string, error) {
var result map[string]string
scanner := bufio.NewScanner(strings.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "NAME") {
// Skip column headers
continue
}
fields := strings.Fields(line)
name := fields[0]
uriSchemeAndRest := strings.SplitN(name, "://", 2)
if len(uriSchemeAndRest) != 2 {
return nil, fmt.Errorf("Expected URI scheme in the list of existing snapshosts")
}
hostAndPath := uriSchemeAndRest[1]
if hostAndPath == expectedPrefix {
// This is the live filesystem not a snapshot
continue
}
if !strings.HasPrefix(hostAndPath, expectedPrefix+"@") {
return nil, fmt.Errorf("Unexpected URI in the list of existing snapshosts")
}
time := hostAndPath[len(expectedPrefix)+1:]
result[time] = name
}
return result, nil
}
func getMountedSnaphots(baseDir string) (map[string]string, error) {
if entries, err := os.ReadDir(baseDir); err == nil {
return nil, err
} else {
var result map[string]string
for _, i := range entries {
if i.IsDir() {
iPath := filepath.Join(baseDir, i.Name())
if isObjFs, err := isObjectiveFsMount(iPath); err == nil && isObjFs {
result[i.Name()] = iPath
}
}
}
return result, nil
}
}
func isObjectiveFsMount(path string) (bool, error) {
mount := exec.Command("df", "--output=target", "-t", "fuse.objectivefs", path)
if data, err := mount.CombinedOutput(); err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(data)))
// On success the first line contains column headers (in our case "Mounted on")
// and the second line contains the nearest mount point on the root path
return scanner.Scan() && scanner.Scan() && scanner.Text() == path, nil
} else {
return false, err
}
}
func setMinus[TKey comparable, TValue any, TValue2 any](a map[TKey]TValue, b map[TKey]TValue2) map[TKey]TValue {
var result map[TKey]TValue
for i, j := range a {
if _, contains := b[i]; contains {
result[i] = j
}
}
return result
}
func setIntersect[TKey comparable, TValue any, TValue2 any](a map[TKey]TValue, b map[TKey]TValue2) map[TKey]TValue {
var result map[TKey]TValue
for i, j := range a {
if _, contains := b[i]; contains {
result[i] = j
}
}
return result
}