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 = "* * * * *"
|
schedule = "* * * * *"
|
||||||
|
|
||||||
config {
|
config {
|
||||||
repo = "s3://..."
|
repo = "s3://..."
|
||||||
passphrase = "foo"
|
passphrase = "foo"
|
||||||
}
|
env = {
|
||||||
|
"foo" = "bar",
|
||||||
task "Dump mysql" {
|
}
|
||||||
mysql {
|
options {
|
||||||
hostname = "foo"
|
VerboseLevel = 3
|
||||||
username = "bar"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task "Create biz file" {
|
mysql "DumpMainDB" {
|
||||||
on_backup {
|
hostname = "foo"
|
||||||
body = <<EOF
|
username = "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite "DumpSqlite" {
|
||||||
|
path = "/db/path"
|
||||||
|
}
|
||||||
|
|
||||||
|
task "RunSomePreScripts" {
|
||||||
|
script {
|
||||||
|
on_backup = <<EOF
|
||||||
echo foo > /biz.txt
|
echo foo > /biz.txt
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
on_restore = "/foo/bar.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
script {
|
||||||
|
on_backup = <<EOF
|
||||||
|
echo bar >> /biz.txt
|
||||||
|
EOF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task "Backup data files" {
|
task "ActuallyBackupSomeStuff" {
|
||||||
files = [
|
backup {
|
||||||
"/foo/bar",
|
files =[
|
||||||
"/biz.txt",
|
"/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
|
package main
|
||||||
|
|
||||||
// JobConfig is all configuration to be sent to Restic
|
import (
|
||||||
type JobConfig struct {
|
"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"`
|
Repo string `hcl:"repo"`
|
||||||
Passphrase string `hcl:"passphrase"`
|
Passphrase string `hcl:"passphrase,optional"`
|
||||||
Env map[string]string `hcl:"env,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
|
// JobTaskScript is a sript to be executed as part of a job task
|
||||||
type JobTaskScript struct {
|
type JobTaskScript struct {
|
||||||
ScriptPath string `hcl:"path,optional"`
|
OnBackup string `hcl:"on_backup,optional"`
|
||||||
Body string `hcl:"body,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
|
// JobTaskMySQL is a sqlite backup task that performs required pre and post tasks
|
||||||
type JobTaskMySQL struct {
|
type JobTaskMySQL struct {
|
||||||
|
Name string `hcl:"name,label"`
|
||||||
Hostname string `hcl:"hostname,optional"`
|
Hostname string `hcl:"hostname,optional"`
|
||||||
Database string `hcl:"database,optional"`
|
Database string `hcl:"database,optional"`
|
||||||
Username string `hcl:"username,optional"`
|
Username string `hcl:"username,optional"`
|
||||||
Password string `hcl:"password,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
|
// JobTaskSqlite is a sqlite backup task that performs required pre and post tasks
|
||||||
type JobTaskSqlite struct {
|
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
|
// JobTask represents a single task within a backup job
|
||||||
type JobTask struct {
|
type JobTask struct {
|
||||||
Name string `hcl:"name,label"`
|
Name string `hcl:"name,label"`
|
||||||
OnBackup []JobTaskScript `hcl:"on_backup,block"`
|
Scripts []JobTaskScript `hcl:"script,block"`
|
||||||
OnRestore []JobTaskScript `hcl:"on_restore,block"`
|
Backup *BackupFilesTask `hcl:"backup,block"`
|
||||||
MySql []JobTaskMySQL `hcl:"mysql,block"`
|
}
|
||||||
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
|
|
||||||
Files []string `hcl:"files,optional"`
|
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
|
// Job contains all configuration required to construct and run a backup
|
||||||
// and restore job
|
// and restore job
|
||||||
type Job struct {
|
type Job struct {
|
||||||
Name string `hcl:"name,label"`
|
Name string `hcl:"name,label"`
|
||||||
Schedule string `hcl:"schedule"`
|
Schedule string `hcl:"schedule"`
|
||||||
Config JobConfig `hcl:"config,block"`
|
Config ResticConfig `hcl:"config,block"`
|
||||||
Tasks []JobTask `hcl:"task,block"`
|
Tasks []JobTask `hcl:"task,block"`
|
||||||
Validate bool `hcl:"validate,optional"`
|
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 {
|
func (j Job) AllTasks() []ExecutableTask {
|
||||||
return ResticCmd{
|
allTasks := []ExecutableTask{}
|
||||||
LogPrefix: job.Name,
|
|
||||||
Repo: job.Config.Repo,
|
// Pre tasks
|
||||||
Env: job.Config.Env,
|
for _, mysql := range j.MySql {
|
||||||
Passphrase: job.Config.Passphrase,
|
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" {
|
job "My App" {
|
||||||
schedule = "* * * * *"
|
schedule = "* * * * *"
|
||||||
config {
|
config {
|
||||||
repo = "s3://..."
|
repo = "s3://..."
|
||||||
passphrase = "foo"
|
passphrase = "foo"
|
||||||
}
|
}
|
||||||
|
|
||||||
task "Dump mysql" {
|
task "Dump mysql" {
|
||||||
mysql {
|
mysql {
|
||||||
hostname = "foo"
|
hostname = "foo"
|
||||||
username = "bar"
|
username = "bar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task "Create biz file" {
|
task "Create biz file" {
|
||||||
on_backup {
|
on_backup {
|
||||||
body = <<EOF
|
body = <<EOF
|
||||||
echo foo > /biz.txt
|
echo foo > /biz.txt
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task "Backup data files" {
|
task "Backup data files" {
|
||||||
files = [
|
files = [
|
||||||
"/foo/bar",
|
"/foo/bar",
|
||||||
"/biz.txt",
|
"/biz.txt",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
***/
|
***/
|
||||||
|
17
main.go
17
main.go
@ -26,9 +26,24 @@ func main() {
|
|||||||
|
|
||||||
var config Config
|
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.Fatalf("Failed to load configuration: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Configuration is %#v", config)
|
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