2016-05-22 13:04:37 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
valid "github.com/asaskevich/govalidator"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
)
|
|
|
|
|
2016-05-23 09:54:45 +00:00
|
|
|
const (
|
2016-05-23 19:11:02 +00:00
|
|
|
commandBackup = "backup"
|
|
|
|
commandFullBackup = "full"
|
|
|
|
commandIncrBackup = "incr"
|
|
|
|
commandCleanup = "cleanup"
|
|
|
|
commandList = "list-current-files"
|
|
|
|
commandRestore = "restore"
|
|
|
|
commandStatus = "status"
|
|
|
|
commandVerify = "verify"
|
|
|
|
commandRemove = "__remove_old"
|
2016-05-23 09:54:45 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
notifyCommands = []string{
|
|
|
|
commandBackup,
|
|
|
|
commandRemove,
|
2016-05-24 06:33:25 +00:00
|
|
|
commandFullBackup,
|
|
|
|
commandIncrBackup,
|
2016-05-23 09:54:45 +00:00
|
|
|
}
|
2016-05-23 09:59:23 +00:00
|
|
|
removeCommands = []string{
|
|
|
|
commandBackup,
|
|
|
|
commandCleanup,
|
|
|
|
}
|
2016-05-23 09:54:45 +00:00
|
|
|
)
|
|
|
|
|
2016-05-22 13:04:37 +00:00
|
|
|
type configFile struct {
|
|
|
|
RootPath string `yaml:"root" valid:"required"`
|
|
|
|
Hostname string `yaml:"hostname"`
|
|
|
|
Destination string `yaml:"dest" valid:"required"`
|
|
|
|
FTPPassword string `yaml:"ftp_password"`
|
|
|
|
AWS struct {
|
|
|
|
AccessKeyID string `yaml:"access_key_id"`
|
|
|
|
SecretAccessKey string `yaml:"secret_access_key"`
|
|
|
|
StorageClass string `yaml:"storage_class"`
|
|
|
|
} `yaml:"aws"`
|
|
|
|
GoogleCloud struct {
|
|
|
|
AccessKeyID string `yaml:"access_key_id"`
|
|
|
|
SecretAccessKey string `yaml:"secret_access_key"`
|
|
|
|
} `yaml:"google_cloud"`
|
|
|
|
Swift struct {
|
|
|
|
Username string `yaml:"username"`
|
|
|
|
Password string `yaml:"password"`
|
|
|
|
AuthURL string `yaml:"auth_url"`
|
|
|
|
AuthVersion int `yaml:"auth_version"`
|
|
|
|
} `yaml:"swift"`
|
|
|
|
Include []string `yaml:"inclist"`
|
|
|
|
Exclude []string `yaml:"exclist"`
|
2016-05-22 14:05:06 +00:00
|
|
|
IncExcFile string `yaml:"incexcfile" valid:"customFileExistsValidator,required"`
|
2016-05-22 13:04:37 +00:00
|
|
|
ExcludeDeviceFiles bool `yaml:"excdevicefiles"`
|
|
|
|
Encryption struct {
|
|
|
|
Enable bool `yaml:"enable"`
|
|
|
|
Passphrase string `yaml:"passphrase"`
|
|
|
|
GPGEncryptionKey string `yaml:"gpg_encryption_key"`
|
|
|
|
GPGSignKey string `yaml:"gpg_sign_key"`
|
|
|
|
HideKeyID bool `yaml:"hide_key_id"`
|
|
|
|
SecretKeyRing string `yaml:"secret_keyring"`
|
|
|
|
} `yaml:"encryption"`
|
|
|
|
StaticBackupOptions []string `yaml:"static_options"`
|
|
|
|
Cleanup struct {
|
|
|
|
Type string `yaml:"type"`
|
|
|
|
Value string `yaml:"value"`
|
|
|
|
} `yaml:"cleanup"`
|
|
|
|
LogDirectory string `yaml:"logdir" valid:"required"`
|
|
|
|
Notifications struct {
|
|
|
|
Slack struct {
|
|
|
|
HookURL string `yaml:"hook_url"`
|
|
|
|
Channel string `yaml:"channel"`
|
|
|
|
Username string `yaml:"username"`
|
|
|
|
Emoji string `yaml:"emoji"`
|
|
|
|
} `yaml:"slack"`
|
|
|
|
MonDash struct {
|
2016-05-23 08:41:20 +00:00
|
|
|
BoardURL string `yaml:"board"`
|
|
|
|
Token string `yaml:"token"`
|
|
|
|
Freshness int64 `yaml:"freshness"`
|
2016-05-22 13:04:37 +00:00
|
|
|
} `yaml:"mondash"`
|
|
|
|
} `yaml:"notifications"`
|
|
|
|
}
|
|
|
|
|
2016-05-22 14:05:06 +00:00
|
|
|
func init() {
|
|
|
|
valid.CustomTypeTagMap.Set("customFileExistsValidator", valid.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
|
|
|
switch v := i.(type) { // this validates a field against the value in another field, i.e. dependent validation
|
|
|
|
case string:
|
|
|
|
_, err := os.Stat(v)
|
|
|
|
return v == "" || err == nil
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2016-05-22 13:04:37 +00:00
|
|
|
func (c *configFile) validate() error {
|
|
|
|
result, err := valid.ValidateStruct(c)
|
|
|
|
if !result || err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Encryption.Enable && c.Encryption.GPGSignKey != "" && c.Encryption.Passphrase == "" {
|
|
|
|
return errors.New("With gpg_sign_key passphrase is required")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Encryption.Enable && c.Encryption.GPGEncryptionKey == "" && c.Encryption.Passphrase == "" {
|
|
|
|
return errors.New("Encryption is enabled but no encryption key or passphrase is specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Destination[0:2] == "s3" && (c.AWS.AccessKeyID == "" || c.AWS.SecretAccessKey == "") {
|
|
|
|
return errors.New("Destination is S3 but AWS credentials are not configured")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Destination[0:2] == "gs" && (c.GoogleCloud.AccessKeyID == "" || c.GoogleCloud.SecretAccessKey == "") {
|
|
|
|
return errors.New("Destination is S3 but AWS credentials are not configured")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadConfigFile(in io.Reader) (*configFile, error) {
|
|
|
|
fileContent, err := ioutil.ReadAll(in)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-05-22 14:20:36 +00:00
|
|
|
hostname, _ := os.Hostname()
|
|
|
|
|
|
|
|
res := &configFile{
|
|
|
|
Hostname: hostname,
|
|
|
|
}
|
2016-05-22 13:04:37 +00:00
|
|
|
if err := yaml.Unmarshal(fileContent, res); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, res.validate()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) GenerateCommand(argv []string, time string) (commandLine []string, env []string, err error) {
|
|
|
|
var (
|
2016-05-23 19:26:20 +00:00
|
|
|
tmpEnv []string
|
2016-05-22 13:04:37 +00:00
|
|
|
option, root, dest string
|
|
|
|
addTime bool
|
|
|
|
command = argv[0]
|
|
|
|
)
|
|
|
|
|
|
|
|
switch command {
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandBackup:
|
2016-05-22 13:04:37 +00:00
|
|
|
option = ""
|
|
|
|
root = c.RootPath
|
|
|
|
dest = c.Destination
|
|
|
|
commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "")
|
2016-05-23 19:11:02 +00:00
|
|
|
case commandFullBackup:
|
|
|
|
option = command
|
|
|
|
root = c.RootPath
|
|
|
|
dest = c.Destination
|
|
|
|
commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "")
|
|
|
|
case commandIncrBackup:
|
|
|
|
option = command
|
|
|
|
root = c.RootPath
|
|
|
|
dest = c.Destination
|
|
|
|
commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "")
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandCleanup:
|
2016-05-22 14:05:06 +00:00
|
|
|
option = command
|
2016-05-22 13:04:37 +00:00
|
|
|
commandLine, env, err = c.generateLiteCommand(option, time, addTime)
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandList:
|
2016-05-22 14:05:06 +00:00
|
|
|
option = command
|
2016-05-22 13:04:37 +00:00
|
|
|
commandLine, env, err = c.generateLiteCommand(option, time, addTime)
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandRestore:
|
2016-05-22 13:04:37 +00:00
|
|
|
addTime = true
|
2016-05-22 14:05:06 +00:00
|
|
|
option = command
|
2016-05-22 13:04:37 +00:00
|
|
|
root = c.Destination
|
|
|
|
restoreFile := ""
|
|
|
|
|
|
|
|
if len(argv) == 3 {
|
|
|
|
restoreFile = argv[1]
|
|
|
|
dest = argv[2]
|
|
|
|
} else if len(argv) == 2 {
|
|
|
|
dest = argv[1]
|
|
|
|
} else {
|
|
|
|
err = errors.New("You need to specify one ore more parameters. See help message.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, restoreFile)
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandStatus:
|
2016-05-22 13:04:37 +00:00
|
|
|
option = "collection-status"
|
|
|
|
commandLine, env, err = c.generateLiteCommand(option, time, addTime)
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandVerify:
|
2016-05-22 14:05:06 +00:00
|
|
|
option = command
|
2016-05-22 13:04:37 +00:00
|
|
|
root = c.Destination
|
|
|
|
dest = c.RootPath
|
|
|
|
commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "")
|
2016-05-23 09:54:45 +00:00
|
|
|
case commandRemove:
|
2016-05-22 13:04:37 +00:00
|
|
|
commandLine, env, err = c.generateRemoveCommand()
|
|
|
|
default:
|
|
|
|
err = fmt.Errorf("Did not understand command '%s', please see 'help' for details what to do.", command)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add destination credentials
|
|
|
|
tmpEnv = c.generateCredentialExport()
|
|
|
|
env = append(env, tmpEnv...)
|
|
|
|
|
2016-05-23 19:26:20 +00:00
|
|
|
commandLine = c.cleanSlice(commandLine)
|
|
|
|
env = c.cleanSlice(env)
|
2016-05-22 13:04:37 +00:00
|
|
|
|
2016-05-23 19:26:20 +00:00
|
|
|
return
|
|
|
|
}
|
2016-05-22 13:04:37 +00:00
|
|
|
|
2016-05-23 19:26:20 +00:00
|
|
|
func (c *configFile) cleanSlice(in []string) (out []string) {
|
|
|
|
for _, i := range in {
|
2016-05-22 13:04:37 +00:00
|
|
|
if i != "" {
|
2016-05-23 19:26:20 +00:00
|
|
|
out = append(out, i)
|
2016-05-22 13:04:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) generateCredentialExport() (env []string) {
|
|
|
|
if c.AWS.AccessKeyID != "" {
|
|
|
|
env = append(env, "AWS_ACCESS_KEY_ID="+c.AWS.AccessKeyID)
|
|
|
|
env = append(env, "AWS_SECRET_ACCESS_KEY="+c.AWS.SecretAccessKey)
|
|
|
|
}
|
|
|
|
if c.GoogleCloud.AccessKeyID != "" {
|
|
|
|
env = append(env, "GS_ACCESS_KEY_ID="+c.GoogleCloud.AccessKeyID)
|
|
|
|
env = append(env, "GS_SECRET_ACCESS_KEY="+c.GoogleCloud.SecretAccessKey)
|
|
|
|
}
|
|
|
|
if c.Swift.Username != "" {
|
|
|
|
env = append(env, "SWIFT_USERNAME="+c.Swift.Username)
|
|
|
|
env = append(env, "SWIFT_PASSWORD="+c.Swift.Password)
|
|
|
|
env = append(env, "SWIFT_AUTHURL="+c.Swift.AuthURL)
|
2016-05-22 14:11:57 +00:00
|
|
|
env = append(env, "SWIFT_AUTHVERSION="+strconv.Itoa(c.Swift.AuthVersion))
|
2016-05-22 13:04:37 +00:00
|
|
|
}
|
|
|
|
if c.FTPPassword != "" {
|
|
|
|
env = append(env, "FTP_PASSWORD="+c.FTPPassword)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) generateRemoveCommand() (commandLine []string, env []string, err error) {
|
|
|
|
var tmpArg, tmpEnv []string
|
|
|
|
// Assemble command
|
|
|
|
commandLine = append(commandLine, c.Cleanup.Type, c.Cleanup.Value)
|
|
|
|
// Static Options
|
|
|
|
commandLine = append(commandLine, c.StaticBackupOptions...)
|
|
|
|
// Encryption options
|
|
|
|
tmpArg, tmpEnv = c.generateEncryption(c.Cleanup.Type)
|
|
|
|
commandLine = append(commandLine, tmpArg...)
|
|
|
|
env = append(env, tmpEnv...)
|
|
|
|
// Enforce cleanup
|
|
|
|
commandLine = append(commandLine, "--force")
|
|
|
|
// Remote repo
|
|
|
|
commandLine = append(commandLine, c.Destination)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) generateLiteCommand(option, time string, addTime bool) (commandLine []string, env []string, err error) {
|
|
|
|
var tmpArg, tmpEnv []string
|
|
|
|
// Assemble command
|
|
|
|
commandLine = append(commandLine, option)
|
|
|
|
// Static Options
|
|
|
|
commandLine = append(commandLine, c.StaticBackupOptions...)
|
|
|
|
if addTime && time != "" {
|
|
|
|
commandLine = append(commandLine, "--time", time)
|
|
|
|
}
|
|
|
|
// Encryption options
|
|
|
|
tmpArg, tmpEnv = c.generateEncryption(option)
|
|
|
|
commandLine = append(commandLine, tmpArg...)
|
|
|
|
env = append(env, tmpEnv...)
|
|
|
|
// Remote repo
|
|
|
|
commandLine = append(commandLine, c.Destination)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) generateFullCommand(option, time, root, dest string, addTime bool, restoreFile string) (commandLine []string, env []string, err error) {
|
|
|
|
var tmpArg, tmpEnv []string
|
|
|
|
// Assemble command
|
|
|
|
commandLine = append(commandLine, option)
|
|
|
|
// Static Options
|
|
|
|
commandLine = append(commandLine, c.StaticBackupOptions...)
|
|
|
|
if addTime && time != "" {
|
|
|
|
commandLine = append(commandLine, "--time", time)
|
|
|
|
}
|
|
|
|
if restoreFile != "" {
|
|
|
|
commandLine = append(commandLine, "--file-to-restore", restoreFile)
|
|
|
|
}
|
|
|
|
// AWS Storage Class (empty if not used, will get stripped)
|
|
|
|
commandLine = append(commandLine, c.AWS.StorageClass)
|
|
|
|
// Encryption options
|
|
|
|
tmpArg, tmpEnv = c.generateEncryption(option)
|
|
|
|
commandLine = append(commandLine, tmpArg...)
|
|
|
|
env = append(env, tmpEnv...)
|
|
|
|
// Includes / Excludes
|
|
|
|
tmpArg, tmpEnv = c.generateIncludeExclude()
|
|
|
|
commandLine = append(commandLine, tmpArg...)
|
|
|
|
env = append(env, tmpEnv...)
|
|
|
|
// Source / Destination
|
|
|
|
commandLine = append(commandLine, root, dest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) generateIncludeExclude() (arguments []string, env []string) {
|
|
|
|
if c.ExcludeDeviceFiles {
|
|
|
|
arguments = append(arguments, "--exclude-device-files")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, exc := range c.Exclude {
|
|
|
|
arguments = append(arguments, "--exclude="+exc)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, inc := range c.Include {
|
|
|
|
arguments = append(arguments, "--include="+inc)
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.IncExcFile != "" {
|
|
|
|
arguments = append(arguments, "--include-globbing-filelist", c.IncExcFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(c.Include) > 0 || c.IncExcFile != "" {
|
|
|
|
arguments = append(arguments, "--exclude=**")
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFile) generateEncryption(command string) (arguments []string, env []string) {
|
|
|
|
if !c.Encryption.Enable {
|
|
|
|
arguments = append(arguments, "--no-encryption")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Encryption.Passphrase != "" {
|
|
|
|
env = append(env, "PASSPHRASE="+c.Encryption.Passphrase)
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Encryption.GPGEncryptionKey != "" {
|
|
|
|
if c.Encryption.HideKeyID {
|
|
|
|
arguments = append(arguments, "--hidden-encrypt-key="+c.Encryption.GPGEncryptionKey)
|
|
|
|
} else {
|
|
|
|
arguments = append(arguments, "--encrypt-key="+c.Encryption.GPGEncryptionKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Encryption.GPGSignKey != "" && command != "restore" {
|
|
|
|
arguments = append(arguments, "--sign-key="+c.Encryption.GPGSignKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Encryption.GPGEncryptionKey != "" && c.Encryption.SecretKeyRing != "" {
|
|
|
|
arguments = append(arguments, "--encrypt-secret-keyring="+c.Encryption.SecretKeyRing)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|