From 6976bf759745af22d6baf30d1f7659dff973bbc6 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Tue, 30 Jul 2024 19:18:34 +0200 Subject: [PATCH] Add SSH remote backup --- docker/Dockerfile | 5 ++- go.mod | 2 + go.sum | 4 ++ pkg/backup.go | 31 +++++++++++++- pkg/scp.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 pkg/scp.go diff --git a/docker/Dockerfile b/docker/Dockerfile index a1ccbe9..4e3f512 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,12 +24,13 @@ ENV AWS_REGION="us-west-2" ENV AWS_DISABLE_SSL="false" ENV GPG_PASSPHRASE="" ENV SSH_USER="" +ENV SSH_REMOTE_PATH="" ENV SSH_PASSWORD="" ENV SSH_HOST_NAME="" -ENV SSH_IDENTIFY_FILE="/root/.ssh/id_rsa" +ENV SSH_IDENTIFY_FILE="" ENV SSH_PORT="22" ARG DEBIAN_FRONTEND=noninteractive -ENV VERSION="v0.8" +ENV VERSION="v1.0" ARG WORKDIR="/app" ARG BACKUPDIR="/backup" ARG BACKUP_TMP_DIR="/tmp/backup" diff --git a/go.mod b/go.mod index 83d89a2..f59aa94 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,10 @@ require ( require ( github.com/aws/aws-sdk-go v1.55.3 // indirect + github.com/bramvdbogaerde/go-scp v1.5.0 // indirect github.com/hpcloud/tail v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/sys v0.22.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/go.sum b/go.sum index 2d2cfef..7512d7f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/aws/aws-sdk-go v1.55.3 h1:0B5hOX+mIx7I5XPOrjrHlKSDQV/+ypFZpIHOx5LOk3E= github.com/aws/aws-sdk-go v1.55.3/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= +github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -35,6 +37,8 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= diff --git a/pkg/backup.go b/pkg/backup.go index a2e6cc2..7d78cb0 100644 --- a/pkg/backup.go +++ b/pkg/backup.go @@ -54,7 +54,7 @@ func StartBackup(cmd *cobra.Command) { case "ssh": sshBackup(backupFileName, s3Path, disableCompression, prune, backupRetention, encryption) case "ftp": - fmt.Println("x is 3") + utils.Fatalf("Not supported storage type: %s", storage) default: localBackup(backupFileName, disableCompression, prune, backupRetention, encryption) } @@ -241,8 +241,35 @@ func s3Backup(backupFileName string, s3Path string, disableCompression bool, pru } utils.Done("Database has been backed up and uploaded to s3 ") } -func sshBackup(backupFileName string, s3Path string, disableCompression bool, prune bool, backupRetention int, encrypt bool) { +func sshBackup(backupFileName string, remotePath string, disableCompression bool, prune bool, backupRetention int, encrypt bool) { + utils.Info("Backup database to Remote server") + //Backup database + BackupDatabase(backupFileName, disableCompression) + finalFileName := backupFileName + if encrypt { + encryptBackup(backupFileName) + finalFileName = fmt.Sprintf("%s.%s", backupFileName, "gpg") + } + utils.Info("Uploading backup file to S3 storage...") + utils.Info("Backup name is ", backupFileName) + err := CopyToRemote(filepath.Join(tmpPath, finalFileName), remotePath) + if err != nil { + utils.Fatalf("Error uploading file to S3: %s ", err) + } + //Delete backup file from tmp folder + err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName)) + if err != nil { + fmt.Println("Error deleting file:", err) + + } + if prune { + //TODO: Delete old backup from remote server + utils.Info("Deleting old backup from a remote server is not implemented yet") + + } + + utils.Done("Database has been backed up and uploaded to remote server ") } func encryptBackup(backupFileName string) { diff --git a/pkg/scp.go b/pkg/scp.go new file mode 100644 index 0000000..6012f09 --- /dev/null +++ b/pkg/scp.go @@ -0,0 +1,100 @@ +package pkg + +import ( + "context" + "errors" + "fmt" + "github.com/bramvdbogaerde/go-scp" + "github.com/bramvdbogaerde/go-scp/auth" + "github.com/jkaninda/pg-bkup/utils" + "golang.org/x/crypto/ssh" + "os" +) + +func CopyToRemote(fileName, remotePath string) error { + sshUser := os.Getenv("SSH_USER") + sshPassword := os.Getenv("SSH_PASSWORD") + sshHostName := os.Getenv("SSH_HOST_NAME") + sshPort := os.Getenv("SSH_PORT") + sshIdentifyFile := os.Getenv("SSH_IDENTIFY_FILE") + + clientConfig, _ := auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey()) + if sshIdentifyFile != "" && utils.FileExists(sshIdentifyFile) { + clientConfig, _ = auth.PrivateKey(sshUser, sshIdentifyFile, ssh.InsecureIgnoreHostKey()) + + } else { + if sshPassword == "" { + return errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty\n") + } + clientConfig, _ = auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey()) + + } + // Create a new SCP client + client := scp.NewClient(fmt.Sprintf("%s:%s", sshHostName, sshPort), &clientConfig) + + // Connect to the remote server + err := client.Connect() + if err != nil { + return errors.New("Couldn't establish a connection to the remote server\n") + } + + // Open a file + file, _ := os.Open(fileName) + + // Close client connection after the file has been copied + defer client.Close() + // Close the file after it has been copied + defer file.Close() + // the context can be adjusted to provide time-outs or inherit from other contexts if this is embedded in a larger application. + err = client.CopyFromFile(context.Background(), *file, remotePath, "0655") + if err != nil { + fmt.Println("Error while copying file ") + return err + } + return nil +} + +func CopyFromRemote(fileName, remotePath string) error { + sshUser := os.Getenv("SSH_USER") + sshPassword := os.Getenv("SSH_PASSWORD") + sshHostName := os.Getenv("SSH_HOST_NAME") + sshPort := os.Getenv("SSH_PORT") + sshIdentifyFile := os.Getenv("SSH_IDENTIFY_FILE") + + clientConfig, _ := auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey()) + if sshIdentifyFile != "" && utils.FileExists(sshIdentifyFile) { + clientConfig, _ = auth.PrivateKey(sshUser, sshIdentifyFile, ssh.InsecureIgnoreHostKey()) + + } else { + if sshPassword == "" { + return errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty\n") + } + clientConfig, _ = auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey()) + + } + + // Create a new SCP client + client := scp.NewClient(fmt.Sprintf("%s:%s", sshHostName, sshPort), &clientConfig) + // Connect to the remote server + err := client.Connect() + if err != nil { + return errors.New("Couldn't establish a connection to the remote server\n") + } + // Close client connection after the file has been copied + defer client.Close() + file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + fmt.Println("Couldn't open the output file") + } + defer file.Close() + + // the context can be adjusted to provide time-outs or inherit from other contexts if this is embedded in a larger application. + err = client.CopyFromRemote(context.Background(), file, remotePath) + + if err != nil { + fmt.Println("Error while copying file ", err) + return err + } + return nil + +}