Some basic functionality
This commit is contained in:
parent
c6201750a7
commit
a1969b681a
73
config.hcl
73
config.hcl
@ -1,30 +1,73 @@
|
||||
job "My App" {
|
||||
// Example job file
|
||||
job "MyApp" {
|
||||
schedule = "* * * * *"
|
||||
|
||||
config {
|
||||
repo = "s3://..."
|
||||
passphrase = "foo"
|
||||
}
|
||||
|
||||
task "Dump mysql" {
|
||||
mysql {
|
||||
hostname = "foo"
|
||||
username = "bar"
|
||||
env = {
|
||||
"foo" = "bar",
|
||||
}
|
||||
options {
|
||||
VerboseLevel = 3
|
||||
}
|
||||
}
|
||||
|
||||
task "Create biz file" {
|
||||
on_backup {
|
||||
body = <<EOF
|
||||
mysql "DumpMainDB" {
|
||||
hostname = "foo"
|
||||
username = "bar"
|
||||
}
|
||||
|
||||
sqlite "DumpSqlite" {
|
||||
path = "/db/path"
|
||||
}
|
||||
|
||||
task "RunSomePreScripts" {
|
||||
script {
|
||||
on_backup = <<EOF
|
||||
echo foo > /biz.txt
|
||||
EOF
|
||||
|
||||
on_restore = "/foo/bar.sh"
|
||||
}
|
||||
|
||||
script {
|
||||
on_backup = <<EOF
|
||||
echo bar >> /biz.txt
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
task "Backup data files" {
|
||||
files = [
|
||||
"/foo/bar",
|
||||
"/biz.txt",
|
||||
]
|
||||
task "ActuallyBackupSomeStuff" {
|
||||
backup {
|
||||
files =[
|
||||
"/foo/bar",
|
||||
"/biz.txt",
|
||||
]
|
||||
|
||||
backup_opts {
|
||||
Tags = ["service"]
|
||||
}
|
||||
|
||||
restore_opts {
|
||||
Verify = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task "RunSomePostScripts" {
|
||||
script {
|
||||
on_backup = <<EOF
|
||||
rm /biz.txt
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
forget {
|
||||
KeepLast = 3
|
||||
KeepWeekly = 2
|
||||
KeepMonthly = 2
|
||||
KeepYearly = 2
|
||||
Prune = true
|
||||
}
|
||||
}
|
||||
|
341
job.go
341
job.go
@ -1,58 +1,303 @@
|
||||
package main
|
||||
|
||||
// JobConfig is all configuration to be sent to Restic
|
||||
type JobConfig struct {
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const WorkDirPerms = 0o666
|
||||
|
||||
type TaskConfig struct {
|
||||
JobDir string
|
||||
Env map[string]string
|
||||
Logger *log.Logger
|
||||
Restic *ResticCmd
|
||||
}
|
||||
|
||||
// ResticConfig is all configuration to be sent to Restic
|
||||
type ResticConfig struct {
|
||||
Repo string `hcl:"repo"`
|
||||
Passphrase string `hcl:"passphrase"`
|
||||
Passphrase string `hcl:"passphrase,optional"`
|
||||
Env map[string]string `hcl:"env,optional"`
|
||||
Args []string `hcl:"args"`
|
||||
GlobalOpts *ResticGlobalOpts `hcl:"options,block"`
|
||||
}
|
||||
|
||||
// ExecutableTask is a task to be run before or after backup/retore
|
||||
type ExecutableTask interface {
|
||||
RunBackup(cfg TaskConfig) error
|
||||
RunRestore(cfg TaskConfig) error
|
||||
Name() string
|
||||
}
|
||||
|
||||
// JobTaskScript is a sript to be executed as part of a job task
|
||||
type JobTaskScript struct {
|
||||
ScriptPath string `hcl:"path,optional"`
|
||||
Body string `hcl:"body,optional"`
|
||||
OnBackup string `hcl:"on_backup,optional"`
|
||||
OnRestore string `hcl:"on_restore,optional"`
|
||||
FromJobDir bool `hcl:"from_job_dir,optional"`
|
||||
env map[string]string
|
||||
name string
|
||||
}
|
||||
|
||||
// RunBackup runs script on backup
|
||||
func (t JobTaskScript) RunBackup(cfg TaskConfig) error {
|
||||
env := MergeEnv(cfg.Env, t.env)
|
||||
if env == nil {
|
||||
env = map[string]string{}
|
||||
}
|
||||
|
||||
env["RESTIC_JOB_DIR"] = cfg.JobDir
|
||||
|
||||
cwd := ""
|
||||
if t.FromJobDir {
|
||||
cwd = cfg.JobDir
|
||||
}
|
||||
|
||||
if err := RunShell(t.OnBackup, cwd, env, cfg.Logger); err != nil {
|
||||
return fmt.Errorf("failed running task script %s: %w", t.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunRestore script on restore
|
||||
func (t JobTaskScript) RunRestore(cfg TaskConfig) error {
|
||||
env := MergeEnv(cfg.Env, t.env)
|
||||
if env == nil {
|
||||
env = map[string]string{}
|
||||
}
|
||||
|
||||
env["RESTIC_JOB_DIR"] = cfg.JobDir
|
||||
|
||||
cwd := ""
|
||||
if t.FromJobDir {
|
||||
cwd = cfg.JobDir
|
||||
}
|
||||
|
||||
if err := RunShell(t.OnRestore, cwd, env, cfg.Logger); err != nil {
|
||||
return fmt.Errorf("failed running task script %s: %w", t.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t JobTaskScript) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *JobTaskScript) SetName(name string) {
|
||||
t.name = name
|
||||
}
|
||||
|
||||
// JobTaskMySQL is a sqlite backup task that performs required pre and post tasks
|
||||
type JobTaskMySQL struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Hostname string `hcl:"hostname,optional"`
|
||||
Database string `hcl:"database,optional"`
|
||||
Username string `hcl:"username,optional"`
|
||||
Password string `hcl:"password,optional"`
|
||||
}
|
||||
|
||||
func (t JobTaskMySQL) GetPreTask() ExecutableTask {
|
||||
return JobTaskScript{
|
||||
name: t.Name,
|
||||
OnBackup: fmt.Sprintf(
|
||||
"mysqldump -h '%s' -u '%s' -p '%s' '%s' > './%s.sql'",
|
||||
t.Hostname,
|
||||
t.Username,
|
||||
t.Password,
|
||||
t.Database,
|
||||
t.Name,
|
||||
),
|
||||
FromJobDir: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (t JobTaskMySQL) GetPostTask() ExecutableTask {
|
||||
return JobTaskScript{
|
||||
name: t.Name,
|
||||
OnRestore: fmt.Sprintf(
|
||||
"mysql -h '%s' -u '%s' -p '%s' '%s' << './%s.sql'",
|
||||
t.Hostname,
|
||||
t.Username,
|
||||
t.Password,
|
||||
t.Database,
|
||||
t.Name,
|
||||
),
|
||||
FromJobDir: true,
|
||||
}
|
||||
}
|
||||
|
||||
// JobTaskSqlite is a sqlite backup task that performs required pre and post tasks
|
||||
type JobTaskSqlite struct {
|
||||
Path string `hcl:"path,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
Path string `hcl:"path"`
|
||||
}
|
||||
|
||||
func (t JobTaskSqlite) GetPreTask() ExecutableTask {
|
||||
return JobTaskScript{
|
||||
name: t.Name,
|
||||
OnBackup: fmt.Sprintf(
|
||||
"sqlite3 %s '.backup $RESTIC_JOB_DIR/%s.bak'",
|
||||
t.Path, t.Name,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (t JobTaskSqlite) GetPostTask() ExecutableTask {
|
||||
return JobTaskScript{
|
||||
name: t.Name,
|
||||
OnRestore: fmt.Sprintf("cp '$RESTIC_JOB_DIR/%s.bak' '%s'", t.Name, t.Path),
|
||||
}
|
||||
}
|
||||
|
||||
type BackupFilesTask struct {
|
||||
Files []string `hcl:"files"`
|
||||
BackupOpts *BackupOpts `hcl:"backup_opts,block"`
|
||||
RestoreOpts *RestoreOpts `hcl:"restore_opts,block"`
|
||||
name string
|
||||
}
|
||||
|
||||
func (t BackupFilesTask) RunBackup(cfg TaskConfig) error {
|
||||
if err := cfg.Restic.Backup(t.Files, t.BackupOpts); err != nil {
|
||||
err = fmt.Errorf("failed backing up files: %w", err)
|
||||
cfg.Logger.Fatal(err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t BackupFilesTask) RunRestore(cfg TaskConfig) error {
|
||||
if err := cfg.Restic.Restore("latest", t.RestoreOpts); err != nil {
|
||||
err = fmt.Errorf("failed restoring files: %w", err)
|
||||
cfg.Logger.Fatal(err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t BackupFilesTask) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *BackupFilesTask) SetName(name string) {
|
||||
t.name = name
|
||||
}
|
||||
|
||||
// JobTask represents a single task within a backup job
|
||||
type JobTask struct {
|
||||
Name string `hcl:"name,label"`
|
||||
OnBackup []JobTaskScript `hcl:"on_backup,block"`
|
||||
OnRestore []JobTaskScript `hcl:"on_restore,block"`
|
||||
MySql []JobTaskMySQL `hcl:"mysql,block"`
|
||||
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
|
||||
Files []string `hcl:"files,optional"`
|
||||
Name string `hcl:"name,label"`
|
||||
Scripts []JobTaskScript `hcl:"script,block"`
|
||||
Backup *BackupFilesTask `hcl:"backup,block"`
|
||||
}
|
||||
|
||||
func (t JobTask) GetTasks() []ExecutableTask {
|
||||
allTasks := []ExecutableTask{}
|
||||
|
||||
for _, exTask := range t.Scripts {
|
||||
exTask.SetName(t.Name)
|
||||
allTasks = append(allTasks, exTask)
|
||||
}
|
||||
|
||||
if t.Backup != nil {
|
||||
t.Backup.SetName(t.Name)
|
||||
allTasks = append(allTasks, t.Backup)
|
||||
}
|
||||
|
||||
return allTasks
|
||||
}
|
||||
|
||||
// Job contains all configuration required to construct and run a backup
|
||||
// and restore job
|
||||
type Job struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Schedule string `hcl:"schedule"`
|
||||
Config JobConfig `hcl:"config,block"`
|
||||
Tasks []JobTask `hcl:"task,block"`
|
||||
Validate bool `hcl:"validate,optional"`
|
||||
Name string `hcl:"name,label"`
|
||||
Schedule string `hcl:"schedule"`
|
||||
Config ResticConfig `hcl:"config,block"`
|
||||
Tasks []JobTask `hcl:"task,block"`
|
||||
Validate bool `hcl:"validate,optional"`
|
||||
Forget *ForgetOpts `hcl:"forget,block"`
|
||||
|
||||
// Meta Tasks
|
||||
MySql []JobTaskMySQL `hcl:"mysql,block"`
|
||||
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
|
||||
}
|
||||
|
||||
func (job Job) NewRestic() ResticCmd {
|
||||
return ResticCmd{
|
||||
LogPrefix: job.Name,
|
||||
Repo: job.Config.Repo,
|
||||
Env: job.Config.Env,
|
||||
Passphrase: job.Config.Passphrase,
|
||||
func (j Job) AllTasks() []ExecutableTask {
|
||||
allTasks := []ExecutableTask{}
|
||||
|
||||
// Pre tasks
|
||||
for _, mysql := range j.MySql {
|
||||
allTasks = append(allTasks, mysql.GetPreTask())
|
||||
}
|
||||
|
||||
for _, sqlite := range j.Sqlite {
|
||||
allTasks = append(allTasks, sqlite.GetPreTask())
|
||||
}
|
||||
|
||||
// Get ordered tasks
|
||||
for _, jobTask := range j.Tasks {
|
||||
allTasks = append(allTasks, jobTask.GetTasks()...)
|
||||
}
|
||||
|
||||
// Post tasks
|
||||
for _, mysql := range j.MySql {
|
||||
allTasks = append(allTasks, mysql.GetPreTask())
|
||||
}
|
||||
|
||||
for _, sqlite := range j.Sqlite {
|
||||
allTasks = append(allTasks, sqlite.GetPreTask())
|
||||
}
|
||||
|
||||
return allTasks
|
||||
}
|
||||
|
||||
func (j Job) JobDir() string {
|
||||
cwd := filepath.Join("/restic_backup", j.Name)
|
||||
_ = os.MkdirAll(cwd, WorkDirPerms)
|
||||
|
||||
return cwd
|
||||
}
|
||||
|
||||
func (j Job) RunTasks() error {
|
||||
logger := GetLogger(j.Name)
|
||||
restic := j.NewRestic()
|
||||
jobDir := j.JobDir()
|
||||
|
||||
if err := restic.EnsureInit(); err != nil {
|
||||
return fmt.Errorf("failed to init restic for job %s: %w", j.Name, err)
|
||||
}
|
||||
|
||||
for _, exTask := range j.AllTasks() {
|
||||
taskCfg := TaskConfig{
|
||||
JobDir: jobDir,
|
||||
Logger: GetChildLogger(logger, exTask.Name()),
|
||||
Restic: restic,
|
||||
}
|
||||
|
||||
if err := exTask.RunBackup(taskCfg); err != nil {
|
||||
return fmt.Errorf("failed running job %s: %w", j.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if j.Forget != nil {
|
||||
restic.Forget(j.Forget)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j Job) NewRestic() *ResticCmd {
|
||||
return &ResticCmd{
|
||||
Logger: GetLogger(j.Name),
|
||||
Repo: j.Config.Repo,
|
||||
Env: j.Config.Env,
|
||||
Passphrase: j.Config.Passphrase,
|
||||
GlobalOpts: j.Config.GlobalOpts,
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,33 +308,33 @@ type Config struct {
|
||||
/***
|
||||
|
||||
job "My App" {
|
||||
schedule = "* * * * *"
|
||||
config {
|
||||
repo = "s3://..."
|
||||
passphrase = "foo"
|
||||
}
|
||||
schedule = "* * * * *"
|
||||
config {
|
||||
repo = "s3://..."
|
||||
passphrase = "foo"
|
||||
}
|
||||
|
||||
task "Dump mysql" {
|
||||
mysql {
|
||||
hostname = "foo"
|
||||
username = "bar"
|
||||
}
|
||||
}
|
||||
task "Dump mysql" {
|
||||
mysql {
|
||||
hostname = "foo"
|
||||
username = "bar"
|
||||
}
|
||||
}
|
||||
|
||||
task "Create biz file" {
|
||||
on_backup {
|
||||
body = <<EOF
|
||||
echo foo > /biz.txt
|
||||
EOF
|
||||
}
|
||||
}
|
||||
task "Create biz file" {
|
||||
on_backup {
|
||||
body = <<EOF
|
||||
echo foo > /biz.txt
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
task "Backup data files" {
|
||||
files = [
|
||||
"/foo/bar",
|
||||
"/biz.txt",
|
||||
]
|
||||
}
|
||||
task "Backup data files" {
|
||||
files = [
|
||||
"/foo/bar",
|
||||
"/biz.txt",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
***/
|
||||
|
17
main.go
17
main.go
@ -26,9 +26,24 @@ func main() {
|
||||
|
||||
var config Config
|
||||
|
||||
if err := hclsimple.DecodeFile("config.hcl", nil, &config); err != nil {
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
log.Fatalf("Requires a path to a job file, but found none")
|
||||
}
|
||||
|
||||
if err := hclsimple.DecodeFile(args[0], nil, &config); err != nil {
|
||||
log.Fatalf("Failed to load configuration: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("Configuration is %#v", config)
|
||||
|
||||
if len(config.Jobs) == 0 {
|
||||
log.Fatalf("No jobs defined in config")
|
||||
}
|
||||
|
||||
for _, job := range config.Jobs {
|
||||
if err := job.RunTasks(); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
321
restic.go
Normal file
321
restic.go
Normal file
@ -0,0 +1,321 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CommandOptions interface {
|
||||
ToArgs() []string
|
||||
}
|
||||
|
||||
type NoOpts struct{}
|
||||
|
||||
func (_ NoOpts) ToArgs() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
type ResticGlobalOpts struct {
|
||||
CaCertFile string `hcl:"CaCertFile,optional"`
|
||||
CacheDir string `hcl:"CacheDir,optional"`
|
||||
PasswordFile string `hcl:"PasswordFile,optional"`
|
||||
TlsClientCertFile string `hcl:"TlsClientCertFile,optional"`
|
||||
LimitDownload int `hcl:"LimitDownload,optional"`
|
||||
LimitUpload int `hcl:"LimitUpload,optional"`
|
||||
VerboseLevel int `hcl:"VerboseLevel,optional"`
|
||||
CleanupCache bool `hcl:"CleanupCache,optional"`
|
||||
NoCache bool `hcl:"NoCache,optional"`
|
||||
NoLock bool `hcl:"NoLock,optional"`
|
||||
}
|
||||
|
||||
func (glo ResticGlobalOpts) ToArgs() (args []string) {
|
||||
if glo.CaCertFile != "" {
|
||||
args = append(args, "--cacert", glo.CaCertFile)
|
||||
}
|
||||
|
||||
if glo.CacheDir != "" {
|
||||
args = append(args, "--cache-dir", glo.CacheDir)
|
||||
}
|
||||
|
||||
if glo.CleanupCache {
|
||||
args = append(args, "--cleanup-cache")
|
||||
}
|
||||
|
||||
if glo.LimitDownload > 0 {
|
||||
args = append(args, "--limit-download", fmt.Sprint(glo.LimitDownload))
|
||||
}
|
||||
|
||||
if glo.LimitUpload > 0 {
|
||||
args = append(args, "--limit-upload", fmt.Sprint(glo.LimitUpload))
|
||||
}
|
||||
|
||||
if glo.NoCache {
|
||||
args = append(args, "--no-cache")
|
||||
}
|
||||
|
||||
if glo.NoLock {
|
||||
args = append(args, "--no-lock")
|
||||
}
|
||||
|
||||
if glo.PasswordFile != "" {
|
||||
args = append(args, "--password-file", glo.PasswordFile)
|
||||
}
|
||||
|
||||
if glo.TlsClientCertFile != "" {
|
||||
args = append(args, "--tls-client-cert", glo.TlsClientCertFile)
|
||||
}
|
||||
|
||||
if glo.VerboseLevel > 0 {
|
||||
args = append(args, "--verbose", fmt.Sprint(glo.VerboseLevel))
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
type ResticCmd struct {
|
||||
Logger *log.Logger
|
||||
Repo string
|
||||
Env map[string]string
|
||||
Passphrase string
|
||||
GlobalOpts *ResticGlobalOpts
|
||||
Cwd string
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) BuildEnv() []string {
|
||||
if rcmd.Env == nil {
|
||||
rcmd.Env = map[string]string{}
|
||||
}
|
||||
|
||||
if rcmd.Passphrase != "" {
|
||||
rcmd.Env["RESTIC_PASSWORD"] = rcmd.Passphrase
|
||||
}
|
||||
|
||||
envList := os.Environ()
|
||||
|
||||
for name, value := range rcmd.Env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
|
||||
return envList
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) RunRestic(command string, options CommandOptions, commandArgs ...string) error {
|
||||
args := []string{}
|
||||
if rcmd.GlobalOpts != nil {
|
||||
args = rcmd.GlobalOpts.ToArgs()
|
||||
}
|
||||
|
||||
args = append(args, "--repo", rcmd.Repo, command)
|
||||
args = append(args, options.ToArgs()...)
|
||||
args = append(args, commandArgs...)
|
||||
|
||||
cmd := exec.Command("restic", args...)
|
||||
|
||||
cmd.Stdout = NewLogWriter(rcmd.Logger)
|
||||
cmd.Stderr = cmd.Stdout
|
||||
cmd.Env = rcmd.BuildEnv()
|
||||
cmd.Dir = rcmd.Cwd
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type BackupOpts struct {
|
||||
Exclude []string `hcl:"Exclude,optional"`
|
||||
Include []string `hcl:"Include,optional"`
|
||||
Tags []string `hcl:"Tags,optional"`
|
||||
Host string `hcl:"Host,optional"`
|
||||
}
|
||||
|
||||
func (bo BackupOpts) ToArgs() (args []string) {
|
||||
for _, tag := range bo.Tags {
|
||||
args = append(args, "--tag", tag)
|
||||
}
|
||||
|
||||
if bo.Host != "" {
|
||||
args = append(args, "--host", bo.Host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Backup(files []string, options *BackupOpts) error {
|
||||
if options == nil {
|
||||
options = &BackupOpts{}
|
||||
}
|
||||
|
||||
err := rcmd.RunRestic("backup", options, files...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type RestoreOpts struct {
|
||||
Exclude []string `hcl:"Exclude,optional"`
|
||||
Host []string `hcl:"Host,optional"`
|
||||
Include []string `hcl:"Include,optional"`
|
||||
Path string `hcl:"Path,optional"`
|
||||
Tags []string `hcl:"Tags,optional"`
|
||||
Target string `hcl:"Target,optional"`
|
||||
Verify bool `hcl:"Verify,optional"`
|
||||
}
|
||||
|
||||
func (ro RestoreOpts) ToArgs() (args []string) {
|
||||
for _, exclude := range ro.Exclude {
|
||||
args = append(args, "--exclude", exclude)
|
||||
}
|
||||
|
||||
for _, include := range ro.Include {
|
||||
args = append(args, "--include", include)
|
||||
}
|
||||
|
||||
for _, host := range ro.Host {
|
||||
args = append(args, "--host", host)
|
||||
}
|
||||
|
||||
if ro.Path != "" {
|
||||
args = append(args, "--path", ro.Path)
|
||||
}
|
||||
|
||||
for _, tag := range ro.Tags {
|
||||
args = append(args, "--tag", tag)
|
||||
}
|
||||
|
||||
if ro.Target != "" {
|
||||
args = append(args, "--target", ro.Target)
|
||||
}
|
||||
|
||||
if ro.Verify {
|
||||
args = append(args, "--verify")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Restore(snapshot string, opts *RestoreOpts) error {
|
||||
if opts == nil {
|
||||
opts = &RestoreOpts{}
|
||||
}
|
||||
|
||||
err := rcmd.RunRestic("restore", opts, snapshot)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type ForgetOpts struct {
|
||||
KeepLast int `hcl:"KeepLast,optional"`
|
||||
KeepHourly int `hcl:"KeepHourly,optional"`
|
||||
KeepDaily int `hcl:"KeepDaily,optional"`
|
||||
KeepWeekly int `hcl:"KeepWeekly,optional"`
|
||||
KeepMonthly int `hcl:"KeepMonthly,optional"`
|
||||
KeepYearly int `hcl:"KeepYearly,optional"`
|
||||
|
||||
KeepWithin time.Duration `hcl:"KeepWithin,optional"`
|
||||
KeepWithinHourly time.Duration `hcl:"KeepWithinHourly,optional"`
|
||||
KeepWithinDaily time.Duration `hcl:"KeepWithinDaily,optional"`
|
||||
KeepWithinWeekly time.Duration `hcl:"KeepWithinWeekly,optional"`
|
||||
KeepWithinMonthly time.Duration `hcl:"KeepWithinMonthly,optional"`
|
||||
KeepWithinYearly time.Duration `hcl:"KeepWithinYearly,optional"`
|
||||
|
||||
Tags []string `hcl:"Tags,optional"`
|
||||
KeepTags []string `hcl:"KeepTags,optional"`
|
||||
|
||||
Prune bool `hcl:"Prune,optional"`
|
||||
}
|
||||
|
||||
func (fo ForgetOpts) ToArgs() (args []string) {
|
||||
// Add keep-*
|
||||
if fo.KeepLast > 0 {
|
||||
args = append(args, "--keep-last", fmt.Sprint(fo.KeepLast))
|
||||
}
|
||||
|
||||
if fo.KeepHourly > 0 {
|
||||
args = append(args, "--keep-hourly", fmt.Sprint(fo.KeepHourly))
|
||||
}
|
||||
|
||||
if fo.KeepDaily > 0 {
|
||||
args = append(args, "--keep-daily", fmt.Sprint(fo.KeepDaily))
|
||||
}
|
||||
|
||||
if fo.KeepWeekly > 0 {
|
||||
args = append(args, "--keep-weekly", fmt.Sprint(fo.KeepWeekly))
|
||||
}
|
||||
|
||||
if fo.KeepMonthly > 0 {
|
||||
args = append(args, "--keep-monthly", fmt.Sprint(fo.KeepMonthly))
|
||||
}
|
||||
|
||||
if fo.KeepYearly > 0 {
|
||||
args = append(args, "--keep-yearly", fmt.Sprint(fo.KeepYearly))
|
||||
}
|
||||
|
||||
if fo.KeepWithin > 0 {
|
||||
args = append(args, "--keep-within", fmt.Sprint(fo.KeepWithin))
|
||||
}
|
||||
|
||||
// Add keep-within-*
|
||||
if fo.KeepWithinHourly > 0 {
|
||||
args = append(args, "--keep-within-hourly", fo.KeepWithinHourly.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinDaily > 0 {
|
||||
args = append(args, "--keep-within-daily", fo.KeepWithinDaily.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinWeekly > 0 {
|
||||
args = append(args, "--keep-within-weekly", fo.KeepWithinWeekly.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinMonthly > 0 {
|
||||
args = append(args, "--keep-within-monthly", fo.KeepWithinMonthly.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinYearly > 0 {
|
||||
args = append(args, "--keep-within-yearly", fo.KeepWithinYearly.String())
|
||||
}
|
||||
|
||||
// Add tags
|
||||
if len(fo.Tags) > 0 {
|
||||
args = append(args, "--tag", strings.Join(fo.Tags, ","))
|
||||
}
|
||||
|
||||
if len(fo.KeepTags) > 0 {
|
||||
args = append(args, "--keep-tag", strings.Join(fo.Tags, ","))
|
||||
}
|
||||
|
||||
// Add prune options
|
||||
|
||||
if fo.Prune {
|
||||
args = append(args, "--prune")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Forget(forgetOpts *ForgetOpts) error {
|
||||
if forgetOpts == nil {
|
||||
forgetOpts = &ForgetOpts{}
|
||||
}
|
||||
|
||||
err := rcmd.RunRestic("forget", forgetOpts)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Check() error {
|
||||
err := rcmd.RunRestic("check", NoOpts{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) EnsureInit() error {
|
||||
if err := rcmd.RunRestic("snapshots", NoOpts{}); err != nil {
|
||||
return rcmd.RunRestic("init", NoOpts{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
169
run.go
169
run.go
@ -1,169 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultFlags = log.LstdFlags | log.Lmsgprefix
|
||||
|
||||
type ResticCmd struct {
|
||||
LogPrefix string
|
||||
Repo string
|
||||
Env map[string]string
|
||||
Passphrase string
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) BuildEnv() []string {
|
||||
rcmd.Env["RESTIC_PASSWORD"] = rcmd.Passphrase
|
||||
|
||||
envList := []string{}
|
||||
|
||||
for name, value := range rcmd.Env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
|
||||
return envList
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) RunRestic(args []string) error {
|
||||
cmd := exec.Command("restic", args...)
|
||||
|
||||
cmd.Stdout = rcmd.Logger().Writer()
|
||||
cmd.Stderr = cmd.Stdout
|
||||
cmd.Env = rcmd.BuildEnv()
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Logger() *log.Logger {
|
||||
logger := log.New(os.Stderr, rcmd.LogPrefix, defaultFlags)
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Backup(path string, args []string) error {
|
||||
args = append([]string{"--repo", rcmd.Repo, "backup"}, args...)
|
||||
args = append(args, path)
|
||||
|
||||
err := rcmd.RunRestic(args)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type ForgetOpts struct {
|
||||
KeepLast int
|
||||
KeepHourly int
|
||||
KeepDaily int
|
||||
KeepWeekly int
|
||||
KeepMonthly int
|
||||
KeepYearly int
|
||||
|
||||
KeepWithin time.Duration
|
||||
KeepWithinHourly time.Duration
|
||||
KeepWithinDaily time.Duration
|
||||
KeepWithinWeekly time.Duration
|
||||
KeepWithinMonthly time.Duration
|
||||
KeepWithinYearly time.Duration
|
||||
|
||||
Tags []string
|
||||
KeepTags []string
|
||||
|
||||
Prune bool
|
||||
}
|
||||
|
||||
func (fo ForgetOpts) ToArgs() []string {
|
||||
args := []string{}
|
||||
|
||||
// Add keep-*
|
||||
|
||||
if fo.KeepLast > 0 {
|
||||
args = append(args, "--keep-last", fmt.Sprint(fo.KeepLast))
|
||||
}
|
||||
|
||||
if fo.KeepHourly > 0 {
|
||||
args = append(args, "--keep-hourly", fmt.Sprint(fo.KeepHourly))
|
||||
}
|
||||
|
||||
if fo.KeepDaily > 0 {
|
||||
args = append(args, "--keep-daily", fmt.Sprint(fo.KeepDaily))
|
||||
}
|
||||
|
||||
if fo.KeepWeekly > 0 {
|
||||
args = append(args, "--keep-weekly", fmt.Sprint(fo.KeepWeekly))
|
||||
}
|
||||
|
||||
if fo.KeepMonthly > 0 {
|
||||
args = append(args, "--keep-monthly", fmt.Sprint(fo.KeepMonthly))
|
||||
}
|
||||
|
||||
if fo.KeepYearly > 0 {
|
||||
args = append(args, "--keep-yearly", fmt.Sprint(fo.KeepYearly))
|
||||
}
|
||||
|
||||
if fo.KeepWithin > 0 {
|
||||
args = append(args, "--keep-within", fmt.Sprint(fo.KeepWithin))
|
||||
}
|
||||
|
||||
// Add keep-within-*
|
||||
|
||||
if fo.KeepWithinHourly > 0 {
|
||||
args = append(args, "--keep-within-hourly", fo.KeepWithinHourly.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinDaily > 0 {
|
||||
args = append(args, "--keep-within-daily", fo.KeepWithinDaily.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinWeekly > 0 {
|
||||
args = append(args, "--keep-within-weekly", fo.KeepWithinWeekly.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinMonthly > 0 {
|
||||
args = append(args, "--keep-within-monthly", fo.KeepWithinMonthly.String())
|
||||
}
|
||||
|
||||
if fo.KeepWithinYearly > 0 {
|
||||
args = append(args, "--keep-within-yearly", fo.KeepWithinYearly.String())
|
||||
}
|
||||
|
||||
// Add tags
|
||||
|
||||
if len(fo.Tags) > 0 {
|
||||
args = append(args, "--tag", strings.Join(fo.Tags, ","))
|
||||
}
|
||||
|
||||
if len(fo.KeepTags) > 0 {
|
||||
args = append(args, "--keep-tag", strings.Join(fo.Tags, ","))
|
||||
}
|
||||
|
||||
// Add prune options
|
||||
|
||||
if fo.Prune {
|
||||
args = append(args, "--prune")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Cleanup(forgetOpts ForgetOpts) error {
|
||||
args := append([]string{"--repo", rcmd.Repo, "forget"}, forgetOpts.ToArgs()...)
|
||||
|
||||
err := rcmd.RunRestic(args)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (rcmd ResticCmd) Check() error {
|
||||
args := []string{"--repo", rcmd.Repo, "check"}
|
||||
|
||||
err := rcmd.RunRestic(args)
|
||||
|
||||
return err
|
||||
}
|
78
shell.go
Normal file
78
shell.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
loggerFlags = log.LstdFlags | log.Lmsgprefix
|
||||
loggers = map[string]*log.Logger{}
|
||||
)
|
||||
|
||||
func GetLogger(name string) *log.Logger {
|
||||
if logger, ok := loggers[name]; ok {
|
||||
return logger
|
||||
}
|
||||
|
||||
logger := log.New(os.Stderr, name+":", loggerFlags)
|
||||
loggers[name] = logger
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func GetChildLogger(parent *log.Logger, name string) *log.Logger {
|
||||
childName := fmt.Sprintf("%s%s", parent.Prefix(), name)
|
||||
|
||||
return GetLogger(childName)
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewLogWriter(logger *log.Logger) *logWriter {
|
||||
return &logWriter{logger}
|
||||
}
|
||||
|
||||
func (w logWriter) Write(p []byte) (n int, err error) {
|
||||
message := fmt.Sprintf("%s", p)
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
w.logger.Printf(" %s", line)
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func RunShell(script string, cwd string, env map[string]string, logger *log.Logger) error {
|
||||
cmd := exec.Command("sh", "-c", strings.TrimSpace(script))
|
||||
|
||||
// Make both stderr and stdout go to logger
|
||||
// fmt.Println("LOGGER PREFIX", logger.Prefix())
|
||||
// logger.Println("From logger")
|
||||
cmd.Stdout = NewLogWriter(logger)
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
// Set working directory
|
||||
cmd.Dir = cwd
|
||||
|
||||
// Convert env to list if values provided
|
||||
if len(env) > 0 {
|
||||
envList := os.Environ()
|
||||
|
||||
for name, value := range env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
|
||||
cmd.Env = envList
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("shell execution failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
40
test.hcl
Normal file
40
test.hcl
Normal file
@ -0,0 +1,40 @@
|
||||
job "TestBackup" {
|
||||
schedule = "1 * * * *"
|
||||
|
||||
config {
|
||||
repo = "./backups"
|
||||
passphrase = "supersecret"
|
||||
|
||||
options {
|
||||
CacheDir = "./cache"
|
||||
}
|
||||
}
|
||||
|
||||
task "before script" {
|
||||
script {
|
||||
on_backup = "echo before backup!"
|
||||
}
|
||||
}
|
||||
|
||||
task "backup" {
|
||||
backup {
|
||||
files = [
|
||||
"./data"
|
||||
]
|
||||
|
||||
backup_opts {
|
||||
Tags = ["foo"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task "after script" {
|
||||
script {
|
||||
on_backup = "echo after backup!"
|
||||
}
|
||||
}
|
||||
|
||||
forget {
|
||||
KeepLast = 2
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user