diff --git a/.gitignore b/.gitignore index 5b90e79..d5d66b1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ *.so *.dylib +# Build files +build/ + # Test binary, built with `go test -c` *.test diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f869263 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.faercol.me/faercol/dnsmasq-netbox-connector + +go 1.23.1 + +require ( + git.faercol.me/faercol/go-healthchecks v0.0.0-20241003132603-97f47d027454 // indirect + github.com/google/uuid v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7a6ab87 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +git.faercol.me/faercol/go-healthchecks v0.0.0-20241002141124-24c3d5c5b25a h1:/TuqUOdAUxHQehPOY7zOUMLvQc3W60xXUZWieYiCJyY= +git.faercol.me/faercol/go-healthchecks v0.0.0-20241002141124-24c3d5c5b25a/go.mod h1:d69XefG7I7Y7XyRajFoNpeyWgt89+OFvxIqjlJwia1E= +git.faercol.me/faercol/go-healthchecks v0.0.0-20241003132603-97f47d027454 h1:sWDu4XNMwODSzKjmIZOIWLPhEOhReRUVpo4fEVywp6g= +git.faercol.me/faercol/go-healthchecks v0.0.0-20241003132603-97f47d027454/go.mod h1:d69XefG7I7Y7XyRajFoNpeyWgt89+OFvxIqjlJwia1E= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d313559 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,44 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" +) + +type HealthchecksConfig struct { + Enabled bool `json:"enabled"` + HostPingAPI string `json:"host_ping_api"` + PingKey string `json:"ping_key"` +} + +type Config struct { + HealthchecksConfig `json:"healthchecks"` + LeasesPath string `json:"leases_path"` +} + +func defaultConfig() *Config { + return &Config{ + HealthchecksConfig: HealthchecksConfig{ + Enabled: false, + }, + LeasesPath: "/var/lib/misc/dnsmasq.leases", + } +} + +func New(path string) (*Config, error) { + fileContent, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return defaultConfig(), nil + } + return nil, fmt.Errorf("failed to read config file %w", err) + } + + var c Config + if err := json.Unmarshal(fileContent, &c); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + return &c, nil +} diff --git a/internal/dnsmasq/dnsmasq.go b/internal/dnsmasq/dnsmasq.go new file mode 100644 index 0000000..71f3fc5 --- /dev/null +++ b/internal/dnsmasq/dnsmasq.go @@ -0,0 +1,61 @@ +package dnsmasq + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" +) + +type Lease struct { + ExpireDate time.Time + Mac string + IP string + Hostname string + ClientID string +} + +func (l Lease) String() string { + return fmt.Sprintf("[expire on %v]: host %s IP %s (mac %s - clientID %s)", l.ExpireDate, l.Hostname, l.IP, l.Mac, l.ClientID) +} + +func parseLeaseLine(rawLine string) (*Lease, error) { + lineValues := strings.Split(rawLine, " ") + if len(lineValues) != 5 { + return nil, fmt.Errorf("unexpected number of values, expected 5, got %d", len(lineValues)) + } + + expireTimeInt, err := strconv.Atoi(lineValues[0]) + if err != nil { + return nil, fmt.Errorf("unexpected unix timestamp value %s", lineValues[0]) + } + + return &Lease{ + ExpireDate: time.Unix(int64(expireTimeInt), 0), + Mac: lineValues[1], + IP: lineValues[2], + Hostname: lineValues[3], + ClientID: lineValues[4], + }, nil +} + +func GetLeases(leasesPath string) ([]*Lease, error) { + fileContent, err := os.ReadFile(leasesPath) + if err != nil { + return nil, fmt.Errorf("failed to read leases file: %w", err) + } + + leases := []*Lease{} + for i, l := range strings.Split(string(fileContent), "\n") { + if l == "" { + continue + } + lease, err := parseLeaseLine(l) + if err != nil { + return nil, fmt.Errorf("failed to parse line %d: %w", i, err) + } + leases = append(leases, lease) + } + return leases, nil +} diff --git a/internal/healthchecks/healthchecks.go b/internal/healthchecks/healthchecks.go new file mode 100644 index 0000000..95295f8 --- /dev/null +++ b/internal/healthchecks/healthchecks.go @@ -0,0 +1,38 @@ +package healthchecks + +import ( + "context" + "fmt" + + "git.faercol.me/faercol/dnsmasq-netbox-connector/internal/config" + go_healthchecks "git.faercol.me/faercol/go-healthchecks" + "github.com/google/uuid" +) + +func Start(ctx context.Context, conf config.HealthchecksConfig, clt go_healthchecks.PingClient) (go_healthchecks.Check, error) { + if !conf.Enabled { + return nil, nil + } + + runID := uuid.New() + check, err := go_healthchecks.NewSlugCheck(conf.PingKey, "dnsmasq-netbox-connector", true, runID.String()) + if err != nil { + return nil, fmt.Errorf("failed to create check: %w", err) + } + + return check, clt.ReportStart(ctx, check) +} + +func Success(ctx context.Context, conf config.HealthchecksConfig, clt go_healthchecks.PingClient, check go_healthchecks.Check) error { + if !conf.Enabled { + return nil + } + return clt.ReportSuccess(ctx, check) +} + +func Failure(ctx context.Context, conf config.HealthchecksConfig, clt go_healthchecks.PingClient, check go_healthchecks.Check, msg string) error { + if !conf.Enabled { + return nil + } + return clt.ReportFailure(ctx, check, msg) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..342d28f --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "fmt" + "os" + + "git.faercol.me/faercol/dnsmasq-netbox-connector/internal/config" + "git.faercol.me/faercol/dnsmasq-netbox-connector/internal/dnsmasq" + "git.faercol.me/faercol/dnsmasq-netbox-connector/internal/healthchecks" + gohealthchecks "git.faercol.me/faercol/go-healthchecks" +) + +func main() { + configPath := "/etc/dnmasq-netbox/config.json" + + if len(os.Args) > 1 { + configPath = os.Args[1] + } + + conf, err := config.New(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read configuration: %s\n", err) + os.Exit(1) + } + + healtchecksClt := gohealthchecks.NewPingClient(conf.HostPingAPI) + + check, err := healthchecks.Start(context.Background(), conf.HealthchecksConfig, healtchecksClt) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to notify process start: %s\n", err) + } + + leases, err := dnsmasq.GetLeases(conf.LeasesPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse leases: %s\n", err) + if err := healthchecks.Failure(context.Background(), conf.HealthchecksConfig, healtchecksClt, check, "failed to parse lease file"); err != nil { + fmt.Fprintf(os.Stderr, "Failed to notify failure: %s\n", err) + } + os.Exit(1) + } + + for _, l := range leases { + fmt.Printf("Got lease %s\n", l) + } + + if err := healthchecks.Success(context.Background(), conf.HealthchecksConfig, healtchecksClt, check); err != nil { + fmt.Fprintf(os.Stderr, "Failed to notify process success: %s\n", err) + } +}