aboutsummaryrefslogtreecommitdiff
path: root/state.go
blob: 9c5ac49093abcfc242b65f3d925ad5b0138529d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"regexp"
	"time"
)

// stateFileHeader : A structure to hold the header of the state file. It is statically aligned for amd64 architecture
// This comes from bareos repository file core/src/lib/bsys.cc:525 and core/src/lib/bsys.cc:652
type stateFileHeader struct {
	ID                        [14]byte
	_                         int16
	Version                   int32
	_                         int32
	LastJobsAddr              uint64
	EndOfRecentJobResultsList uint64
	Reserved                  [19]uint64
}

func (sfh stateFileHeader) String() string {
	return fmt.Sprintf("ID: \"%s\", Version: %d, LastJobsAddr: %d, EndOfRecentJobResultsList: %d", sfh.ID[:len(sfh.ID)-2], sfh.Version, sfh.EndOfRecentJobResultsList, sfh.Reserved)
}

// jobEntry : A structure to hold a job result from the state file
// This comes from bareos repository file core/src/lib/recent_job_results_list.h:29 and file core/src/lib/recent_job_results_list.cc:44
type jobEntry struct {
	Pad            [16]byte
	Errors         int32
	JobType        int32
	JobStatus      int32
	JobLevel       int32
	JobID          uint32
	VolSessionID   uint32
	VolSessionTime uint32
	JobFiles       uint32
	JobBytes       uint64
	StartTime      uint64
	EndTime        uint64
	Job            [maxNameLength]byte
}

func (je jobEntry) String() string {
	var matches = jobNameRegex.FindSubmatchIndex(je.Job[:])
	var jobNameLen int
	if len(matches) >= 4 {
		jobNameLen = matches[3]
	}
	return fmt.Sprintf("Errors: %d, JobType: %c, JobStatus: %c, JobLevel: %c, JobID: %d, VolSessionID: %d, VolSessionTime: %d, JobFiles: %d, JobBytes: %d, StartTime: %s, EndTime: %s, Job: %s",
		je.Errors, je.JobType, je.JobStatus, je.JobLevel, je.JobID, je.VolSessionID, je.VolSessionTime, je.JobFiles, je.JobBytes, time.Unix(int64(je.StartTime), 0), time.Unix(int64(je.EndTime), 0), je.Job[:jobNameLen])
}

const (
	// maxNameLength : the maximum length of a string, hard coded in bareos
	maxNameLength = 128
	// stateFileHeaderLength : the length of the state file header struct
	stateFileHeaderLength = 14 + 2 + 4 + 4 + 8 + 8 + 19*8
	// jobResultLength : the length of the job result struct
	jobResultLength = 16 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 8 + 8 + 8 + maxNameLength
)

var jobNameRegex = regexp.MustCompilePOSIX(`^([-A-Za-z0-9_]+)\.[0-9]{4}-[0-9]{2}-[0-9]{2}.*`)

// readNextBytes : Reads the next "number" bytes from a "file", returns the number of bytes actually read as well as the bytes read
func readNextBytes(file *os.File, number int) (n int, bytes []byte, err error) {
	bytes = make([]byte, number)
	n, err = file.Read(bytes)
	if err != nil {
		return 0, nil, fmt.Errorf("file.Read failed in %s : %s", stateFile, err)
	}

	return
}

