blob: f01066ebdff7377d2f574575d8bb124e1564b65a [file]
package report_test
import (
"fmt"
"io"
"os"
"strings"
"testing"
"time"
"github.com/apache/cloudberry-backup/backup"
"github.com/apache/cloudberry-backup/filepath"
"github.com/apache/cloudberry-backup/history"
"github.com/apache/cloudberry-backup/options"
"github.com/apache/cloudberry-backup/testutils"
"github.com/apache/cloudberry-backup/utils"
"github.com/apache/cloudberry-go-libs/cluster"
"github.com/apache/cloudberry-go-libs/dbconn"
"github.com/apache/cloudberry-go-libs/gplog"
"github.com/apache/cloudberry-go-libs/operating"
"github.com/apache/cloudberry-go-libs/structmatcher"
"github.com/apache/cloudberry-go-libs/testhelper"
"github.com/blang/semver/v4"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"gopkg.in/yaml.v2"
"github.com/apache/cloudberry-backup/report"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
)
var (
stdout *Buffer
logfile *Buffer
buffer *Buffer
)
func TestReport(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Report Suite")
}
var _ = BeforeEach(func() {
stdout, buffer, logfile = testhelper.SetupTestLogger()
})
var _ = Describe("utils/report tests", func() {
Describe("ParseErrorMessage", func() {
It("Parses a CRITICAL error message and returns error code 1", func() {
errStr := "testProgram:testUser:testHost:000000-[CRITICAL]:-Error Message"
errMsg := report.ParseErrorMessage(errStr)
Expect(errMsg).To(Equal("Error Message"))
})
It("Returns error code 0 for an empty error message", func() {
errMsg := report.ParseErrorMessage("")
Expect(errMsg).To(Equal(""))
})
})
Describe("WriteBackupReportFile", func() {
timestamp := "20170101010101"
endtime := time.Date(2017, 1, 1, 5, 4, 3, 2, time.Local)
config := history.BackupConfig{
BackupVersion: "0.1.0",
DatabaseName: "testdb",
DatabaseVersion: "5.0.0 build test",
SegmentCount: 3,
}
backupReport := &report.Report{}
objectCounts := map[string]int{"tables": 42, "sequences": 1, "types": 1000}
BeforeEach(func() {
backupReport = &report.Report{
BackupParamsString: `compression: gzip
backup section: All Sections
object filtering: None
includes statistics: No
data file format: Single Data File Per Segment`,
DatabaseSize: "42 MB",
BackupConfig: config,
}
operating.System.OpenFileWrite = func(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
return buffer, nil
}
operating.System.Now = func() time.Time {
return time.Date(2017, 1, 1, 5, 4, 3, 2, time.Local)
}
operating.System.Chmod = func(name string, mode os.FileMode) error {
return nil
}
})
It("writes a report for a successful backup", func() {
backupReport.WriteBackupReportFile("filename", timestamp, endtime, objectCounts, "")
Expect(buffer).To(Say(`Apache Cloudberry Backup Report
timestamp key: 20170101010101
database version: 5\.0\.0 build test
gpbackup version: 0\.1\.0
database name: testdb
command line: .*
compression: gzip
backup section: All Sections
object filtering: None
includes statistics: No
data file format: Single Data File Per Segment
start time: Sun Jan 01 2017 01:01:01
end time: Sun Jan 01 2017 05:04:03
duration: 4:03:02
backup status: Success
database size: 42 MB
segment count: 3
count of database objects in backup:
sequences 1
tables 42
types 1000`))
})
It("writes a report for a failed backup", func() {
backupReport.WriteBackupReportFile("filename", timestamp, endtime, objectCounts, "Cannot access /tmp/backups: Permission denied")
Expect(buffer).To(Say(`Apache Cloudberry Backup Report
timestamp key: 20170101010101
database version: 5\.0\.0 build test
gpbackup version: 0\.1\.0
database name: testdb
command line: .*
compression: gzip
backup section: All Sections
object filtering: None
includes statistics: No
data file format: Single Data File Per Segment
start time: Sun Jan 01 2017 01:01:01
end time: Sun Jan 01 2017 05:04:03
duration: 4:03:02
backup status: Failure
backup error: Cannot access /tmp/backups: Permission denied
database size: 42 MB
segment count: 3
count of database objects in backup:
sequences 1
tables 42
types 1000`))
})
It("writes a report without database size information", func() {
backupReport.DatabaseSize = ""
backupReport.WriteBackupReportFile("filename", timestamp, endtime, objectCounts, "")
Expect(buffer).To(Say(`Apache Cloudberry Backup Report
timestamp key: 20170101010101
database version: 5\.0\.0 build test
gpbackup version: 0\.1\.0
database name: testdb
command line: .*
compression: gzip
backup section: All Sections
object filtering: None
includes statistics: No
data file format: Single Data File Per Segment
start time: Sun Jan 01 2017 01:01:01
end time: Sun Jan 01 2017 05:04:03
duration: 4:03:02
backup status: Success
segment count: 3
count of database objects in backup:
sequences 1
tables 42
types 1000`))
})
})
Describe("AppendBackupParams", func() {
It("correctly parses the string and appends to the LineInfo array", func() {
testParamsStr := `compression: exampleStr
data file format: exampleStr
`
lineInfoTest := make([]report.LineInfo, 0)
report.AppendBackupParams(&lineInfoTest, testParamsStr)
Expect(lineInfoTest[0].Key).To(Equal("compression:"))
Expect(lineInfoTest[0].Value).To(Equal("exampleStr"))
Expect(lineInfoTest[1].Key).To(Equal("data file format:"))
Expect(lineInfoTest[1].Value).To(Equal("exampleStr"))
})
It("correctly parses a string with incremental info", func() {
backupTimestamps := []string{"1234", "5678"}
testParamsStr := fmt.Sprintf(`incremental: True
incremental backup set:
%s`, strings.Join(backupTimestamps, "\n"))
lineInfoTest := make([]report.LineInfo, 0)
report.AppendBackupParams(&lineInfoTest, testParamsStr)
Expect(lineInfoTest[0].Key).To(Equal("incremental:"))
Expect(lineInfoTest[0].Value).To(Equal("True"))
Expect(lineInfoTest[1].Key).To(Equal("incremental backup set:"))
Expect(lineInfoTest[1].Value).To(Equal(""))
Expect(lineInfoTest[2].Key).To(Equal("1234"))
Expect(lineInfoTest[2].Value).To(Equal(""))
Expect(lineInfoTest[3].Key).To(Equal("5678"))
Expect(lineInfoTest[3].Value).To(Equal(""))
})
})
Describe("WriteRestoreReportFile", func() {
timestamp := "20170101010101"
restoreStartTime := "20170101010102"
restoreVersion := "0.1.0"
connectionPool := &dbconn.DBConn{
DBName: "testdb",
Version: dbconn.GPDBVersion{
VersionString: "5.0.0 build test",
},
}
BeforeEach(func() {
operating.System.OpenFileWrite = func(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
return buffer, nil
}
operating.System.Now = func() time.Time {
return time.Date(2017, 1, 1, 5, 4, 3, 2, time.Local)
}
operating.System.Chmod = func(name string, mode os.FileMode) error {
return nil
}
gplog.SetErrorCode(0)
})
AfterEach(func() {
gplog.SetErrorCode(0)
})
It("writes a report for a failed restore", func() {
gplog.SetErrorCode(2)
report.WriteRestoreReportFile("filename", timestamp, restoreStartTime, connectionPool, restoreVersion, 3, 4, "Cannot access /tmp/backups: Permission denied")
Expect(buffer).To(Say(`Apache Cloudberry Restore Report
timestamp key: 20170101010101
database version: 5\.0\.0 build test
gprestore version: 0\.1\.0
database name: testdb
command line: .*
backup segment count: 3
restore segment count: 4
start time: Sun Jan 01 2017 01:01:02
end time: Sun Jan 01 2017 05:04:03
duration: 4:03:01
restore status: Failure
restore error: Cannot access /tmp/backups: Permission denied`))
})
It("writes a report for a successful restore", func() {
gplog.SetErrorCode(0)
report.WriteRestoreReportFile("filename", timestamp, restoreStartTime, connectionPool, restoreVersion, 3, 3, "")
Expect(buffer).To(Say(`Apache Cloudberry Restore Report
timestamp key: 20170101010101
database version: 5\.0\.0 build test
gprestore version: 0\.1\.0
database name: testdb
command line: .*
backup segment count: 3
restore segment count: 3
start time: Sun Jan 01 2017 01:01:02
end time: Sun Jan 01 2017 05:04:03
duration: 4:03:01
restore status: Success`))
})
It("writes a report for a successful restore with errors", func() {
gplog.SetErrorCode(1)
report.WriteRestoreReportFile("filename", timestamp, restoreStartTime, connectionPool, restoreVersion, 3, 3, "")
Expect(buffer).To(Say(`Apache Cloudberry Restore Report
timestamp key: 20170101010101
database version: 5\.0\.0 build test
gprestore version: 0\.1\.0
database name: testdb
command line: .*
backup segment count: 3
restore segment count: 3
start time: Sun Jan 01 2017 01:01:02
end time: Sun Jan 01 2017 05:04:03
duration: 4:03:01
restore status: Success but non-fatal errors occurred. See log file .+ for details.`))
})
It("warns if the report file cannot be written", func() {
operating.System.OpenFileWrite = func(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
// Normally no handle would be returned on error, we return buffer here so we can check that it isn't used
return buffer, errors.New("Cannot access /tmp/backup-dir: Permission denied")
}
report.WriteRestoreReportFile("filename", timestamp, restoreStartTime, connectionPool, restoreVersion, 3, 3, "")
Expect(stdout).To(Say("skipping report creation"))
Expect(buffer).ToNot(Say("Apache Cloudberry Restore Report"))
Expect(gplog.GetErrorCode()).To(Equal(0))
})
})
Describe("SetBackupParamFromFlags", func() {
AfterEach(func() {
utils.InitializePipeThroughParameters(false, "", 0)
})
It("configures the Report struct correctly", func() {
utils.InitializePipeThroughParameters(true, "gzip", 0)
backupCmdFlags := pflag.NewFlagSet("gpbackup", pflag.ExitOnError)
backup.SetCmdFlags(backupCmdFlags)
err := backupCmdFlags.Set(options.INCLUDE_RELATION, "public.foobar")
Expect(err).ToNot(HaveOccurred())
opts, err := options.NewOptions(backupCmdFlags)
Expect(err).ToNot(HaveOccurred())
opts.AddIncludedRelation("public.baz")
err = backupCmdFlags.Set(options.INCLUDE_RELATION, "public.baz")
Expect(err).ToNot(HaveOccurred())
backupConfig := backup.NewBackupConfig("testdb",
"5.0.0 build test", "0.1.0",
"/tmp/plugin.sh", "timestamp1", *opts)
structmatcher.ExpectStructsToMatch(history.BackupConfig{
BackupVersion: "0.1.0",
Compressed: true,
CompressionType: "gzip",
DatabaseName: "testdb",
DatabaseVersion: "5.0.0 build test",
IncludeSchemas: []string{},
IncludeRelations: []string{"public.foobar"},
ExcludeSchemas: []string{},
ExcludeRelations: []string{},
Plugin: "/tmp/plugin.sh",
Timestamp: "timestamp1",
IncludeTableFiltered: true,
Status: history.BackupStatusInProgress,
}, backupConfig)
})
})
Describe("GetDurationInfo", func() {
timestamp := "20170101010101"
AfterEach(func() {
operating.System.Local = time.Local
})
It("prints times and duration for a sub-minute backup", func() {
endTime := time.Date(2017, 1, 1, 1, 1, 3, 2, operating.System.Local)
start, end, duration := report.GetDurationInfo(timestamp, endTime)
Expect(start).To(Equal("Sun Jan 01 2017 01:01:01"))
Expect(end).To(Equal("Sun Jan 01 2017 01:01:03"))
Expect(duration).To(Equal("0:00:02"))
})
It("prints times and duration for a sub-hour backup", func() {
endTime := time.Date(2017, 1, 1, 1, 4, 3, 2, operating.System.Local)
start, end, duration := report.GetDurationInfo(timestamp, endTime)
Expect(start).To(Equal("Sun Jan 01 2017 01:01:01"))
Expect(end).To(Equal("Sun Jan 01 2017 01:04:03"))
Expect(duration).To(Equal("0:03:02"))
})
It("prints times and duration for a multiple-hour backup", func() {
endTime := time.Date(2017, 1, 1, 5, 4, 3, 2, operating.System.Local)
start, end, duration := report.GetDurationInfo(timestamp, endTime)
Expect(start).To(Equal("Sun Jan 01 2017 01:01:01"))
Expect(end).To(Equal("Sun Jan 01 2017 05:04:03"))
Expect(duration).To(Equal("4:03:02"))
})
It("prints times and duration for a backup going past midnight", func() {
endTime := time.Date(2017, 1, 2, 1, 4, 3, 2, operating.System.Local)
start, end, duration := report.GetDurationInfo(timestamp, endTime)
Expect(start).To(Equal("Sun Jan 01 2017 01:01:01"))
Expect(end).To(Equal("Mon Jan 02 2017 01:04:03"))
Expect(duration).To(Equal("24:03:02"))
})
It("prints times and duration for a backup during the spring time change", func() {
operating.System.Local, _ = time.LoadLocation("America/Los_Angeles") // Ensure test works regardless of time zone of test machine
dst := "20170312010000"
endTime := time.Date(2017, 3, 12, 3, 0, 0, 0, operating.System.Local)
start, end, duration := report.GetDurationInfo(dst, endTime)
Expect(start).To(Equal("Sun Mar 12 2017 01:00:00"))
Expect(end).To(Equal("Sun Mar 12 2017 03:00:00"))
Expect(duration).To(Equal("1:00:00"))
})
It("prints times and duration for a backup during the fall time change", func() {
operating.System.Local, _ = time.LoadLocation("America/Los_Angeles") // Ensure test works regardless of time zone of test machine
dst := "20171105010000"
endTime := time.Date(2017, 11, 5, 3, 0, 0, 0, operating.System.Local)
start, end, duration := report.GetDurationInfo(dst, endTime)
Expect(start).To(Equal("Sun Nov 05 2017 01:00:00"))
Expect(end).To(Equal("Sun Nov 05 2017 03:00:00"))
Expect(duration).To(Equal("3:00:00"))
})
})
Describe("EnsureBackupVersionCompatibility", func() {
It("Panics if gpbackup version is greater than gprestore version", func() {
defer testhelper.ShouldPanicWithMessage("gprestore 0.1.0 cannot restore a backup taken with gpbackup 0.2.0; please use gprestore 0.2.0 or later.")
report.EnsureBackupVersionCompatibility("0.2.0", "0.1.0")
})
It("Does not panic if gpbackup version is less than gprestore version", func() {
report.EnsureBackupVersionCompatibility("0.1.0", "0.1.3")
})
It("Does not panic if gpbackup version equals gprestore version", func() {
report.EnsureBackupVersionCompatibility("0.1.0", "0.1.0")
})
})
Describe("EnsureDatabaseVersionCompatibility", func() {
var restoreVersion dbconn.GPDBVersion
BeforeEach(func() {
semver, _ := semver.Make("5.0.0")
restoreVersion = dbconn.GPDBVersion{
VersionString: "5.0.0-beta.9+dev.129.g4bd4e41 build dev",
SemVer: semver,
}
})
It("Panics if backup database major version is greater than restore major version", func() {
defer testhelper.ShouldPanicWithMessage("Cannot restore from GPDB version 6.0.0-beta.9+dev.129.g4bd4e41 build dev to 5.0.0-beta.9+dev.129.g4bd4e41 build dev due to catalog incompatibilities.")
report.EnsureDatabaseVersionCompatibility("6.0.0-beta.9+dev.129.g4bd4e41 build dev", restoreVersion)
})
It("Does not panic if backup database major version is greater than restore major version", func() {
report.EnsureDatabaseVersionCompatibility("4.3.16-beta.9+dev.129.g4bd4e41 build dev", restoreVersion)
})
It("Does not panic if backup database major version is equal to restore major version", func() {
report.EnsureDatabaseVersionCompatibility("5.0.6-beta.9+dev.129.g4bd4e41 build dev", restoreVersion)
})
})
Describe("Email-related functions", func() {
reportFileContents := []byte(`Apache Cloudberry Backup Report
Timestamp Key: 20170101010101`)
contactsFileContents, _ := yaml.Marshal(report.ContactFile{
Contacts: map[string][]report.EmailContact{
"gpbackup": {
{Address: "contact1@example.com",
Status: map[string]bool{
"success": true,
"success_with_errors": true,
"failure": false,
}},
{Address: "contact2@example.org",
Status: map[string]bool{
"success": false,
"success_with_errors": true,
"failure": true,
}},
},
"gprestore": {
{Address: "contact3@example.com"},
{Address: "contact4@example.org",
Status: map[string]bool{
"success": true,
"success_with_errors": true,
"failure": true,
}},
},
}})
contactsList := "contact1@example.com contact2@example.org"
var (
testExecutor *testhelper.TestExecutor
testCluster *cluster.Cluster
testFPInfo filepath.FilePathInfo
w *os.File
r *os.File
)
BeforeEach(func() {
r, w, _ = os.Pipe()
testCluster = testutils.SetDefaultSegmentConfiguration()
testFPInfo = filepath.NewFilePathInfo(testCluster, "", "20170101010101", "gpseg", false)
operating.System.OpenFileRead = func(name string, flag int, perm os.FileMode) (operating.ReadCloserAt, error) { return r, nil }
operating.System.ReadFile = func(filename string) ([]byte, error) { return io.ReadAll(r) }
operating.System.Hostname = func() (string, error) { return "localhost", nil }
operating.System.Getenv = func(key string) string {
if key == "HOME" {
return "home"
} else {
return "gphome"
}
}
testExecutor = &testhelper.TestExecutor{}
testCluster.Executor = testExecutor
gplog.SetErrorCode(0)
})
AfterEach(func() {
operating.InitializeSystemFunctions()
gplog.SetErrorCode(0)
})
Context("GetContacts", func() {
contactsFilename := fmt.Sprintf("%s/bin/gp_email_contacts.yaml", operating.System.Getenv("GPHOME"))
It("Gets a list of gpbackup contacts on success", func() {
gplog.SetErrorCode(0)
_, _ = w.Write(contactsFileContents)
_ = w.Close()
contacts := report.GetContacts(contactsFilename, "gpbackup")
Expect(contacts).To(Equal("contact1@example.com"))
})
It("Gets a list of gpbackup contacts on success with errors", func() {
gplog.SetErrorCode(1)
_, _ = w.Write(contactsFileContents)
_ = w.Close()
contacts := report.GetContacts(contactsFilename, "gpbackup")
Expect(contacts).To(Equal("contact1@example.com contact2@example.org"))
})
It("Gets a list of gpbackup contacts on failure", func() {
gplog.SetErrorCode(2)
_, _ = w.Write(contactsFileContents)
_ = w.Close()
contacts := report.GetContacts(contactsFilename, "gpbackup")
Expect(contacts).To(Equal("contact2@example.org"))
})
It("Gets a list of gprestore contacts and doesn't fail when no status specified", func() {
gplog.SetErrorCode(0)
_, _ = w.Write(contactsFileContents)
_ = w.Close()
contacts := report.GetContacts(contactsFilename, "gprestore")
Expect(contacts).To(Equal("contact4@example.org"))
})
})
Context("ConstructEmailMessage for successful backup", func() {
It("adds HTML formatting to the contents of the report file", func() {
_, _ = w.Write(reportFileContents)
_ = w.Close()
message := report.ConstructEmailMessage(testFPInfo.Timestamp, contactsList, "report_file", "gpbackup", true, "testdb")
expectedMessage := `To: contact1@example.com contact2@example.org
Subject: gpbackup 20170101010101 of database testdb on localhost completed: Success
Content-Type: text/html
Content-Disposition: inline
<html>
<body>
<pre style=\"font: monospace\">
Apache Cloudberry Backup Report
Timestamp Key: 20170101010101
</pre>
</body>
</html>`
Expect(message).To(Equal(expectedMessage))
})
})
Context("ConstructEmailMessage for Failed backup", func() {
It("adds HTML formatting to the contents of the report file", func() {
_, _ = w.Write(reportFileContents)
_ = w.Close()
message := report.ConstructEmailMessage(testFPInfo.Timestamp, contactsList, "report_file", "gpbackup", false, "testdb")
expectedMessage := `To: contact1@example.com contact2@example.org
Subject: gpbackup 20170101010101 of database testdb on localhost completed: Failure
Content-Type: text/html
Content-Disposition: inline
<html>
<body>
<pre style=\"font: monospace\">
Apache Cloudberry Backup Report
Timestamp Key: 20170101010101
</pre>
</body>
</html>`
Expect(message).To(Equal(expectedMessage))
})
})
Context("EmailReport", func() {
var (
expectedHomeCmd = "test -f home/gp_email_contacts.yaml"
expectedGpHomeCmd = "test -f gphome/bin/gp_email_contacts.yaml"
expectedMessage = `echo "To: contact1@example.com
Subject: gpbackup 20170101010101 of database testdb on localhost completed: Success
Content-Type: text/html
Content-Disposition: inline
<html>
<body>
<pre style=\"font: monospace\">
</pre>
</body>
</html>" | sendmail -t`
)
It("sends no email and raises a warning if no gp_email_contacts.yaml file is found", func() {
_, _ = w.Write(contactsFileContents)
_ = w.Close()
testExecutor.LocalError = errors.Errorf("exit status 2")
report.EmailReport(testCluster, testFPInfo.Timestamp, "report_file", "gpbackup", true, "testdb")
Expect(testExecutor.NumExecutions).To(Equal(2))
Expect(testExecutor.LocalCommands).To(Equal([]string{expectedHomeCmd, expectedGpHomeCmd}))
Expect(stdout).To(Say("Found neither gphome/bin/gp_email_contacts.yaml nor home/gp_email_contacts.yaml"))
})
It("sends an email to contacts in $HOME/gp_email_contacts.yaml if only that file is found", func() {
_, _ = w.Write(contactsFileContents)
_ = w.Close()
testExecutor.ErrorOnExecNum = 2 // Shouldn't hit this case, as it shouldn't be executed a second time
testExecutor.LocalError = errors.Errorf("exit status 2")
report.EmailReport(testCluster, testFPInfo.Timestamp, "report_file", "gpbackup", true, "testdb")
Expect(testExecutor.NumExecutions).To(Equal(2))
Expect(testExecutor.LocalCommands).To(Equal([]string{expectedHomeCmd, expectedMessage}))
Expect(logfile).To(Say("Sending email report to the following addresses: contact1@example.com"))
})
It("sends an email to contacts in $GPHOME/bin/gp_email_contacts.yaml if only that file is found", func() {
_, _ = w.Write(contactsFileContents)
_ = w.Close()
testExecutor.ErrorOnExecNum = 1
testExecutor.LocalError = errors.Errorf("exit status 2")
report.EmailReport(testCluster, testFPInfo.Timestamp, "report_file", "gpbackup", true, "testdb")
Expect(testExecutor.NumExecutions).To(Equal(3))
Expect(testExecutor.LocalCommands).To(Equal([]string{expectedHomeCmd, expectedGpHomeCmd, expectedMessage}))
Expect(logfile).To(Say("Sending email report to the following addresses: contact1@example.com"))
})
It("sends an email to contacts in $HOME/gp_email_contacts.yaml if a file exists in both $HOME and $GPHOME/bin", func() {
_, _ = w.Write(contactsFileContents)
_ = w.Close()
report.EmailReport(testCluster, testFPInfo.Timestamp, "report_file", "gpbackup", true, "testdb")
Expect(testExecutor.NumExecutions).To(Equal(2))
Expect(testExecutor.LocalCommands).To(Equal([]string{expectedHomeCmd, expectedMessage}))
Expect(logfile).To(Say("Sending email report to the following addresses: contact1@example.com"))
})
})
})
})