1
0
Fork 0

Added tests to the main package and completely reworked the code around that

This commit is contained in:
Julien Dessaux 2020-02-24 23:05:45 +01:00
parent 2661ce9a2b
commit cadb15f7af
17 changed files with 285 additions and 239 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
.*.sw[a-p] .*.sw[a-p]
bareos-zabbix-check bareos-zabbix-check
bareos-zabbix-check.spool bareos-zabbix-check.spool
*.state zabbix/tmp

View file

@ -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 : 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. - -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. - -w string : Force the work directory to use, defaults to /var/lib/bareos if it exists else /var/lib/bacula.
## Output ## Output

View file

@ -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 }

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -1,25 +1,32 @@
package job package job
// KeepOldestOnly filters a job list and keeps only the most recent entry for a job name // KeepOldestOnly filters a job list and keeps only the most recent entry for a job name
func KeepOldestOnly(jobs []Job) []Job { func KeepOldestOnly(jobs []Job) (results []Job) {
tmpMap := make(map[string]Job) outerLoop:
for _, elt := range jobs { for i := 0; i < len(jobs); i++ {
prev, exists := tmpMap[elt.Name] job := jobs[i]
if !exists || (exists && prev.Timestamp < elt.Timestamp) { for j := 0; j < len(results); j++ {
tmpMap[elt.Name] = elt 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)) return
for _, value := range tmpMap {
values = append(values, value)
}
return values
} }
// KeepSuccessOnly returns only the successful jobs from a job list (suiatble to write a new spool file) // KeepSuccessOnly returns only the successful jobs from a job list (suiatble to write a new spool file)
func KeepSuccessOnly(jobs []Job) (result []Job) { func KeepSuccessOnly(jobs []Job) (result []Job) {
result = make([]Job, 0) result = make([]Job, 0)
for _, job := range jobs { for i := 0; i < len(jobs); i++ {
job := jobs[i]
if job.Success { if job.Success {
result = append(result, job) result = append(result, job)
} }

View file

@ -9,8 +9,13 @@ func TestKeepOldestOnly(t *testing.T) {
emptyList := []Job{} emptyList := []Job{}
oneJob := []Job{{Name: "a", Timestamp: 10, Success: true}} oneJob := []Job{{Name: "a", Timestamp: 10, Success: true}}
twoJobs := []Job{ twoJobs := []Job{
{Name: "a", Timestamp: 10, Success: true},
{Name: "a", Timestamp: 5, Success: true}, {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 { type args struct {
jobs []Job jobs []Job
@ -20,9 +25,10 @@ func TestKeepOldestOnly(t *testing.T) {
args args args args
want []Job want []Job
}{ }{
{"empty list", args{emptyList}, emptyList}, {"empty list", args{emptyList}, nil},
{"one job", args{oneJob}, oneJob}, {"one job", args{oneJob}, oneJob},
{"two jobs", args{twoJobs}, oneJob}, {"two jobs", args{twoJobs}, oneJob},
{"three jobs", args{threeJobs}, oneJob},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

110
main.go
View file

@ -1,114 +1,12 @@
//+build !test
package main package main
import ( import (
"bareos-zabbix-check/config" "bareos-zabbix-check/zabbix"
"bareos-zabbix-check/job"
"bareos-zabbix-check/spool"
"bareos-zabbix-check/state"
"fmt" "fmt"
"log"
"os"
"path/filepath"
"time"
)
const (
spoolFileName = "bareos-zabbix-check.spool"
) )
func main() { func main() {
var ( fmt.Print(zabbix.Main())
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")
}
} }

21
zabbix/flags.go Normal file
View file

@ -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
}

104
zabbix/main.go Normal file
View file

@ -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"
}

56
zabbix/main_test.go Normal file
View file

@ -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")
}

34
zabbix/statefile.go Normal file
View file

@ -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
}

BIN
zabbix/testdata/bareos-fd-17.2.state vendored Normal file

Binary file not shown.

BIN
zabbix/testdata/bareos-fd-18.2.state vendored Normal file

Binary file not shown.

Binary file not shown.

38
zabbix/workdir.go Normal file
View file

@ -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
}