func parseStateFile() (successfulJobs jobs, errorJobs jobs, err error) {
	var (
		n               int
		stateFileHandle *os.File
		data            []byte
		buffer          *bytes.Buffer
		numberOfJobs    uint32
		matches         []int
	)
	// Open the state file
	stateFileHandle, err = os.Open(stateFile)
	if err != nil {
		return nil, nil, fmt.Errorf("INFO Couldn't open state file : %s", err)
	}
	defer stateFileHandle.Close()

	// Parsing the state file header
	var header stateFileHeader
	n, data, err = readNextBytes(stateFileHandle, stateFileHeaderLength)
	if err != nil {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : %s", err)
	}
	if n != stateFileHeaderLength {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : invalid header length in %s", stateFile)
	}
	buffer = bytes.NewBuffer(data)
	err = binary.Read(buffer, binary.LittleEndian, &header)
	if err != nil {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : binary.Read failed on header in %s : %s", stateFile, err)
	}
	if verbose {
		log.Printf("Parsed header: %+s\n", header)
	}
	if id := string(header.ID[:len(header.ID)-1]); id != "Bareos State\n" && id != "Bacula State\n" {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : Not a bareos or bacula state file %s", stateFile)
	}
	if header.Version != 4 {
		return nil, nil, fmt.Errorf("INFO Invalid state file : This script only supports bareos state file version 4, got %d", header.Version)
	}
	if header.LastJobsAddr == 0 {
		return nil, nil, fmt.Errorf("INFO No jobs exist in the state file")
	}

	// We seek to the jobs position in the state file
	stateFileHandle.Seek(int64(header.LastJobsAddr), 0)

	// We read how many jobs there are in the state file
	n, data, err = readNextBytes(stateFileHandle, 4)
	if err != nil {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : %s", err)
	}
	if n != 4 {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : invalid numberOfJobs read length in %s", stateFile)
	}
	buffer = bytes.NewBuffer(data)
	err = binary.Read(buffer, binary.LittleEndian, &numberOfJobs)
	if err != nil {
		return nil, nil, fmt.Errorf("INFO Corrupted state file : binary.Read failed on numberOfJobs in %s : %s", stateFile, err)
	}
	if verbose {
		log.Printf("%d jobs found in state file\n", numberOfJobs)
	}

	// We parse the job entries
	successfulJobs = make(map[string]uint64)
	errorJobs = make(map[string]uint64)
	for ; numberOfJobs > 0; numberOfJobs-- {
		var (
			jobResult jobEntry
			jobName   string
		)
		n, data, err = readNextBytes(stateFileHandle, jobResultLength)
		if err != nil {
			return nil, nil, fmt.Errorf("INFO Corrupted state file : %s", err)
		}
		if n != jobResultLength {
			return nil, nil, fmt.Errorf("INFO Corrupted state file : invalid job entry in %s", stateFile)
		}
		buffer = bytes.NewBuffer(data)
		err = binary.Read(buffer, binary.LittleEndian, &jobResult)
		if err != nil {
			return nil, nil, fmt.Errorf("INFO Corrupted state file : binary.Read failed on job entry in %s : %s", stateFile, err)
		}
		matches = jobNameRegex.FindSubmatchIndex(jobResult.Job[:])
		if len(matches) >= 4 {
			jobName = string(jobResult.Job[:matches[3]])
		} else {
			return nil, nil, fmt.Errorf("INFO Couldn't parse job name, this shouldn't happen : %s", jobResult.Job[:])
		}
		if verbose {
			log.Printf("Parsed job entry: %s\n", jobResult)
		}
		// If the job is of type backup (B == ascii 66)
		if jobResult.JobType == 66 {
			var (
				successExists  bool
				errorExists    bool
				currentSuccess uint64
				currentError   uint64
			)
			currentSuccess, successExists = successfulJobs[jobName]
			currentError, errorExists = errorJobs[jobName]
			// If the job is of status success (T == ascii 84)
			if jobResult.JobStatus == 84 {
				// if there is an older entry in errorJobs we delete it
				if errorExists && jobResult.StartTime > currentError {
					delete(errorJobs, jobName)
				}
				// if there are no entries more recent in successfulJobs we add this one
				if !successExists || successExists && jobResult.StartTime > currentSuccess {
					successfulJobs[jobName] = jobResult.StartTime
				}
			} else {
				if !errorExists || jobResult.StartTime > currentError {
					errorJobs[jobName] = jobResult.StartTime
				}
			}
		}
	}
	return
}