package main import ( "errors" "fmt" "io" "io/ioutil" "os" "strconv" valid "github.com/asaskevich/govalidator" "gopkg.in/yaml.v2" ) 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"` IncExcFile string `yaml:"incexcfile"` 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 { Board string `yaml:"board"` Token string `yaml:"token"` Metric string `yaml:"metric"` Instance string `yaml:"instance"` } `yaml:"mondash"` } `yaml:"notifications"` } 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") } if _, err := os.Stat(c.IncExcFile); c.IncExcFile != "" && err == os.ErrNotExist { return errors.New("Specified incexcfile does not exist") } return nil } func loadConfigFile(in io.Reader) (*configFile, error) { fileContent, err := ioutil.ReadAll(in) if err != nil { return nil, err } res := &configFile{} 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 ( tmpArg, tmpEnv []string option, root, dest string addTime bool command = argv[0] ) switch command { case "backup": option = "" root = c.RootPath dest = c.Destination commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") case "cleanup": option = "cleanup" commandLine, env, err = c.generateLiteCommand(option, time, addTime) case "list-current-files": option = "list-current-files" commandLine, env, err = c.generateLiteCommand(option, time, addTime) case "restore": addTime = true option = "restore" 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) case "status": option = "collection-status" commandLine, env, err = c.generateLiteCommand(option, time, addTime) case "verify": option = "verify" root = c.Destination dest = c.RootPath commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") case "__remove_old": 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...) // Clean empty entries from the list tmpArg = []string{} tmpEnv = []string{} for _, i := range commandLine { if i != "" { tmpArg = append(tmpArg, i) } } for _, i := range env { if i != "" { tmpEnv = append(tmpEnv, i) } } commandLine = tmpArg env = tmpEnv 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) env = append(env, "SWIFT_AUTHVERSION="+strconv.FormatInt(int64(c.Swift.AuthVersion), 10)) } 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 }