From cadb15f7afb5e7c88667eb4006209efca17744af Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Mon, 24 Feb 2020 23:05:45 +0100 Subject: Added tests to the main package and completely reworked the code around that --- .gitignore | 2 +- README.md | 2 - config/config.go | 21 ----- config/init.go | 16 ---- config/statefile.go | 39 --------- config/workdir.go | 40 --------- job/utils.go | 31 ++++--- job/utils_test.go | 8 +- main.go | 110 +----------------------- zabbix/flags.go | 21 +++++ zabbix/main.go | 104 ++++++++++++++++++++++ zabbix/main_test.go | 56 ++++++++++++ zabbix/statefile.go | 34 ++++++++ zabbix/testdata/bareos-fd-17.2.state | Bin 0 -> 2196 bytes zabbix/testdata/bareos-fd-18.2.state | Bin 0 -> 2196 bytes zabbix/testdata/bareos-fd-18.2.state-with-error | Bin 0 -> 2196 bytes zabbix/workdir.go | 38 ++++++++ 17 files changed, 284 insertions(+), 238 deletions(-) delete mode 100644 config/config.go delete mode 100644 config/init.go delete mode 100644 config/statefile.go delete mode 100644 config/workdir.go create mode 100644 zabbix/flags.go create mode 100644 zabbix/main.go create mode 100644 zabbix/main_test.go create mode 100644 zabbix/statefile.go create mode 100644 zabbix/testdata/bareos-fd-17.2.state create mode 100644 zabbix/testdata/bareos-fd-18.2.state create mode 100644 zabbix/testdata/bareos-fd-18.2.state-with-error create mode 100644 zabbix/workdir.go diff --git a/.gitignore b/.gitignore index 5800ceb..ec82ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .*.sw[a-p] bareos-zabbix-check bareos-zabbix-check.spool -*.state +zabbix/tmp diff --git a/README.md b/README.md index 884189d..d36b922 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,6 @@ The common way to run this check is without any argument : There are several flags available if you need to override the defaults : - -f string : Force the state file to use, defaults to bareos-fd.9102.state if it exists else bacula-fd.9102.state. - - -q bool : Suppress all output, suitable to force a silent update of the spool file. - - -v bool : Activates verbose debugging output, defaults to false. - -w string : Force the work directory to use, defaults to /var/lib/bareos if it exists else /var/lib/bacula. ## Output diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 262a5d2..0000000 --- a/config/config.go +++ /dev/null @@ -1,21 +0,0 @@ -package config - -// Config object -type Config struct { - verbose bool - quiet bool - stateFile string - workDir string -} - -// Verbose gets the verbose field of the configuration -func (config *Config) Verbose() bool { return config.verbose } - -// Quiet gets the quiet field of the configuration -func (config *Config) Quiet() bool { return config.quiet } - -// StateFile gets the stateFile field of the configuration -func (config *Config) StateFile() string { return config.stateFile } - -// WorkDir gets the workDir field of the configuration -func (config *Config) WorkDir() string { return config.workDir } diff --git a/config/init.go b/config/init.go deleted file mode 100644 index f737fb3..0000000 --- a/config/init.go +++ /dev/null @@ -1,16 +0,0 @@ -package config - -import "flag" - -// Init initialises a program config from the command line flags -func (c *Config) Init() { - flag.BoolVar(&c.verbose, "v", false, "Activates verbose debugging output, defaults to false.") - flag.BoolVar(&c.quiet, "q", false, "Suppress all output, suitable to force a silent update of the spool file.") - flag.StringVar(&c.stateFile, "f", "", "Force the state file to use, defaults to "+bareosStateFile+" if it exists else "+baculaStateFile+".") - flag.StringVar(&c.workDir, "w", "", "Force the work directory to use, defaults to "+bareosWorkDir+" if it exists else "+baculaWorkDir+".") - - // command line arguments parsing - flag.Parse() - c.checkWorkDir() - c.checkStateFile() -} diff --git a/config/statefile.go b/config/statefile.go deleted file mode 100644 index 22fd214..0000000 --- a/config/statefile.go +++ /dev/null @@ -1,39 +0,0 @@ -package config - -import ( - "fmt" - "log" - "os" - "path/filepath" -) - -const ( - bareosStateFile = "bareos-fd.9102.state" - baculaStateFile = "bacula-fd.9102.state" -) - -func (c *Config) checkStateFile() { - // Finds the state file to parse - if c.stateFile != "" { - c.stateFile = filepath.Join(c.workDir, c.stateFile) - info, err := os.Stat(c.stateFile) - if os.IsNotExist(err) || info.IsDir() { - fmt.Printf("INFO The state file %s does not exist.\n", c.stateFile) - os.Exit(0) - } - } else { - c.stateFile = filepath.Join(c.workDir, bareosStateFile) - info, err := os.Stat(c.stateFile) - if os.IsNotExist(err) || info.IsDir() { - c.stateFile = filepath.Join(c.workDir, baculaStateFile) - info, err = os.Stat(c.stateFile) - if os.IsNotExist(err) || info.IsDir() { - fmt.Println("INFO Could not find a suitable state file. Has a job ever run?") - os.Exit(0) - } - } - } - if c.verbose { - log.Println("Using state file ", c.stateFile) - } -} diff --git a/config/workdir.go b/config/workdir.go deleted file mode 100644 index 208df1a..0000000 --- a/config/workdir.go +++ /dev/null @@ -1,40 +0,0 @@ -package config - -import ( - "fmt" - "log" - "os" - "path/filepath" -) - -const ( - bareosWorkDir = "/var/lib/bareos" - baculaWorkDir = "/var/lib/bacula" -) - -// checkWorkDir checks if a work directory is valid -func (c *Config) checkWorkDir() { - // Determine the work directory to use. - if c.workDir != "" { - info, err := os.Stat(c.workDir) - if os.IsNotExist(err) || !info.IsDir() { - fmt.Printf("INFO Invalid work directory %s : it does not exist or is not a directory.\n", c.workDir) - os.Exit(0) - } - } else { - c.workDir = bareosWorkDir - info, err := os.Stat(c.workDir) - if os.IsNotExist(err) || !info.IsDir() { - c.workDir = baculaWorkDir - info, err := os.Stat(c.workDir) - if os.IsNotExist(err) || !info.IsDir() { - fmt.Println("INFO Could not find a suitable work directory. Is bareos or bacula installed?") - os.Exit(0) - } - } - } - c.workDir = filepath.Clean(c.workDir) - if c.verbose { - log.Println("Setting work directory to ", c.workDir) - } -} diff --git a/job/utils.go b/job/utils.go index 33c25cd..a6a2f43 100644 --- a/job/utils.go +++ b/job/utils.go @@ -1,25 +1,32 @@ package job // KeepOldestOnly filters a job list and keeps only the most recent entry for a job name -func KeepOldestOnly(jobs []Job) []Job { - tmpMap := make(map[string]Job) - for _, elt := range jobs { - prev, exists := tmpMap[elt.Name] - if !exists || (exists && prev.Timestamp < elt.Timestamp) { - tmpMap[elt.Name] = elt +func KeepOldestOnly(jobs []Job) (results []Job) { +outerLoop: + for i := 0; i < len(jobs); i++ { + job := jobs[i] + for j := 0; j < len(results); j++ { + result := results[j] + if result.Name == job.Name { + continue outerLoop + } } + for j := i + 1; j < len(jobs); j++ { + sec := jobs[j] + if sec.Name == job.Name && sec.Timestamp > job.Timestamp { + job = sec + } + } + results = append(results, job) } - values := make([]Job, 0, len(tmpMap)) - for _, value := range tmpMap { - values = append(values, value) - } - return values + return } // KeepSuccessOnly returns only the successful jobs from a job list (suiatble to write a new spool file) func KeepSuccessOnly(jobs []Job) (result []Job) { result = make([]Job, 0) - for _, job := range jobs { + for i := 0; i < len(jobs); i++ { + job := jobs[i] if job.Success { result = append(result, job) } diff --git a/job/utils_test.go b/job/utils_test.go index a3e4dcd..250ae2c 100644 --- a/job/utils_test.go +++ b/job/utils_test.go @@ -9,8 +9,13 @@ func TestKeepOldestOnly(t *testing.T) { emptyList := []Job{} oneJob := []Job{{Name: "a", Timestamp: 10, Success: true}} twoJobs := []Job{ + {Name: "a", Timestamp: 5, Success: true}, {Name: "a", Timestamp: 10, Success: true}, + } + threeJobs := []Job{ {Name: "a", Timestamp: 5, Success: true}, + {Name: "a", Timestamp: 10, Success: true}, + {Name: "a", Timestamp: 8, Success: false}, } type args struct { jobs []Job @@ -20,9 +25,10 @@ func TestKeepOldestOnly(t *testing.T) { args args want []Job }{ - {"empty list", args{emptyList}, emptyList}, + {"empty list", args{emptyList}, nil}, {"one job", args{oneJob}, oneJob}, {"two jobs", args{twoJobs}, oneJob}, + {"three jobs", args{threeJobs}, oneJob}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/main.go b/main.go index 62347ab..f55943d 100644 --- a/main.go +++ b/main.go @@ -1,114 +1,12 @@ +//+build !test + package main import ( - "bareos-zabbix-check/config" - "bareos-zabbix-check/job" - "bareos-zabbix-check/spool" - "bareos-zabbix-check/state" + "bareos-zabbix-check/zabbix" "fmt" - "log" - "os" - "path/filepath" - "time" -) - -const ( - spoolFileName = "bareos-zabbix-check.spool" ) func main() { - var ( - config config.Config - errorString string - missingString string - ) - config.Init() - // Open the state file - stateFile, err := os.Open(config.StateFile()) - if err != nil { - fmt.Printf("INFO Couldn't open state file : %s", err) - os.Exit(0) - } - defer stateFile.Close() - // parse the state file - header, err := state.ParseHeader(stateFile) - if err != nil { - fmt.Printf("INFO Could not parse state file header : %s", err) - os.Exit(0) - } - if config.Verbose() { - log.Printf("Parsed header: %+s\n", header) - } - // seek to the job entries in the state file - offset, err := stateFile.Seek(int64(header.LastJobsAddr), 0) - if err != nil { - fmt.Printf("INFO Couldn't seek to jobs position in state file : %s", err) - } - if uint64(offset) != header.LastJobsAddr { - fmt.Print("INFO Truncated state file") - } - // Then parse the jobs in the state file - jobs, err := state.ParseJobs(stateFile) - if err != nil { - fmt.Printf("INFO Could not parse jobs in state file : %s", err) - } - if config.Verbose() { - log.Printf("%d jobs found in state file\n", len(jobs)) - for i := 0; i < len(jobs); i++ { - log.Print(jobs[i]) - } - } - - // We will check for errors in loading the spool file only at the end. If all jobs ran successfully without errors - // in the state file and we manage to write a new spool file without errors, then we will ignore any error here to - // avoid false positives during backup bootstrap - // Open the spool file - spoolFile, spoolErr := os.Open(filepath.Join(config.WorkDir(), spoolFileName)) - var spoolJobs []job.Job - if err == nil { - defer spoolFile.Close() - spoolJobs, spoolErr = spool.Parse(spoolFile) - } - - jobs = job.KeepOldestOnly(append(jobs, spoolJobs...)) - - // we write this new spool - spoolFile, err = os.Create(filepath.Join(config.WorkDir(), spoolFileName)) - if err == nil { - defer spoolFile.Close() - err = spool.Serialize(spoolFile, jobs) - } - if err != nil { - fmt.Printf("AVERAGE: Error saving the spool file : %s\n", err) - os.Exit(0) - } - - now := uint64(time.Now().Unix()) - // We build the error strings - for _, job := range jobs { - if job.Success { - if job.Timestamp < now-24*3600 { - if missingString == "" { - missingString = fmt.Sprintf("missing: %s", job.Name) - } else { - missingString = fmt.Sprintf("%s, %s", missingString, job.Name) - } - } - } else { - if errorString == "" { - errorString = fmt.Sprintf("errors: %s", job.Name) - } else { - errorString = fmt.Sprintf("%s, %s", errorString, job.Name) - } - } - } - // Finally we output - if errorString != "" || missingString != "" { - fmt.Printf("AVERAGE: %s %s", errorString, missingString) - if spoolErr != nil { - fmt.Printf(" additionnal errors: %s", spoolErr) - } - } else { - fmt.Printf("OK") - } + fmt.Print(zabbix.Main()) } diff --git a/zabbix/flags.go b/zabbix/flags.go new file mode 100644 index 0000000..3e95321 --- /dev/null +++ b/zabbix/flags.go @@ -0,0 +1,21 @@ +package zabbix + +import ( + "flag" +) + +var ( + stateFileName string + workDir string +) + +func processFlags() (err error) { + flag.StringVar(&stateFileName, "f", "", "Force the state file to use, defaults to "+bareosStateFile+" if it exists else "+baculaStateFile+".") + flag.StringVar(&workDir, "w", "", "Force the work directory to use, defaults to "+bareosWorkDir+" if it exists else "+baculaWorkDir+".") + flag.Parse() + err = checkWorkDir() + if err == nil { + err = checkStateFile() + } + return +} diff --git a/zabbix/main.go b/zabbix/main.go new file mode 100644 index 0000000..576fc17 --- /dev/null +++ b/zabbix/main.go @@ -0,0 +1,104 @@ +package zabbix + +import ( + "bareos-zabbix-check/job" + "bareos-zabbix-check/spool" + "bareos-zabbix-check/state" + "fmt" + "os" + "path/filepath" + "time" +) + +const ( + spoolFileName = "bareos-zabbix-check.spool" +) + +var now = uint64(time.Now().Unix()) + +// Main the true main function of this program +func Main() string { + err := processFlags() + if err != nil { + return fmt.Sprintf("INFO Failed to init programm : %s", err) + } + // Open the state file + stateFile, err := os.Open(stateFileName) + if err != nil { + return fmt.Sprintf("INFO Could not open state file : %s", err) + } + defer stateFile.Close() + // parse the state file + header, err := state.ParseHeader(stateFile) + if err != nil { + return fmt.Sprintf("INFO Could not parse state file header : %s", err) + } + // seek to the job entries in the state file + offset, err := stateFile.Seek(int64(header.LastJobsAddr), 0) + if err != nil { + return fmt.Sprintf("INFO Couldn't seek to jobs position in state file : %s", err) + } + if uint64(offset) != header.LastJobsAddr { + return fmt.Sprint("INFO Truncated state file") + } + // Then parse the jobs in the state file + jobs, err := state.ParseJobs(stateFile) + if err != nil { + return fmt.Sprintf("INFO Could not parse jobs in state file : %s", err) + } + + // We will check for errors in loading the spool file only at the end. If all jobs ran successfully without errors + // in the state file and we manage to write a new spool file without errors, then we will ignore any error here to + // avoid false positives during backup bootstrap + // Open the spool file + spoolFile, spoolErr := os.Open(filepath.Join(workDir, spoolFileName)) + var spoolJobs []job.Job + if err == nil { + defer spoolFile.Close() + spoolJobs, spoolErr = spool.Parse(spoolFile) + } + + jobs = job.KeepOldestOnly(append(jobs, spoolJobs...)) + + // we write this new spool + spoolFile, err = os.Create(filepath.Join(workDir, spoolFileName)) + if err == nil { + defer spoolFile.Close() + err = spool.Serialize(spoolFile, jobs) + } + if err != nil { + return fmt.Sprintf("AVERAGE: Error saving the spool file : %s\n", err) + } + + var ( + errorString string + missingString string + ) + // We build the error strings + for i := 0; i < len(jobs); i++ { + job := jobs[i] + if job.Success { + if job.Timestamp < now-24*3600 { + if missingString == "" { + missingString = fmt.Sprintf("missing: %s", job.Name) + } else { + missingString = fmt.Sprintf("%s, %s", missingString, job.Name) + } + } + } else { + if errorString == "" { + errorString = fmt.Sprintf("errors: %s", job.Name) + } else { + errorString = fmt.Sprintf("%s, %s", errorString, job.Name) + } + } + } + // Finally we output + if errorString != "" || missingString != "" { + if spoolErr != nil { + return fmt.Sprintf("AVERAGE: %s %s %s", errorString, missingString, spoolErr) + } + return fmt.Sprintf("AVERAGE: %s %s", errorString, missingString) + } + return "OK" +} diff --git a/zabbix/main_test.go b/zabbix/main_test.go new file mode 100644 index 0000000..c0d6056 --- /dev/null +++ b/zabbix/main_test.go @@ -0,0 +1,56 @@ +package zabbix + +import ( + "flag" + "os" + "path/filepath" + "testing" +) + +func TestMain(t *testing.T) { + os.RemoveAll("tmp") + cwd, _ := os.Getwd() + err := os.MkdirAll("tmp/ok-18.2", 0777) + if err != nil { + t.Skipf("skipping main tests because tmp directory cannot be created : %s", err) + } + os.MkdirAll("tmp/ok-17.2", 0777) + os.MkdirAll("tmp/no_state_file", 0777) + os.MkdirAll("tmp/bacula_auto_detect_failed/var/lib/bacula", 0777) + os.MkdirAll("tmp/bareos_auto_detect_failed/var/lib/bareos", 0777) + os.MkdirAll("tmp/error", 0777) + os.Symlink("../../testdata/bareos-fd-17.2.state", "tmp/ok-17.2/state") + os.Symlink("../../testdata/bareos-fd-18.2.state", "tmp/ok-18.2/state") + os.Symlink("../../testdata/bareos-fd-18.2.state-with-error", "tmp/error/state") + tests := []struct { + name string + timestamp uint64 + rootDir string + args []string + want string + }{ + {"failed bacula_auto_detect", 0, "tmp/bacula_auto_detect_failed", []string{}, "INFO Failed to init programm : Could not find a suitable state file. Has a job ever run?"}, + {"failed bareos_auto_detect", 0, "tmp/bareos_auto_detect_failed", []string{}, "INFO Failed to init programm : Could not find a suitable state file. Has a job ever run?"}, + {"failed auto_detect", 0, "tmp/non_existent", []string{}, "INFO Failed to init programm : Could not find a suitable work directory. Is bareos or bacula installed?"}, + {"no work directory", 0, "tmp", []string{"-w", "/non_existent"}, "INFO Failed to init programm : Invalid work directory /home/julien/git/awh/bareos-zabbix-check/zabbix/tmp/non_existent : it does not exist or is not a directory"}, + {"no state file auto_detect", 0, "tmp", []string{"-w", "/no_state_file"}, "INFO Failed to init programm : Could not find a suitable state file. Has a job ever run?"}, + {"no state file", 0, "tmp", []string{"-w", "/no_state_file", "-f", "test"}, "INFO Failed to init programm : The state file /home/julien/git/awh/bareos-zabbix-check/zabbix/tmp/no_state_file/test does not exist"}, + {"ok bareos 18.2", 1582579731, "tmp/ok-18.2", []string{"-w", "/", "-f", "state"}, "OK"}, + {"ok bareos 17.2", 1582579731, "tmp/ok-17.2", []string{"-w", "/", "-f", "state"}, "OK"}, + {"missing", 1582709331, "tmp/ok-18.2", []string{"-w", "/", "-f", "state"}, "AVERAGE: missing: awhphpipam1_percona_xtrabackup, awhphpipam1_LinuxAll, awhphpipam1_www"}, + {"error", 1582579731, "tmp/error", []string{"-w", "/", "-f", "state"}, "AVERAGE: errors: awhphpipam1_percona_xtrabackup, awhphpipam1_www Corrupted spool file: invalid argument"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now = tt.timestamp + root = filepath.Join(cwd, tt.rootDir) + flag.CommandLine = flag.NewFlagSet("bareos-zabbix-check", flag.ExitOnError) //flags are now reset + os.Args = append([]string{"bareos-zabbix-check"}, tt.args...) + if got := Main(); got != tt.want { + t.Log(workDir) + t.Errorf("Main() = %v, want %v", got, tt.want) + } + }) + } + os.RemoveAll("tmp") +} diff --git a/zabbix/statefile.go b/zabbix/statefile.go new file mode 100644 index 0000000..26ea650 --- /dev/null +++ b/zabbix/statefile.go @@ -0,0 +1,34 @@ +package zabbix + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + bareosStateFile = "bareos-fd.9102.state" + baculaStateFile = "bacula-fd.9102.state" +) + +func checkStateFile() error { + // Finds the state file to parse + if stateFileName != "" { + stateFileName = filepath.Join(workDir, stateFileName) + info, err := os.Stat(stateFileName) + if os.IsNotExist(err) || info.IsDir() { + return fmt.Errorf("The state file %s does not exist", stateFileName) + } + } else { + stateFileName = filepath.Join(workDir, bareosStateFile) + info, err := os.Stat(stateFileName) + if os.IsNotExist(err) || info.IsDir() { + stateFileName = filepath.Join(workDir, baculaStateFile) + info, err = os.Stat(stateFileName) + if os.IsNotExist(err) || info.IsDir() { + return fmt.Errorf("Could not find a suitable state file. Has a job ever run?") + } + } + } + return nil +} diff --git a/zabbix/testdata/bareos-fd-17.2.state b/zabbix/testdata/bareos-fd-17.2.state new file mode 100644 index 0000000..e98786c Binary files /dev/null and b/zabbix/testdata/bareos-fd-17.2.state differ diff --git a/zabbix/testdata/bareos-fd-18.2.state b/zabbix/testdata/bareos-fd-18.2.state new file mode 100644 index 0000000..ca1f30c Binary files /dev/null and b/zabbix/testdata/bareos-fd-18.2.state differ diff --git a/zabbix/testdata/bareos-fd-18.2.state-with-error b/zabbix/testdata/bareos-fd-18.2.state-with-error new file mode 100644 index 0000000..3d28356 Binary files /dev/null and b/zabbix/testdata/bareos-fd-18.2.state-with-error differ diff --git a/zabbix/workdir.go b/zabbix/workdir.go new file mode 100644 index 0000000..287c80a --- /dev/null +++ b/zabbix/workdir.go @@ -0,0 +1,38 @@ +package zabbix + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + bareosWorkDir = "/var/lib/bareos" + baculaWorkDir = "/var/lib/bacula" +) + +var root = "/" + +// checkWorkDir checks if a work directory is valid +func checkWorkDir() error { + // Determine the work directory to use. + if workDir != "" { + workDir = filepath.Join(root, workDir) + info, err := os.Stat(workDir) + if os.IsNotExist(err) || !info.IsDir() { + return fmt.Errorf("Invalid work directory %s : it does not exist or is not a directory", workDir) + } + } else { + workDir = filepath.Join(root, bareosWorkDir) + info, err := os.Stat(workDir) + if os.IsNotExist(err) || !info.IsDir() { + workDir = filepath.Join(root, baculaWorkDir) + info, err := os.Stat(workDir) + if os.IsNotExist(err) || !info.IsDir() { + return fmt.Errorf("Could not find a suitable work directory. Is bareos or bacula installed?") + } + } + } + workDir = filepath.Clean(workDir) + return nil +} -- cgit v1.2.3