gomonop/cmd/zoneserial/main.go
Emmanuel BENOîT 5432360f0e
refactor(pkg): refactor Plugin to make it easier to test
Writing the plugin's output string and exiting the program will no
longer take place in the plugin status itself. It will only be done in
the main program.
2024-07-19 23:46:33 +02:00

187 lines
6.2 KiB
Go

package zoneserial // import nocternity.net/gomonop/cmd/zoneserial
import (
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/karrick/golf"
"github.com/miekg/dns"
"nocternity.net/gomonop/pkg/perfdata"
"nocternity.net/gomonop/pkg/plugin"
"nocternity.net/gomonop/pkg/program"
)
//-------------------------------------------------------------------------------------------------------
type (
// A response to a DNS query. Includes the actual response, the RTT and the error, if any.
queryResponse struct {
data *dns.Msg
rtt time.Duration
err error
}
// A channel that can be used to send DNS query responses back to the caller.
responseChannel chan<- queryResponse
)
// Query a zone's SOA record through a given DNS and return the response using the channel.
func queryZoneSOA(dnsq *dns.Msg, hostname string, port int, output responseChannel) {
dnsc := new(dns.Client)
in, rtt, err := dnsc.Exchange(dnsq, net.JoinHostPort(hostname, strconv.Itoa(port)))
output <- queryResponse{
data: in,
rtt: rtt,
err: err,
}
}
//-------------------------------------------------------------------------------------------------------
// Command line flags that have been parsed.
type programFlags struct {
hostname string // DNS to check - hostname
port int // DNS to check - port
zone string // Zone name
rsHostname string // Reference DNS - hostname
rsPort int // Reference DNS - port
}
// Program data including configuration and runtime data.
type checkProgram struct {
programFlags // Flags from the command line
plugin *plugin.Plugin // Plugin output state
}
// Parse command line arguments and store their values. If the -h flag is present,
// help will be displayed and the program will exit.
func (flags *programFlags) parseArguments() {
var help bool
golf.BoolVarP(&help, 'h', "help", false, "Display usage information")
golf.StringVarP(&flags.hostname, 'H', "hostname", "", "Hostname of the DNS to check.")
golf.IntVarP(&flags.port, 'P', "port", 53, "Port number of the DNS to check.")
golf.StringVarP(&flags.zone, 'z', "zone", "", "Zone name.")
golf.StringVarP(&flags.rsHostname, 'r', "rs-hostname", "", "Hostname of the reference DNS.")
golf.IntVarP(&flags.rsPort, 'p', "rs-port", 53, "Port number of the reference DNS.")
golf.Parse()
if help {
golf.Usage()
os.Exit(0)
}
}
// Initialise the monitoring check program.
func NewProgram() program.Program {
program := &checkProgram{
plugin: plugin.New("DNS zone serial match check"),
}
program.parseArguments()
return program
}
// Return the program's output value.
func (program *checkProgram) Output() *plugin.Plugin {
return program.plugin
}
// Check the values that were specified from the command line. Returns true if the arguments made sense.
func (program *checkProgram) CheckArguments() bool {
if program.hostname == "" {
program.plugin.SetState(plugin.StatusUnknown, "no DNS hostname specified")
return false
}
if program.port < 1 || program.port > 65535 {
program.plugin.SetState(plugin.StatusUnknown, "invalid DNS port number")
return false
}
if program.zone == "" {
program.plugin.SetState(plugin.StatusUnknown, "no DNS zone specified")
return false
}
if program.rsHostname == "" {
program.plugin.SetState(plugin.StatusUnknown, "no reference DNS hostname specified")
return false
}
if program.rsPort < 1 || program.rsPort > 65535 {
program.plugin.SetState(plugin.StatusUnknown, "invalid reference DNS port number")
return false
}
program.hostname = strings.ToLower(program.hostname)
program.zone = strings.ToLower(program.zone)
program.rsHostname = strings.ToLower(program.rsHostname)
return true
}
// Query both the server to check and the reference server for the zone's SOA record and return both
// responses (checked server response and reference server response, respectively).
func (program *checkProgram) queryServers() (queryResponse, queryResponse) {
dnsq := new(dns.Msg)
dnsq.SetQuestion(dns.Fqdn(program.zone), dns.TypeSOA)
checkOut := make(chan queryResponse)
refOut := make(chan queryResponse)
go queryZoneSOA(dnsq, program.hostname, program.port, checkOut)
go queryZoneSOA(dnsq, program.rsHostname, program.rsPort, refOut)
var checkResponse, refResponse queryResponse
for range 2 {
select {
case m := <-checkOut:
checkResponse = m
case m := <-refOut:
refResponse = m
}
}
return checkResponse, refResponse
}
// Add a server's RTT to the performance data.
func (program *checkProgram) addRttPerf(name string, value time.Duration) {
s := fmt.Sprintf("%f", value.Seconds())
pd := perfdata.New(name, perfdata.UomSeconds, s)
program.plugin.AddPerfData(pd)
}
// Add information about one of the servers' response to the plugin output. This includes
// the error message if the query failed or the RTT performance data if it succeeded. It
// then attempts to extract the serial from a server's response and returns it if
// successful.
func (program *checkProgram) getSerial(server string, response queryResponse) (ok bool, serial uint32) {
if response.err != nil {
program.plugin.AddLinef("%s server error : %s", server, response.err)
return false, 0
}
program.addRttPerf(server+"_rtt", response.rtt)
if len(response.data.Answer) != 1 {
program.plugin.AddLine(server + " server did not return exactly one record")
return false, 0
}
if soa, ok := response.data.Answer[0].(*dns.SOA); ok {
program.plugin.AddLinef("serial on %s server: %d", server, soa.Serial)
return true, soa.Serial
}
t := reflect.TypeOf(response.data.Answer[0])
program.plugin.AddLinef("%s server did not return SOA record; record type: %v", server, t)
return false, 0
}
// Run the monitoring check. This implies querying both servers, extracting the serial from
// their responses, then comparing the serials.
func (program *checkProgram) RunCheck() {
checkResponse, refResponse := program.queryServers()
cOk, cSerial := program.getSerial("checked", checkResponse)
rOk, rSerial := program.getSerial("reference", refResponse)
if !(cOk && rOk) {
program.plugin.SetState(plugin.StatusUnknown, "could not read serials")
return
}
if cSerial == rSerial {
program.plugin.SetState(plugin.StatusOK, "serials match")
} else {
program.plugin.SetState(plugin.StatusCritical, "serials mismatch")
}
}