From d2b85d8b4359c61da5495381ed3ba763a9d9b033 Mon Sep 17 00:00:00 2001 From: Bekir Pehlivan Date: Wed, 20 Mar 2024 12:30:43 +0300 Subject: [PATCH] reorganized sftp and added rotation to it --- backup/backup.go | 68 ++++++++++++++++++---------------- backup/mysql.go | 7 +++- backup/sftp.go | 79 +++++++++++++++++++++++++++++++++------- config/config.sample.yml | 13 +++++-- config/params.go | 20 ++++++---- go.mod | 2 + go.sum | 4 ++ notify/email.go | 43 +++++++++++++++++----- notify/webhook.go | 21 +++++------ 9 files changed, 179 insertions(+), 78 deletions(-) diff --git a/backup/backup.go b/backup/backup.go index 3670d8d..52c67d6 100644 --- a/backup/backup.go +++ b/backup/backup.go @@ -77,37 +77,44 @@ func Backup() { filePath, fileName, err := dumpDB(db, dst) if err != nil { notify.SendAlarm("Problem during backing up "+db+" - Error: "+err.Error(), true) + err = os.Remove(filePath) + if err != nil { + logger.Error("Couldn't delete faulty dump file at " + filePath + " - Error: " + err.Error()) + } else { + logger.Info("Faulty dump file at " + filePath + " successfully deleted.") + } } else { logger.Info("Successfully backed up database:" + db + " at " + filePath) notify.SendAlarm("Successfully backed up "+db+" at "+filePath, false) - } - if params.Rotation.Enabled { - shouldRotate, name := rotate(db) - if shouldRotate { - var newDst string - if params.Minio.Path != "" { - newDst = params.Minio.Path - } - newDst = params.Minio.S3FS.MountPath + "/" + newDst - newDst = strings.TrimSuffix(newDst, "/") - err := os.MkdirAll(strings.TrimSuffix(newDst, "/")+"/"+rotatePath(), os.FileMode(0750)) - if err != nil { - notify.SendAlarm("Couldn't create folder in MinIO at path: "+dst+" - Error: "+err.Error(), true) - logger.Fatal("Couldn't create folder in MinIO at path: " + dst + " - Error: " + err.Error()) - return - } - extension := strings.Split(fileName, ".") - for i := 1; i < len(extension); i++ { - name = name + "." + extension[i] - } - name = newDst + "/" + name - _, err = copyFile(filePath, name) - if err != nil { - logger.Error("Couldn't create a copy of " + filePath + " for rotation\npath: " + name + "\n Error: " + err.Error()) - notify.SendAlarm("Couldn't create a copy of "+filePath+" for rotation\npath: "+name+"\n Error: "+err.Error(), true) - } else { - logger.Info("Successfully created a copy of " + filePath + " for rotation\npath: " + name) - notify.SendAlarm("Successfully created a copy of "+filePath+" for rotation\npath: "+name, false) + + if params.Rotation.Enabled { + shouldRotate, name := rotate(db) + if shouldRotate { + var newDst string + if params.Minio.Path != "" { + newDst = params.Minio.Path + } + newDst = params.Minio.S3FS.MountPath + "/" + newDst + newDst = strings.TrimSuffix(newDst, "/") + err := os.MkdirAll(strings.TrimSuffix(newDst, "/")+"/"+rotatePath(), os.FileMode(0750)) + if err != nil { + notify.SendAlarm("Couldn't create folder in MinIO at path: "+dst+" - Error: "+err.Error(), true) + logger.Fatal("Couldn't create folder in MinIO at path: " + dst + " - Error: " + err.Error()) + return + } + extension := strings.Split(fileName, ".") + for i := 1; i < len(extension); i++ { + name = name + "." + extension[i] + } + name = newDst + "/" + name + _, err = copyFile(filePath, name) + if err != nil { + logger.Error("Couldn't create a copy of " + filePath + " for rotation\npath: " + name + "\n Error: " + err.Error()) + notify.SendAlarm("Couldn't create a copy of "+filePath+" for rotation\npath: "+name+"\n Error: "+err.Error(), true) + } else { + logger.Info("Successfully created a copy of " + filePath + " for rotation\npath: " + name) + notify.SendAlarm("Successfully created a copy of "+filePath+" for rotation\npath: "+name, false) + } } } } @@ -139,9 +146,8 @@ func Backup() { } if params.SFTP.Enabled { - err = SendSFTP(filePath, "/root/"+name, params.SFTP.User, params.SFTP.Target, params.SFTP.Port) - if err != nil { - logger.Error("Couldn't upload " + name + " at " + filePath + " to target with sftp" + " - Error: " + err.Error()) + for _, target := range params.SFTP.Targets { + SendSFTP(filePath, name, db, target) } } } diff --git a/backup/mysql.go b/backup/mysql.go index 527b778..072eef6 100644 --- a/backup/mysql.go +++ b/backup/mysql.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "strconv" + "strings" ) func getMySQLList() []string { @@ -142,8 +143,10 @@ func dumpMySQLDb(db, dst string) (string, string, error) { } n, _ := stderr2.Read(output) if n > 0 { - logger.Error("Couldn't back up " + db + " - Error: " + string(string(output[:n]))) - return dumpPath, name, errors.New(string(output[:n])) + if !strings.Contains(string(string(output[:n])), "[Warning] Using a password on the command line interface can be insecure.") { + logger.Error("Couldn't back up " + db + " - Error: " + string(string(output[:n]))) + return dumpPath, name, errors.New(string(output[:n])) + } } return dumpPath, name, nil } diff --git a/backup/sftp.go b/backup/sftp.go index cb888ec..cfd7273 100644 --- a/backup/sftp.go +++ b/backup/sftp.go @@ -1,77 +1,128 @@ package backup import ( + "monodb-backup/config" + "monodb-backup/notify" "net" "os" + "strings" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) -func SendSFTP(srcPath, dstPath, user, target, port string) error { +func SendSFTP(srcPath, dstPath, db string, target config.Target) { + dstPath = target.Path + "/" + nameWithPath(dstPath) + logger.Info("SFTP transfer started.\n Source: " + srcPath + " - Destination: " + target.Host + ":" + dstPath) sock, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) if err != nil { - return err + logger.Error("Couldn't get environment variable SSH_AUTH_SOCK - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't get environment variable SSH_AUTH_SOCK - Error: "+err.Error(), true) + return } sockAgent := agent.NewClient(sock) signers, err := sockAgent.Signers() if err != nil { - return err + logger.Error("Couldn't get signers for ssh keys - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't get signers for ssh keys - Error: "+err.Error(), true) + return } auths := []ssh.AuthMethod{ssh.PublicKeys(signers...)} config := &ssh.ClientConfig{ - User: user, + User: target.User, Auth: auths, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } - client, _ := ssh.Dial("tcp", target+":"+port, config) + client, _ := ssh.Dial("tcp", target.Host+":"+target.Port, config) defer func() { err = client.Close() if err != nil { - // TODO + logger.Error("Couldn't close SSH client - Error: " + err.Error()) + notify.SendAlarm("Couldn't close SSH client - Error: "+err.Error(), true) } }() sftpCli, err := sftp.NewClient(client) if err != nil { - return err + logger.Error("Couldn't create an SFTP client - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't create an SFTP client - Error: "+err.Error(), true) + return } defer func() { err = sftpCli.Close() if err != nil { - // TODO + logger.Error("Couldn't close SFTP client - Error: " + err.Error()) + notify.SendAlarm("Couldn't close SFTP client - Error: "+err.Error(), true) } }() src, err := os.Open(srcPath) if err != nil { - return err + logger.Error("Couldn't open source file " + srcPath + " for copying - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't open source file "+srcPath+" for copying - Error: "+err.Error(), true) + return } defer func() { err = src.Close() if err != nil { - // TODO + logger.Error("Couldn't close source file: " + srcPath + " - Error: " + err.Error()) + notify.SendAlarm("Couldn't close source file: "+srcPath+" - Error: "+err.Error(), true) } }() + sendOverSFTp(srcPath, dstPath, src, target, sftpCli) + + if params.Rotation.Enabled { + shouldRotate, newDst := rotate(db) + if shouldRotate { + extension := strings.Split(dstPath, ".") + for i := 1; i < len(extension); i++ { + newDst = newDst + "." + extension[i] + } + newDst = target.Path + "/" + newDst + sendOverSFTp(srcPath, newDst, src, target, sftpCli) + } + } + +} + +func sendOverSFTp(srcPath, dstPath string, src *os.File, target config.Target, sftpCli *sftp.Client) { + fullPath := strings.Split(dstPath, "/") + newPath := "/" + for i := 0; i < len(fullPath)-1; i++ { + newPath = newPath + "/" + fullPath[i] + } + err := sftpCli.MkdirAll(newPath) + if err != nil { + logger.Error("Couldn't create folders " + newPath + " - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't create folders "+newPath+" - Error: "+err.Error(), true) + return + } dst, err := sftpCli.Create(dstPath) if err != nil { - return err + logger.Error("Couldn't create file " + dstPath + " - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't create file "+dstPath+" - Error: "+err.Error(), true) + return } defer func() { err = dst.Close() if err != nil { - // TODO + logger.Error("Couldn't close destination file: " + dstPath + " - Error: " + err.Error()) + notify.SendAlarm("Couldn't close destination file: "+dstPath+" - Error: "+err.Error(), true) } }() + logger.Info("Created destination file " + dstPath + " Now starting copying") if _, err := dst.ReadFrom(src); err != nil { - return err + logger.Error("Couldn't read from file " + srcPath + " to write at " + dstPath + " - Error: " + err.Error()) + notify.SendAlarm("Couldn't upload backup "+srcPath+" to "+target.Host+":"+dstPath+"\nCouldn't read from file "+srcPath+" to write at "+dstPath+" - Error: "+err.Error(), true) + return } - return nil + logger.Info("Successfully copied " + srcPath + " to " + target.Host + ":" + dstPath) + notify.SendAlarm("Successfully copied "+srcPath+" to "+target.Host+":"+dstPath, false) } diff --git a/config/config.sample.yml b/config/config.sample.yml index 4edd11f..eb5a5b8 100755 --- a/config/config.sample.yml +++ b/config/config.sample.yml @@ -53,13 +53,20 @@ minio: keepPasswdFile: true sftp: enabled: false - user: username - target: ssh.example.com - port: 22 + targets: + - user: username + host: ssh.example.com + path: /var/backups + port: 22 + - user: username2 + host: ssh.example2.com + path: /var/backups + port: 22 notify: email: enabled: false onlyOnError: false + insecureSkipVerify: false info: smtpHost: smtp.gmail.com smtpPort: 587 diff --git a/config/params.go b/config/params.go index b38fff3..83ccf40 100644 --- a/config/params.go +++ b/config/params.go @@ -20,10 +20,11 @@ type Params struct { Cluster Cluster Notify struct { Email struct { - Enabled bool - OnlyOnError bool - Info EmailConfig - Error EmailConfig + Enabled bool + OnlyOnError bool + InsecureSkipVerify bool + Info EmailConfig + Error EmailConfig } Webhook Webhook } @@ -39,9 +40,7 @@ type Params struct { Minio MinIO SFTP struct { Enabled bool - User string - Target string - Port string + Targets []Target } Log LoggerParams Fqdn string @@ -59,6 +58,13 @@ type MinIO struct { S3FS S3FS } +type Target struct { + User string + Host string + Port string + Path string +} + type S3FS struct { ShouldMount bool MountPath string diff --git a/go.mod b/go.mod index 7759e36..68a8666 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d github.com/spf13/viper v1.18.2 golang.org/x/crypto v0.21.0 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) require ( @@ -43,6 +44,7 @@ require ( golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index adf8c32..9af57b8 100644 --- a/go.sum +++ b/go.sum @@ -135,9 +135,13 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= diff --git a/notify/email.go b/notify/email.go index d77faf9..ba35cf7 100644 --- a/notify/email.go +++ b/notify/email.go @@ -1,8 +1,12 @@ package notify import ( + "crypto/tls" "monodb-backup/config" - "net/smtp" + "strconv" + "strings" + + "gopkg.in/gomail.v2" ) var emailStruct = &config.Parameters.Notify.Email @@ -16,7 +20,8 @@ func Email(subject string, message string, isError bool) error { return nil } - var smtpHost, smtpPort, from, username, password, to string + var smtpHost, smtpPort, from, username, password string + var to []string if isError { smtpHost = emailStruct.Error.SmtpHost @@ -24,22 +29,40 @@ func Email(subject string, message string, isError bool) error { from = emailStruct.Error.From username = emailStruct.Error.Username password = emailStruct.Error.Password - to = emailStruct.Error.To + to = strings.Split(emailStruct.Error.To, ",") } else { smtpHost = emailStruct.Info.SmtpHost smtpPort = emailStruct.Info.SmtpPort from = emailStruct.Info.From username = emailStruct.Info.Username password = emailStruct.Info.Password - to = emailStruct.Info.To + to = strings.Split(emailStruct.Info.To, ",") + } + port, _ := strconv.Atoi(smtpPort) + + d := gomail.NewDialer(smtpHost, port, username, password) + if emailStruct.InsecureSkipVerify { + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} } - auth := smtp.CRAMMD5Auth(username, password) + m := gomail.NewMessage() + m.SetHeader("From", from) + m.SetHeader("To", to...) + m.SetHeader("Subject", subject) + m.SetBody("text/html", message) + + return d.DialAndSend(m) - msg := []byte("From: " + from + "\r\n" + - "To: " + to + "\r\n" + - "Subject: [" + config.Parameters.Fqdn + "] " + subject + "\r\n\r\n" + - message + "\r\n") + // var auth smtp.Auth + // if username != "" || password != "" { + // auth = smtp.CRAMMD5Auth(username, password) + // } else { + // auth = smtp.PlainAuth("", username, password, smtpHost) + // } + // msg := []byte("From: " + from + "\r\n" + + // "To: " + to + "\r\n" + + // "Subject: [" + config.Parameters.Fqdn + "] " + subject + "\r\n\r\n" + + // message + "\r\n") - return smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{to}, msg) + // return smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{to}, msg) } diff --git a/notify/webhook.go b/notify/webhook.go index 79765f9..bed6c38 100644 --- a/notify/webhook.go +++ b/notify/webhook.go @@ -10,19 +10,18 @@ import ( var webhookStruct *config.Webhook = &config.Parameters.Notify.Webhook var logger *clog.CustomLogger = &clog.Logger -var db string = func() (db string) { - switch config.Parameters.Database { - case "postgresql": - db = "PostgreSQL" - case "mysql": - db = "MySQL" - default: - db = "PostgreSQL" - } - return db -}() func SendAlarm(message string, isError bool) { + var db string = func() string { + switch config.Parameters.Database { + case "postgresql": + return "PostgreSQL" + case "mysql": + return "MySQL" + default: + return "PostgreSQL" + } + }() var subject string if isError { subject = "Error"