diff --git a/pkg/tstune/config_file.go b/pkg/tstune/config_file.go index 9e62b6f..f9f312d 100644 --- a/pkg/tstune/config_file.go +++ b/pkg/tstune/config_file.go @@ -5,21 +5,32 @@ import ( "fmt" "io" "os" + "path" "strings" + "time" ) const ( - osMac = "darwin" - osLinux = "linux" - fileNameMac = "/usr/local/var/postgres/postgresql.conf" - fileNameDebianFmt = "/etc/postgresql/%s/main/postgresql.conf" - fileNameRPMFmt = "/var/lib/pgsql/%s/data/postgresql.conf" - fileNameArch = "/var/lib/postgres/data/postgresql.conf" - errConfigNotFoundFmt = "could not find postgresql.conf at any of these locations:\n%v" + osMac = "darwin" + osLinux = "linux" + + fileNameMac = "/usr/local/var/postgres/postgresql.conf" + fileNameDebianFmt = "/etc/postgresql/%s/main/postgresql.conf" + fileNameRPMFmt = "/var/lib/pgsql/%s/data/postgresql.conf" + fileNameArch = "/var/lib/postgres/data/postgresql.conf" + + errConfigNotFoundFmt = "could not find postgresql.conf at any of these locations:\n%v" + errBackupNotCreatedFmt = "could not create backup at %s: %v" + + backupFilePrefix = "timescaledb_tune.backup" + backupDateFmt = "200601021504" ) // allows us to substitute mock versions in tests var osStatFn = os.Stat +var osCreateFn = func(path string) (io.Writer, error) { + return os.Create(path) +} // fileExists is a simple check for stating if a file exists and if any error // occurs it returns false. @@ -119,6 +130,19 @@ func getConfigFileState(r io.Reader) (*configFileState, error) { return cfs, nil } +// Backup writes the conf file state to the system's temporary directory +// with a well known name format so it can potentially be restored. +func (cfs *configFileState) Backup() (string, error) { + backupName := backupFilePrefix + time.Now().Format(backupDateFmt) + backupPath := path.Join(os.TempDir(), backupName) + bf, err := osCreateFn(backupPath) + if err != nil { + return backupPath, fmt.Errorf(errBackupNotCreatedFmt, backupPath, err) + } + _, err = cfs.WriteTo(bf) + return backupPath, err +} + func (cfs *configFileState) WriteTo(w io.Writer) (int64, error) { ret := int64(0) for _, l := range cfs.lines { diff --git a/pkg/tstune/config_file_test.go b/pkg/tstune/config_file_test.go index c30aa1a..8b5112a 100644 --- a/pkg/tstune/config_file_test.go +++ b/pkg/tstune/config_file_test.go @@ -1,11 +1,15 @@ package tstune import ( + "bufio" "bytes" "fmt" + "io" "os" + "path" "strings" "testing" + "time" "github.com/timescale/timescaledb-tune/pkg/pgtune" ) @@ -324,6 +328,61 @@ func (w *testWriter) Write(buf []byte) (int, error) { return 0, nil } +func TestConfigFileStateBackup(t *testing.T) { + oldOSCreateFn := osCreateFn + now := time.Now() + lines := []string{"foo", "bar", "baz", "quaz"} + cfs := &configFileState{lines: lines} + wantFileName := backupFilePrefix + now.Format(backupDateFmt) + wantPath := path.Join(os.TempDir(), wantFileName) + + osCreateFn = func(_ string) (io.Writer, error) { + return nil, fmt.Errorf("erroring") + } + + path, err := cfs.Backup() + if path != wantPath { + t.Errorf("incorrect path in error case: got\n%s\nwant\n%s", path, wantPath) + } + if err == nil { + t.Errorf("unexpected lack of error for bad create") + } + want := fmt.Sprintf(errBackupNotCreatedFmt, wantPath, "erroring") + if got := err.Error(); got != want { + t.Errorf("incorrect error: got\n%s\nwant\n%s", got, want) + } + + var buf bytes.Buffer + osCreateFn = func(p string) (io.Writer, error) { + if p != wantPath { + t.Errorf("incorrect backup path: got %s want %s", p, wantPath) + } + return &buf, nil + } + path, err = cfs.Backup() + if path != wantPath { + t.Errorf("incorrect path in correct case: got\n%s\nwant\n%s", path, wantPath) + } + if err != nil { + t.Errorf("unexpected error for backup: %v", err) + } + + scanner := bufio.NewScanner(bytes.NewReader(buf.Bytes())) + i := 0 + for scanner.Scan() { + if scanner.Err() != nil { + t.Errorf("unexpected error while scanning: %v", scanner.Err()) + } + got := scanner.Text() + if want := lines[i]; got != want { + t.Errorf("incorrect line at %d: got\n%s\nwant\n%s", i, got, want) + } + i++ + } + + osCreateFn = oldOSCreateFn +} + func TestConfigFileStateWriteTo(t *testing.T) { cases := []struct { desc string diff --git a/pkg/tstune/tuner.go b/pkg/tstune/tuner.go index 89c9b2a..6790769 100644 --- a/pkg/tstune/tuner.go +++ b/pkg/tstune/tuner.go @@ -49,8 +49,8 @@ const ( fmtTunableParam = "%s = %s%s\n" fmtLastTuned = "timescaledb.last_tuned = '%s'" - dateFmt = "2006-01-02 15:04" - fudgeFactor = 0.05 + lastTunedDateFmt = "2006-01-02 15:04" + fudgeFactor = 0.05 pgMajor96 = "9.6" pgMajor10 = "10" @@ -192,6 +192,12 @@ func (t *Tuner) Run(flags *TunerFlags, in io.Reader, out io.Writer, outErr io.Wr ifErrHandle(err) t.cfs = cfs + // Write backup + backupPath, err := cfs.Backup() + t.handler.p.Statement("Writing backup to:") + printFn(os.Stderr, backupPath+"\n\n") + ifErrHandle(err) + // Process the tuning of settings if t.flags.Quiet { err = t.processQuiet(config) @@ -211,7 +217,7 @@ func (t *Tuner) Run(flags *TunerFlags, in io.Reader, out io.Writer, outErr io.Wr } // Append the current time to mark when database was last tuned - lastTunedLine := fmt.Sprintf(fmtLastTuned, time.Now().Format(dateFmt)) + lastTunedLine := fmt.Sprintf(fmtLastTuned, time.Now().Format(lastTunedDateFmt)) cfs.lines = append(cfs.lines, lastTunedLine) // Wrap up: Either write it out, or show success in --dry-run @@ -519,7 +525,7 @@ func (t *Tuner) processQuiet(config *pgtune.SystemConfig) error { return err } if changedSettings > 0 { - printFn(os.Stdout, fmtLastTuned+"\n", time.Now().Format(dateFmt)) + printFn(os.Stdout, fmtLastTuned+"\n", time.Now().Format(lastTunedDateFmt)) checker := newYesNoChecker("not using these settings could lead to suboptimal performance") err = t.promptUntilValidInput("Use these recommendations? "+promptYesNo, checker) if err != nil { diff --git a/pkg/tstune/tuner_test.go b/pkg/tstune/tuner_test.go index 29a13b8..4f74109 100644 --- a/pkg/tstune/tuner_test.go +++ b/pkg/tstune/tuner_test.go @@ -1266,7 +1266,7 @@ var ( ) func TestTunerProcessQuiet(t *testing.T) { - lastTuned := fmt.Sprintf(fmtLastTuned, time.Now().Format(dateFmt)) + lastTuned := fmt.Sprintf(fmtLastTuned, time.Now().Format(lastTunedDateFmt)) cases := []struct { desc string lines []string