diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e6f45a --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +build: + go build . + +package: build + mkdir -p snapz-1 + cp snapzd snapz-1 + cp systemd/snapzd.service snapz-1/snapzd.service + tar -czvf snapzd.tar.gz snapz-1 + +makepkg: package + makepkg -f --skipinteg + +pacinstall: makepkg + sudo pacman -U snapz-1-1-x86_64.pkg.tar.zst + +clean: + rm snapzd + rm -r snapz-1 + rm snapzd.tar.gz + rm snapz-1-1-x86_64.pkg.tar.zst + rm -r src + rm -r pkg diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..e3daa46 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,52 @@ +# This is an example PKGBUILD file. Use this as a start to creating your own, +# and remove these comments. For more information, see 'man PKGBUILD'. +# NOTE: Please fill out the license field for your package! If it is unknown, +# then please put 'unknown'. + +# Maintainer: Your Name +pkgname=snapz +pkgver=1 +pkgrel=1 +epoch= +pkgdesc="" +arch=('x86_64') +url="" +license=('GPL') +groups=() +depends=() +makedepends=() +checkdepends=() +optdepends=() +provides=() +conflicts=() +replaces=() +backup=() +options=() +install= +changelog= +source=("file://snapzd.tar.gz") +noextract=() +md5sums=() +validpgpkeys=() + +prepare() { + echo "$PWD" +} + +build() { + echo "foo" +} + +check() { + cd "$pkgname-$pkgver" +} + +package() { + cd "$pkgname-$pkgver" + mkdir -p "$pkgdir/usr/bin" + mkdir -p "$pkgdir/etc/snapz/" + cp snapzd "$pkgdir/usr/bin" + + mkdir -p "$pkgdir/usr/lib/systemd/system/" + cp snapzd.service "$pkgdir/usr/lib/systemd/system/" +} diff --git a/configuration/config.go b/configuration/config.go new file mode 100644 index 0000000..e90c4bd --- /dev/null +++ b/configuration/config.go @@ -0,0 +1,60 @@ +package configuration + +import ( + "os" + "path/filepath" + + "github.com/pelletier/go-toml/v2" + "go.uber.org/zap" +) + +const ( + SNAPZ_CONFIG_ROOT = "/etc/snapz" + SNAPZ_JOBS_FILE = "jobs.toml" + SNAPZ_DATASET_DIRECTORY = "datasets.toml" +) + +/* +[[job]] +name = "example" +description = "example job" +cron = "* /1 * * * *" +recursive = true +*/ + +type JobConfig struct { + Name string `toml:"name"` + Description string `toml:"description"` + Cron string `toml:"cron"` + Dataset string `toml:"dataset"` + Recursive bool `toml:"recusrive"` +} + +type JobsConfig struct { + Jobs []JobConfig `toml:"job"` +} + +func getConfigPaths(filename string) []string { + global_file_path := filepath.Join(SNAPZ_CONFIG_ROOT, filename) + return []string{global_file_path} +} + +func GetJobs() ([]JobConfig, error) { + job_file_paths := getConfigPaths(SNAPZ_JOBS_FILE) + jobs := make([]JobConfig, 0) + for _, file := range job_file_paths { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + var jobsConfig JobsConfig + err = toml.Unmarshal(data, &jobsConfig) + if err != nil { + return nil, err + } + zap.S().Infof("adding %v jobs to queue", len(jobsConfig.Jobs)) + jobs = append(jobs, jobsConfig.Jobs...) + } + + return jobs, nil +} diff --git a/configuration/snapshot.go b/configuration/snapshot.go new file mode 100644 index 0000000..fa7e066 --- /dev/null +++ b/configuration/snapshot.go @@ -0,0 +1,62 @@ +package configuration + +import ( + "io/ioutil" + "time" + + "github.com/go-co-op/gocron" + "gopkg.in/yaml.v3" +) + + + +type Schedule struct { + Every int + Unit string + At []string +} + + + +func (s Schedule) ToGocron() *gocron.Scheduler { + out := gocron.NewScheduler(time.Local) + out = out.Every(s.Every) + + switch s.Unit { + case "Day": + out = out.Day() + case "Hour": + out = out.Hour() + default: + return nil + } + + for _, at := range s.At { + out = out.At(at) + } + + return out +} + +type SnapJob struct { + Name string `yaml:"name"` + DatasetPath string `yaml:"dataset_path"` + Description string `yaml:"descriptions"` + Recursive bool `yaml:"recursive"` + Schedules []Schedule `yaml:"schedules"` + Cron string `yaml:"cron"` +} + +func LoadSnap(path string) (*SnapJob, error) { + configFile, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var jobInfo SnapJob + err = yaml.Unmarshal(configFile, &jobInfo) + if err != nil { + return nil, err + } + return &jobInfo, nil +} diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..f067588 --- /dev/null +++ b/example.yaml @@ -0,0 +1,6 @@ +name: "SnapshotUserdata" +description: "Daily snapshot of userdata/home for backup to network storage" +dataset_path: "userdata/home" +recursive: true +cron: "*/1 * * * *" +# Makes a snapshot once a day at 8:00PM local time named autosnap@ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2523f9a --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.ohea.xyz/spaceman/snapzd + +go 1.19 + +require github.com/go-co-op/gocron v1.18.0 + +require ( + github.com/mistifyio/go-zfs v2.1.1+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..18b3ba3 --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +git.ohea.xyz/golang/config v0.0.0-20221002005232-8a901413a8b0 h1:a8ygEuzmqFDxXmf+e1IseDKBcAtkaIwfL3k4PIVVVr8= +git.ohea.xyz/golang/config v0.0.0-20221002005232-8a901413a8b0/go.mod h1:86PbXJ2WdqQ+3hYqrnv3ukgKNRK9nQfThnlY03FAO0g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-co-op/gocron v1.18.0 h1:SxTyJ5xnSN4byCq7b10LmmszFdxQlSQJod8s3gbnXxA= +github.com/go-co-op/gocron v1.18.0/go.mod h1:sD/a0Aadtw5CpflUJ/lpP9Vfdk979Wl1Sg33HPHg0FY= +github.com/mistifyio/go-zfs v2.1.1+incompatible h1:gAMO1HM9xBRONLHHYnu5iFsOJUiJdNZo6oqSENd4eW8= +github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/jobs.toml b/jobs.toml new file mode 100644 index 0000000..4197017 --- /dev/null +++ b/jobs.toml @@ -0,0 +1,7 @@ +[[job]] +name = "dailysnap" +description = "snapshot home dir daily" +cron = "* * * * *" +dataset = "userdata/home" +rescursive = false + diff --git a/main.go b/main.go new file mode 100644 index 0000000..477c8bd --- /dev/null +++ b/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "os" + "time" + + "git.ohea.xyz/spaceman/snapzd/configuration" + "github.com/go-co-op/gocron" + "github.com/mistifyio/go-zfs" + "go.uber.org/zap" +) + +func CreateSnapshotJob(job_info *configuration.JobConfig) func() { + return func() { + fs, err := zfs.Filesystems(job_info.Dataset) + if err != nil { + zap.S().Errorf("failed to start job %v: %v", job_info.Name, err) + return + } + if len(fs) != 1 { + zap.S().Errorf("dataset path returned %v datasets. Expected 1", len(fs)) + return + } + to_snapshot := fs[0] + snaptime := time.Now() + snapname := fmt.Sprintf("snapz.%s.%s", job_info.Name, snaptime.Format(time.RFC3339)) + + + snapshot, err := to_snapshot.Snapshot(snapname, job_info.Recursive) + if err != nil { + zap.S().Errorf("failed to create snapshot: %v: ", snapname, err) + return + } + zap.S().Infof("created snapshot: %v", snapshot.Name) + } +} + +func configureLogging() error { + logger, err := zap.NewDevelopment() + if err != nil { + return err + } + zap.ReplaceGlobals(logger) + return nil +} + +func main() { + err := configureLogging() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to initalize logging, can't continue: %v", err) + return + } + zap.S().Info("begining conifg scan") + jobs, err := configuration.GetJobs() + if err != nil { + zap.S().Fatalf("failed to read configuration: %v", err) + } + + scheduler := gocron.NewScheduler(time.Local) + + for _, job := range jobs { + zap.S().Infof("discovered job: %v", job.Name) + zap.S().Infof("adding job. Name: %v, Dataset: %v, Cron: %v", job.Name, job.Dataset, job.Cron) + scheduler.Cron(job.Cron).SingletonMode().Tag(job.Name).Do(CreateSnapshotJob(&job)) + } + + scheduler.StartBlocking() +} diff --git a/systemd/snapzd.service b/systemd/snapzd.service new file mode 100644 index 0000000..beafca5 --- /dev/null +++ b/systemd/snapzd.service @@ -0,0 +1,10 @@ +[Unit] +Description=snapz snapshot daemon +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/snapzd + +[Install] +WantedBy=multi-user.target