summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/charts.go111
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/collect.go166
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/config_schema.json50
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp.go111
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp_test.go149
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/init.go21
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md205
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/metadata.yaml151
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/parse_configuration.go384
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.json6
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.yaml4
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.conf77
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/.dnsmasq.conf1
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv4.any10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv6.any10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv4.any10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv6.any10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/~dnsmasq.conf1
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasq.bak1
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv4.any10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv6.any3
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasq.other1
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv4.conf10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv6.conf10
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.leases19
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq2.conf6
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq3.conf4
28 files changed, 1542 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/README.md b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/README.md
new file mode 120000
index 000000000..ad22eb4ee
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/README.md
@@ -0,0 +1 @@
+integrations/dnsmasq_dhcp.md \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/charts.go b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/charts.go
new file mode 100644
index 000000000..39ac0024f
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/charts.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsmasq_dhcp
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioDHCPRangeUtilization = module.Priority + iota
+ prioDHCPRangeAllocatesLeases
+ prioDHCPRanges
+ prioDHCPHosts
+)
+
+var charts = module.Charts{
+ {
+ ID: "dhcp_ranges",
+ Title: "Number of DHCP Ranges",
+ Units: "ranges",
+ Fam: "dhcp ranges",
+ Ctx: "dnsmasq_dhcp.dhcp_ranges",
+ Type: module.Stacked,
+ Priority: prioDHCPRanges,
+ Dims: module.Dims{
+ {ID: "ipv4_dhcp_ranges", Name: "ipv4"},
+ {ID: "ipv6_dhcp_ranges", Name: "ipv6"},
+ },
+ },
+ {
+ ID: "dhcp_hosts",
+ Title: "Number of DHCP Hosts",
+ Units: "hosts",
+ Fam: "dhcp hosts",
+ Ctx: "dnsmasq_dhcp.dhcp_host",
+ Type: module.Stacked,
+ Priority: prioDHCPHosts,
+ Dims: module.Dims{
+ {ID: "ipv4_dhcp_hosts", Name: "ipv4"},
+ {ID: "ipv6_dhcp_hosts", Name: "ipv6"},
+ },
+ },
+}
+
+var (
+ chartsTmpl = module.Charts{
+ chartTmplDHCPRangeUtilization.Copy(),
+ chartTmplDHCPRangeAllocatedLeases.Copy(),
+ }
+)
+
+var (
+ chartTmplDHCPRangeUtilization = module.Chart{
+ ID: "dhcp_range_%s_utilization",
+ Title: "DHCP Range utilization",
+ Units: "percentage",
+ Fam: "dhcp range utilization",
+ Ctx: "dnsmasq_dhcp.dhcp_range_utilization",
+ Type: module.Area,
+ Priority: prioDHCPRangeUtilization,
+ Dims: module.Dims{
+ {ID: "dhcp_range_%s_utilization", Name: "used"},
+ },
+ }
+ chartTmplDHCPRangeAllocatedLeases = module.Chart{
+ ID: "dhcp_range_%s_allocated_leases",
+ Title: "DHCP Range Allocated Leases",
+ Units: "leases",
+ Fam: "dhcp range leases",
+ Ctx: "dnsmasq_dhcp.dhcp_range_allocated_leases",
+ Priority: prioDHCPRangeAllocatesLeases,
+ Dims: module.Dims{
+ {ID: "dhcp_range_%s_allocated_leases", Name: "leases"},
+ },
+ }
+)
+
+func newDHCPRangeCharts(dhcpRange string) *module.Charts {
+ charts := chartsTmpl.Copy()
+
+ for _, c := range *charts {
+ c.ID = fmt.Sprintf(c.ID, dhcpRange)
+ c.Labels = []module.Label{
+ {Key: "dhcp_range", Value: dhcpRange},
+ }
+ for _, d := range c.Dims {
+ d.ID = fmt.Sprintf(d.ID, dhcpRange)
+ }
+ }
+ return charts
+}
+
+func (d *DnsmasqDHCP) addDHCPRangeCharts(dhcpRange string) {
+ charts := newDHCPRangeCharts(dhcpRange)
+ if err := d.Charts().Add(*charts...); err != nil {
+ d.Warning(err)
+ }
+}
+
+func (d *DnsmasqDHCP) removeDHCPRangeCharts(dhcpRange string) {
+ p := "dhcp_range_" + dhcpRange
+ for _, c := range *d.Charts() {
+ if strings.HasSuffix(c.ID, p) {
+ c.MarkRemove()
+ c.MarkNotCreated()
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/collect.go b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/collect.go
new file mode 100644
index 000000000..4d3e3ac5e
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/collect.go
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsmasq_dhcp
+
+import (
+ "bufio"
+ "io"
+ "math"
+ "math/big"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/iprange"
+)
+
+func (d *DnsmasqDHCP) collect() (map[string]int64, error) {
+ now := time.Now()
+ var updated bool
+
+ if now.Sub(d.parseConfigTime) > d.parseConfigEvery {
+ d.parseConfigTime = now
+
+ dhcpRanges, dhcpHosts := d.parseDnsmasqDHCPConfiguration()
+ d.dhcpRanges, d.dhcpHosts = dhcpRanges, dhcpHosts
+ updated = d.updateCharts()
+
+ d.collectV4V6Stats()
+ }
+
+ f, err := os.Open(d.LeasesPath)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = f.Close() }()
+
+ if !updated {
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+
+ if d.leasesModTime.Equal(fi.ModTime()) {
+ d.Debug("lease database file modification time has not changed, old data is returned")
+ return d.mx, nil
+ }
+
+ d.Debug("leases db file modification time has changed, reading it")
+ d.leasesModTime = fi.ModTime()
+ }
+
+ leases := findLeases(f)
+ d.collectRangesStats(leases)
+
+ return d.mx, nil
+}
+
+func (d *DnsmasqDHCP) collectV4V6Stats() {
+ d.mx["ipv4_dhcp_ranges"], d.mx["ipv6_dhcp_ranges"] = 0, 0
+ for _, r := range d.dhcpRanges {
+ if r.Family() == iprange.V6Family {
+ d.mx["ipv6_dhcp_ranges"]++
+ } else {
+ d.mx["ipv4_dhcp_ranges"]++
+ }
+ }
+
+ d.mx["ipv4_dhcp_hosts"], d.mx["ipv6_dhcp_hosts"] = 0, 0
+ for _, ip := range d.dhcpHosts {
+ if ip.To4() == nil {
+ d.mx["ipv6_dhcp_hosts"]++
+ } else {
+ d.mx["ipv4_dhcp_hosts"]++
+ }
+ }
+}
+
+func (d *DnsmasqDHCP) collectRangesStats(leases []net.IP) {
+ for _, r := range d.dhcpRanges {
+ d.mx["dhcp_range_"+r.String()+"_allocated_leases"] = 0
+ d.mx["dhcp_range_"+r.String()+"_utilization"] = 0
+ }
+
+ for _, ip := range leases {
+ for _, r := range d.dhcpRanges {
+ if r.Contains(ip) {
+ d.mx["dhcp_range_"+r.String()+"_allocated_leases"]++
+ break
+ }
+ }
+ }
+
+ for _, ip := range d.dhcpHosts {
+ for _, r := range d.dhcpRanges {
+ if r.Contains(ip) {
+ d.mx["dhcp_range_"+r.String()+"_allocated_leases"]++
+ break
+ }
+ }
+ }
+
+ for _, r := range d.dhcpRanges {
+ name := "dhcp_range_" + r.String() + "_allocated_leases"
+ numOfIps, ok := d.mx[name]
+ if !ok {
+ d.mx[name] = 0
+ }
+ d.mx["dhcp_range_"+r.String()+"_utilization"] = int64(math.Round(calcPercent(numOfIps, r.Size())))
+ }
+}
+
+func (d *DnsmasqDHCP) updateCharts() bool {
+ var updated bool
+ seen := make(map[string]bool)
+ for _, r := range d.dhcpRanges {
+ seen[r.String()] = true
+ if !d.cacheDHCPRanges[r.String()] {
+ d.cacheDHCPRanges[r.String()] = true
+ d.addDHCPRangeCharts(r.String())
+ updated = true
+ }
+ }
+
+ for v := range d.cacheDHCPRanges {
+ if !seen[v] {
+ delete(d.cacheDHCPRanges, v)
+ d.removeDHCPRangeCharts(v)
+ updated = true
+ }
+ }
+ return updated
+}
+
+func findLeases(r io.Reader) []net.IP {
+ /*
+ 1560300536 08:00:27:61:3c:ee 2.2.2.3 debian8 *
+ duid 00:01:00:01:24:90:cf:5b:08:00:27:61:2e:2c
+ 1560300414 660684014 1234::20b * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
+ */
+ var ips []net.IP
+ s := bufio.NewScanner(r)
+
+ for s.Scan() {
+ parts := strings.Fields(s.Text())
+ if len(parts) != 5 {
+ continue
+ }
+
+ ip := net.ParseIP(parts[2])
+ if ip == nil {
+ continue
+ }
+ ips = append(ips, ip)
+ }
+
+ return ips
+}
+
+func calcPercent(ips int64, hosts *big.Int) float64 {
+ h := hosts.Int64()
+ if ips == 0 || h == 0 || !hosts.IsInt64() {
+ return 0
+ }
+ return float64(ips) * 100 / float64(h)
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/config_schema.json b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/config_schema.json
new file mode 100644
index 000000000..f51a3b2a2
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/config_schema.json
@@ -0,0 +1,50 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Dnsmasq DHCP collector configuration.",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "leases_path": {
+ "title": "Leases file",
+ "description": "File path to the Dnsmasq DHCP server's lease database.",
+ "type": "string",
+ "default": "/var/lib/misc/dnsmasq.leases",
+ "pattern": "^$|^/"
+ },
+ "conf_path": {
+ "title": "Config file",
+ "description": "File path for the Dnsmasq configuration. Used to find all configured DHCP ranges.",
+ "type": "string",
+ "default": "/etc/dnsmasq.conf",
+ "pattern": "^$|^/"
+ },
+ "conf_dir": {
+ "title": "Config directory",
+ "description": "Directory path for Dnsmasq configurations. The syntax follows the same format as the [--conf-dir](https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) option.",
+ "type": "string",
+ "default": "/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new",
+ "pattern": "^$|^/"
+ }
+ },
+ "required": [
+ "leases_path",
+ "conf_path"
+ ],
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp.go b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp.go
new file mode 100644
index 000000000..45ddacf46
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsmasq_dhcp
+
+import (
+ _ "embed"
+ "errors"
+ "net"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/iprange"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("dnsmasq_dhcp", module.Creator{
+ JobConfigSchema: configSchema,
+ Create: func() module.Module { return New() },
+ Config: func() any { return &Config{} },
+ })
+}
+
+func New() *DnsmasqDHCP {
+ return &DnsmasqDHCP{
+ Config: Config{
+ // debian defaults
+ LeasesPath: "/var/lib/misc/dnsmasq.leases",
+ ConfPath: "/etc/dnsmasq.conf",
+ ConfDir: "/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new",
+ },
+ charts: charts.Copy(),
+ parseConfigEvery: time.Minute,
+ cacheDHCPRanges: make(map[string]bool),
+ mx: make(map[string]int64),
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
+ LeasesPath string `yaml:"leases_path" json:"leases_path"`
+ ConfPath string `yaml:"conf_path,omitempty" json:"conf_path"`
+ ConfDir string `yaml:"conf_dir,omitempty" json:"conf_dir"`
+}
+
+type DnsmasqDHCP struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ leasesModTime time.Time
+ parseConfigTime time.Time
+ parseConfigEvery time.Duration
+ dhcpRanges []iprange.Range
+ dhcpHosts []net.IP
+ cacheDHCPRanges map[string]bool
+
+ mx map[string]int64
+}
+
+func (d *DnsmasqDHCP) Configuration() any {
+ return d.Config
+}
+
+func (d *DnsmasqDHCP) Init() error {
+ if err := d.validateConfig(); err != nil {
+ d.Errorf("config validation: %v", err)
+ return err
+ }
+ if err := d.checkLeasesPath(); err != nil {
+ d.Errorf("leases path check: %v", err)
+ return err
+ }
+
+ return nil
+}
+
+func (d *DnsmasqDHCP) Check() error {
+ mx, err := d.collect()
+ if err != nil {
+ d.Error(err)
+ return err
+ }
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+
+ }
+ return nil
+}
+
+func (d *DnsmasqDHCP) Charts() *module.Charts {
+ return d.charts
+}
+
+func (d *DnsmasqDHCP) Collect() map[string]int64 {
+ mx, err := d.collect()
+ if err != nil {
+ d.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+
+ return mx
+}
+
+func (d *DnsmasqDHCP) Cleanup() {}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp_test.go b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp_test.go
new file mode 100644
index 000000000..b83b6a3f5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/dhcp_test.go
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsmasq_dhcp
+
+import (
+ "os"
+ "testing"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+)
+
+const (
+ testLeasesPath = "testdata/dnsmasq.leases"
+ testConfPath = "testdata/dnsmasq.conf"
+ testConfDir = "testdata/dnsmasq.d"
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+ } {
+ require.NotNil(t, data, name)
+ }
+}
+
+func TestDnsmasqDHCP_ConfigurationSerialize(t *testing.T) {
+ module.TestConfigurationSerialize(t, &DnsmasqDHCP{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestDnsmasqDHCP_Init(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.ConfPath = testConfPath
+ job.ConfDir = testConfDir
+
+ assert.NoError(t, job.Init())
+}
+
+func TestDnsmasqDHCP_InitEmptyLeasesPath(t *testing.T) {
+ job := New()
+ job.LeasesPath = ""
+
+ assert.Error(t, job.Init())
+}
+
+func TestDnsmasqDHCP_InitInvalidLeasesPath(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.LeasesPath += "!"
+
+ assert.Error(t, job.Init())
+}
+
+func TestDnsmasqDHCP_InitZeroDHCPRanges(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.ConfPath = "testdata/dnsmasq3.conf"
+ job.ConfDir = ""
+
+ assert.NoError(t, job.Init())
+}
+
+func TestDnsmasqDHCP_Check(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.ConfPath = testConfPath
+ job.ConfDir = testConfDir
+
+ require.NoError(t, job.Init())
+ assert.NoError(t, job.Check())
+}
+
+func TestDnsmasqDHCP_Charts(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.ConfPath = testConfPath
+ job.ConfDir = testConfDir
+
+ require.NoError(t, job.Init())
+
+ assert.NotNil(t, job.Charts())
+}
+
+func TestDnsmasqDHCP_Cleanup(t *testing.T) {
+ assert.NotPanics(t, New().Cleanup)
+}
+
+func TestDnsmasqDHCP_Collect(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.ConfPath = testConfPath
+ job.ConfDir = testConfDir
+
+ require.NoError(t, job.Init())
+ require.NoError(t, job.Check())
+
+ expected := map[string]int64{
+ "dhcp_range_1230::1-1230::64_allocated_leases": 7,
+ "dhcp_range_1230::1-1230::64_utilization": 7,
+ "dhcp_range_1231::1-1231::64_allocated_leases": 1,
+ "dhcp_range_1231::1-1231::64_utilization": 1,
+ "dhcp_range_1232::1-1232::64_allocated_leases": 1,
+ "dhcp_range_1232::1-1232::64_utilization": 1,
+ "dhcp_range_1233::1-1233::64_allocated_leases": 1,
+ "dhcp_range_1233::1-1233::64_utilization": 1,
+ "dhcp_range_1234::1-1234::64_allocated_leases": 1,
+ "dhcp_range_1234::1-1234::64_utilization": 1,
+ "dhcp_range_192.168.0.1-192.168.0.100_allocated_leases": 6,
+ "dhcp_range_192.168.0.1-192.168.0.100_utilization": 6,
+ "dhcp_range_192.168.1.1-192.168.1.100_allocated_leases": 5,
+ "dhcp_range_192.168.1.1-192.168.1.100_utilization": 5,
+ "dhcp_range_192.168.2.1-192.168.2.100_allocated_leases": 4,
+ "dhcp_range_192.168.2.1-192.168.2.100_utilization": 4,
+ "dhcp_range_192.168.200.1-192.168.200.100_allocated_leases": 1,
+ "dhcp_range_192.168.200.1-192.168.200.100_utilization": 1,
+ "dhcp_range_192.168.3.1-192.168.3.100_allocated_leases": 1,
+ "dhcp_range_192.168.3.1-192.168.3.100_utilization": 1,
+ "dhcp_range_192.168.4.1-192.168.4.100_allocated_leases": 1,
+ "dhcp_range_192.168.4.1-192.168.4.100_utilization": 1,
+ "ipv4_dhcp_hosts": 6,
+ "ipv4_dhcp_ranges": 6,
+ "ipv6_dhcp_hosts": 5,
+ "ipv6_dhcp_ranges": 5,
+ }
+
+ assert.Equal(t, expected, job.Collect())
+}
+
+func TestDnsmasqDHCP_CollectFailedToOpenLeasesPath(t *testing.T) {
+ job := New()
+ job.LeasesPath = testLeasesPath
+ job.ConfPath = testConfPath
+ job.ConfDir = testConfDir
+
+ require.NoError(t, job.Init())
+ require.NoError(t, job.Check())
+
+ job.LeasesPath = ""
+ assert.Nil(t, job.Collect())
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/init.go b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/init.go
new file mode 100644
index 000000000..6c74674a3
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/init.go
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsmasq_dhcp
+
+import "errors"
+
+func (d *DnsmasqDHCP) validateConfig() error {
+ if d.LeasesPath == "" {
+ return errors.New("empty 'leases_path'")
+ }
+ return nil
+}
+
+func (d *DnsmasqDHCP) checkLeasesPath() error {
+ f, err := openFile(d.LeasesPath)
+ if err != nil {
+ return err
+ }
+ _ = f.Close()
+ return nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md
new file mode 100644
index 000000000..23eb07388
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md
@@ -0,0 +1,205 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/README.md"
+meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/metadata.yaml"
+sidebar_label: "Dnsmasq DHCP"
+learn_status: "Published"
+learn_rel_path: "Collecting Metrics/DNS and DHCP Servers"
+most_popular: False
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
+endmeta-->
+
+# Dnsmasq DHCP
+
+
+<img src="https://netdata.cloud/img/dnsmasq.svg" width="150"/>
+
+
+Plugin: go.d.plugin
+Module: dnsmasq_dhcp
+
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
+
+## Overview
+
+This collector monitors Dnsmasq DHCP leases databases, depending on your configuration.
+
+By default, it uses:
+
+- `/var/lib/misc/dnsmasq.leases` to read leases.
+- `/etc/dnsmasq.conf` to detect dhcp-ranges.
+- `/etc/dnsmasq.d` to find additional configurations.
+
+
+
+
+This collector is supported on all platforms.
+
+This collector only supports collecting metrics from a single instance of this integration.
+
+
+### Default Behavior
+
+#### Auto-Detection
+
+All configured dhcp-ranges are detected automatically
+
+
+#### Limits
+
+The default configuration for this integration does not impose any limits on data collection.
+
+#### Performance Impact
+
+The default configuration for this integration is not expected to impose a significant performance impact on the system.
+
+
+## Metrics
+
+Metrics grouped by *scope*.
+
+The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
+
+
+
+### Per Dnsmasq DHCP instance
+
+These metrics refer to the entire monitored application.
+
+This scope has no labels.
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| dnsmasq_dhcp.dhcp_ranges | ipv4, ipv6 | ranges |
+| dnsmasq_dhcp.dhcp_hosts | ipv4, ipv6 | hosts |
+
+### Per dhcp range
+
+These metrics refer to the DHCP range.
+
+Labels:
+
+| Label | Description |
+|:-----------|:----------------|
+| dhcp_range | DHCP range in `START_IP:END_IP` format |
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| dnsmasq_dhcp.dhcp_range_utilization | used | percentage |
+| dnsmasq_dhcp.dhcp_range_allocated_leases | allocated | leases |
+
+
+
+## Alerts
+
+
+The following alerts are available:
+
+| Alert name | On metric | Description |
+|:------------|:----------|:------------|
+| [ dnsmasq_dhcp_dhcp_range_utilization ](https://github.com/netdata/netdata/blob/master/src/health/health.d/dnsmasq_dhcp.conf) | dnsmasq_dhcp.dhcp_range_utilization | DHCP range utilization |
+
+
+## Setup
+
+### Prerequisites
+
+No action required.
+
+### Configuration
+
+#### File
+
+The configuration file name for this integration is `go.d/dnsmasq_dhcp.conf`.
+
+
+You can edit the configuration file using the `edit-config` script from the
+Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory).
+
+```bash
+cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
+sudo ./edit-config go.d/dnsmasq_dhcp.conf
+```
+#### Options
+
+The following options can be defined globally: update_every, autodetection_retry.
+
+
+<details open><summary>Config options</summary>
+
+| Name | Description | Default | Required |
+|:----|:-----------|:-------|:--------:|
+| update_every | Data collection frequency. | 1 | no |
+| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no |
+| leases_path | Path to dnsmasq DHCP leases file. | /var/lib/misc/dnsmasq.leases | no |
+| conf_path | Path to dnsmasq configuration file. | /etc/dnsmasq.conf | no |
+| conf_dir | Path to dnsmasq configuration directory. | /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new | no |
+
+</details>
+
+#### Examples
+
+##### Basic
+
+An example configuration.
+
+<details open><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: dnsmasq_dhcp
+ leases_path: /var/lib/misc/dnsmasq.leases
+ conf_path: /etc/dnsmasq.conf
+ conf_dir: /etc/dnsmasq.d
+
+```
+</details>
+
+##### Pi-hole
+
+Dnsmasq DHCP on Pi-hole.
+
+<details open><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: dnsmasq_dhcp
+ leases_path: /etc/pihole/dhcp.leases
+ conf_path: /etc/dnsmasq.conf
+ conf_dir: /etc/dnsmasq.d
+
+```
+</details>
+
+
+
+## Troubleshooting
+
+### Debug Mode
+
+To troubleshoot issues with the `dnsmasq_dhcp` collector, run the `go.d.plugin` with the debug option enabled. The output
+should give you clues as to why the collector isn't working.
+
+- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on
+ your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`.
+
+ ```bash
+ cd /usr/libexec/netdata/plugins.d/
+ ```
+
+- Switch to the `netdata` user.
+
+ ```bash
+ sudo -u netdata -s
+ ```
+
+- Run the `go.d.plugin` to debug the collector:
+
+ ```bash
+ ./go.d.plugin -d -m dnsmasq_dhcp
+ ```
+
+
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/metadata.yaml b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/metadata.yaml
new file mode 100644
index 000000000..13b73336c
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/metadata.yaml
@@ -0,0 +1,151 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-dnsmasq_dhcp
+ plugin_name: go.d.plugin
+ module_name: dnsmasq_dhcp
+ monitored_instance:
+ name: Dnsmasq DHCP
+ link: https://www.thekelleys.org.uk/dnsmasq/doc.html
+ icon_filename: dnsmasq.svg
+ categories:
+ - data-collection.dns-and-dhcp-servers
+ keywords:
+ - dnsmasq
+ - dhcp
+ related_resources:
+ integrations:
+ list: []
+ info_provided_to_referring_integrations:
+ description: ""
+ most_popular: false
+ overview:
+ data_collection:
+ metrics_description: |
+ This collector monitors Dnsmasq DHCP leases databases, depending on your configuration.
+
+ By default, it uses:
+
+ - `/var/lib/misc/dnsmasq.leases` to read leases.
+ - `/etc/dnsmasq.conf` to detect dhcp-ranges.
+ - `/etc/dnsmasq.d` to find additional configurations.
+ method_description: ""
+ supported_platforms:
+ include: []
+ exclude: []
+ multi_instance: false
+ additional_permissions:
+ description: ""
+ default_behavior:
+ auto_detection:
+ description: |
+ All configured dhcp-ranges are detected automatically
+ limits:
+ description: ""
+ performance_impact:
+ description: ""
+ setup:
+ prerequisites:
+ list: []
+ configuration:
+ file:
+ name: go.d/dnsmasq_dhcp.conf
+ options:
+ description: |
+ The following options can be defined globally: update_every, autodetection_retry.
+ folding:
+ title: Config options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 1
+ required: false
+ - name: autodetection_retry
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
+ default_value: 0
+ required: false
+ - name: leases_path
+ description: Path to dnsmasq DHCP leases file.
+ default_value: /var/lib/misc/dnsmasq.leases
+ required: false
+ - name: conf_path
+ description: Path to dnsmasq configuration file.
+ default_value: /etc/dnsmasq.conf
+ required: false
+ - name: conf_dir
+ description: Path to dnsmasq configuration directory.
+ default_value: /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new
+ required: false
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list:
+ - name: Basic
+ description: An example configuration.
+ config: |
+ jobs:
+ - name: dnsmasq_dhcp
+ leases_path: /var/lib/misc/dnsmasq.leases
+ conf_path: /etc/dnsmasq.conf
+ conf_dir: /etc/dnsmasq.d
+ - name: Pi-hole
+ description: Dnsmasq DHCP on Pi-hole.
+ config: |
+ jobs:
+ - name: dnsmasq_dhcp
+ leases_path: /etc/pihole/dhcp.leases
+ conf_path: /etc/dnsmasq.conf
+ conf_dir: /etc/dnsmasq.d
+ troubleshooting:
+ problems:
+ list: []
+ alerts:
+ - name: dnsmasq_dhcp_dhcp_range_utilization
+ metric: dnsmasq_dhcp.dhcp_range_utilization
+ info: DHCP range utilization
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/dnsmasq_dhcp.conf
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: global
+ description: These metrics refer to the entire monitored application.
+ labels: []
+ metrics:
+ - name: dnsmasq_dhcp.dhcp_ranges
+ description: Number of DHCP Ranges
+ unit: ranges
+ chart_type: stacked
+ dimensions:
+ - name: ipv4
+ - name: ipv6
+ - name: dnsmasq_dhcp.dhcp_hosts
+ description: Number of DHCP Hosts
+ unit: hosts
+ chart_type: stacked
+ dimensions:
+ - name: ipv4
+ - name: ipv6
+ - name: dhcp range
+ description: These metrics refer to the DHCP range.
+ labels:
+ - name: dhcp_range
+ description: DHCP range in `START_IP:END_IP` format
+ metrics:
+ - name: dnsmasq_dhcp.dhcp_range_utilization
+ description: DHCP Range utilization
+ unit: percentage
+ chart_type: line
+ dimensions:
+ - name: used
+ - name: dnsmasq_dhcp.dhcp_range_allocated_leases
+ description: DHCP Range Allocated Leases
+ unit: leases
+ chart_type: line
+ dimensions:
+ - name: allocated
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/parse_configuration.go b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/parse_configuration.go
new file mode 100644
index 000000000..24a20bb59
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/parse_configuration.go
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsmasq_dhcp
+
+import (
+ "bufio"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/iprange"
+)
+
+func (d *DnsmasqDHCP) parseDnsmasqDHCPConfiguration() ([]iprange.Range, []net.IP) {
+ configs := findConfigurationFiles(d.ConfPath, d.ConfDir)
+
+ dhcpRanges := d.getDHCPRanges(configs)
+ dhcpHosts := d.getDHCPHosts(configs)
+
+ return dhcpRanges, dhcpHosts
+}
+
+func (d *DnsmasqDHCP) getDHCPRanges(configs []*configFile) []iprange.Range {
+ var dhcpRanges []iprange.Range
+ var parsed string
+ seen := make(map[string]bool)
+
+ for _, conf := range configs {
+ d.Debugf("looking in '%s'", conf.path)
+
+ for _, value := range conf.get("dhcp-range") {
+ d.Debugf("found dhcp-range '%s'", value)
+ if parsed = parseDHCPRangeValue(value); parsed == "" || seen[parsed] {
+ continue
+ }
+ seen[parsed] = true
+
+ r, err := iprange.ParseRange(parsed)
+ if r == nil || err != nil {
+ d.Warningf("error on parsing dhcp-range '%s', skipping it", parsed)
+ continue
+ }
+
+ d.Debugf("adding dhcp-range '%s'", parsed)
+ dhcpRanges = append(dhcpRanges, r)
+ }
+ }
+
+ // order: ipv4, ipv6
+ sort.Slice(dhcpRanges, func(i, j int) bool { return dhcpRanges[i].Family() < dhcpRanges[j].Family() })
+
+ return dhcpRanges
+}
+
+func (d *DnsmasqDHCP) getDHCPHosts(configs []*configFile) []net.IP {
+ var dhcpHosts []net.IP
+ seen := make(map[string]bool)
+ var parsed string
+
+ for _, conf := range configs {
+ d.Debugf("looking in '%s'", conf.path)
+
+ for _, value := range conf.get("dhcp-host") {
+ d.Debugf("found dhcp-host '%s'", value)
+ if parsed = parseDHCPHostValue(value); parsed == "" || seen[parsed] {
+ continue
+ }
+ seen[parsed] = true
+
+ v := net.ParseIP(parsed)
+ if v == nil {
+ d.Warningf("error on parsing dhcp-host '%s', skipping it", parsed)
+ continue
+ }
+
+ d.Debugf("adding dhcp-host '%s'", parsed)
+ dhcpHosts = append(dhcpHosts, v)
+ }
+ }
+ return dhcpHosts
+}
+
+/*
+Examples:
+ - 192.168.0.50,192.168.0.150,12h
+ - 192.168.0.50,192.168.0.150,255.255.255.0,12h
+ - set:red,1.1.1.50,1.1.2.150, 255.255.252.0
+ - 192.168.0.0,static
+ - 1234::2,1234::500, 64, 12h
+ - 1234::2,1234::500
+ - 1234::2,1234::500, slaac
+ - 1234::,ra-only
+ - 1234::,ra-names
+ - 1234::,ra-stateless
+*/
+var reDHCPRange = regexp.MustCompile(`([0-9a-f.:]+),([0-9a-f.:]+)`)
+
+func parseDHCPRangeValue(s string) (r string) {
+ if strings.Contains(s, "ra-stateless") {
+ return
+ }
+
+ match := reDHCPRange.FindStringSubmatch(s)
+ if match == nil {
+ return
+ }
+
+ start, end := net.ParseIP(match[1]), net.ParseIP(match[2])
+ if start == nil || end == nil {
+ return
+ }
+
+ return fmt.Sprintf("%s-%s", start, end)
+}
+
+/*
+Examples:
+ - 11:22:33:44:55:66,192.168.0.60
+ - 11:22:33:44:55:66,fred,192.168.0.60,45m
+ - 11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60
+ - bert,192.168.0.70,infinite
+ - id:01:02:02:04,192.168.0.60
+ - id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61
+ - id:marjorie,192.168.0.60
+ - id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5]
+*/
+var (
+ reDHCPHostV4 = regexp.MustCompile(`(?:[0-9]{1,3}\.){3}[0-9]{1,3}`)
+ reDHCPHostV6 = regexp.MustCompile(`\[([0-9a-f.:]+)]`)
+)
+
+func parseDHCPHostValue(s string) (r string) {
+ if strings.Contains(s, "[") {
+ return strings.Trim(reDHCPHostV6.FindString(s), "[]")
+ }
+ return reDHCPHostV4.FindString(s)
+}
+
+type (
+ extension string
+
+ extensions []extension
+
+ configDir struct {
+ path string
+ include extensions
+ exclude extensions
+ }
+)
+
+func (e extension) match(filename string) bool {
+ return strings.HasSuffix(filename, string(e))
+}
+
+func (es extensions) match(filename string) bool {
+ for _, e := range es {
+ if e.match(filename) {
+ return true
+ }
+ }
+ return false
+}
+
+func parseConfDir(confDirStr string) configDir {
+ // # Include all the files in a directory except those ending in .bak
+ //#conf-dir=/etc/dnsmasq.d,.bak
+ //# Include all files in a directory which end in .conf
+ //#conf-dir=/etc/dnsmasq.d/,*.conf
+
+ parts := strings.Split(confDirStr, ",")
+ cd := configDir{path: parts[0]}
+
+ for _, arg := range parts[1:] {
+ arg = strings.TrimSpace(arg)
+ if strings.HasPrefix(arg, "*") {
+ cd.include = append(cd.include, extension(arg[1:]))
+ } else {
+ cd.exclude = append(cd.exclude, extension(arg))
+ }
+ }
+ return cd
+}
+
+func (cd configDir) isValidFilename(filename string) bool {
+ switch {
+ default:
+ return true
+ case strings.HasPrefix(filename, "."):
+ case strings.HasPrefix(filename, "~"):
+ case strings.HasPrefix(filename, "#") && strings.HasSuffix(filename, "#"):
+ }
+ return false
+}
+
+func (cd configDir) match(filename string) bool {
+ switch {
+ default:
+ return true
+ case !cd.isValidFilename(filename):
+ case len(cd.include) > 0 && !cd.include.match(filename):
+ case cd.exclude.match(filename):
+ }
+ return false
+}
+
+func (cd configDir) findConfigs() ([]string, error) {
+ fis, err := os.ReadDir(cd.path)
+ if err != nil {
+ return nil, err
+ }
+
+ var files []string
+ for _, fi := range fis {
+ info, err := fi.Info()
+ if err != nil {
+ return nil, err
+ }
+ if !info.Mode().IsRegular() || !cd.match(fi.Name()) {
+ continue
+ }
+ files = append(files, filepath.Join(cd.path, fi.Name()))
+ }
+ return files, nil
+}
+
+func openFile(filepath string) (f *os.File, err error) {
+ defer func() {
+ if err != nil && f != nil {
+ _ = f.Close()
+ }
+ }()
+
+ f, err = os.Open(filepath)
+ if err != nil {
+ return nil, err
+ }
+
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+
+ if !fi.Mode().IsRegular() {
+ return nil, fmt.Errorf("'%s' is not a regular file", filepath)
+ }
+ return f, nil
+}
+
+type (
+ configOption struct {
+ key, value string
+ }
+
+ configFile struct {
+ path string
+ options []configOption
+ }
+)
+
+func (cf *configFile) get(name string) []string {
+ var options []string
+ for _, o := range cf.options {
+ if o.key != name {
+ continue
+ }
+ options = append(options, o.value)
+ }
+ return options
+}
+
+func parseConfFile(filename string) (*configFile, error) {
+ f, err := openFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = f.Close() }()
+
+ cf := configFile{path: filename}
+ s := bufio.NewScanner(f)
+ for s.Scan() {
+ line := strings.TrimSpace(s.Text())
+ if strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ if !strings.Contains(line, "=") {
+ continue
+ }
+
+ line = strings.ReplaceAll(line, " ", "")
+ parts := strings.Split(line, "=")
+ if len(parts) != 2 {
+ continue
+ }
+
+ cf.options = append(cf.options, configOption{key: parts[0], value: parts[1]})
+ }
+ return &cf, nil
+}
+
+type ConfigFinder struct {
+ entryConfig string
+ entryDir string
+ visitedConfigs map[string]bool
+ visitedDirs map[string]bool
+}
+
+func (f *ConfigFinder) find() []*configFile {
+ f.visitedConfigs = make(map[string]bool)
+ f.visitedDirs = make(map[string]bool)
+
+ configs := f.recursiveFind(f.entryConfig)
+
+ for _, file := range f.entryDirConfigs() {
+ configs = append(configs, f.recursiveFind(file)...)
+ }
+ return configs
+}
+
+func (f *ConfigFinder) entryDirConfigs() []string {
+ if f.entryDir == "" {
+ return nil
+ }
+ files, err := parseConfDir(f.entryDir).findConfigs()
+ if err != nil {
+ return nil
+ }
+ return files
+}
+
+func (f *ConfigFinder) recursiveFind(filename string) (configs []*configFile) {
+ if f.visitedConfigs[filename] {
+ return nil
+ }
+
+ config, err := parseConfFile(filename)
+ if err != nil {
+ return nil
+ }
+
+ files, dirs := config.get("conf-file"), config.get("conf-dir")
+
+ f.visitedConfigs[filename] = true
+ configs = append(configs, config)
+
+ for _, file := range files {
+ configs = append(configs, f.recursiveFind(file)...)
+ }
+
+ for _, dir := range dirs {
+ if dir == "" {
+ continue
+ }
+
+ d := parseConfDir(dir)
+
+ if f.visitedDirs[d.path] {
+ continue
+ }
+ f.visitedDirs[d.path] = true
+
+ files, err = d.findConfigs()
+ if err != nil {
+ continue
+ }
+
+ for _, file := range files {
+ configs = append(configs, f.recursiveFind(file)...)
+ }
+ }
+ return configs
+}
+
+func findConfigurationFiles(entryConfig string, entryDir string) []*configFile {
+ cf := ConfigFinder{
+ entryConfig: entryConfig,
+ entryDir: entryDir,
+ }
+ return cf.find()
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.json b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.json
new file mode 100644
index 000000000..6df6faec6
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.json
@@ -0,0 +1,6 @@
+{
+ "update_every": 123,
+ "leases_path": "ok",
+ "conf_path": "ok",
+ "conf_dir": "ok"
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.yaml
new file mode 100644
index 000000000..4a03e6db8
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/config.yaml
@@ -0,0 +1,4 @@
+update_every: 123
+leases_path: "ok"
+conf_path: "ok"
+conf_dir: "ok"
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.conf
new file mode 100644
index 000000000..4cf77478e
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.conf
@@ -0,0 +1,77 @@
+# Uncomment this to enable the integrated DHCP server, you need
+# to supply the range of addresses available for lease and optionally
+# a lease time. If you have more than one network, you will need to
+# repeat this for each network on which you want to supply DHCP
+# service.
+#dhcp-range=192.168.0.50,192.168.0.150,12h
+
+# This is an example of a DHCP range where the netmask is given. This
+# is needed for networks we reach the dnsmasq DHCP server via a relay
+# agent. If you don't know what a DHCP relay agent is, you probably
+# don't need to worry about this.
+#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h
+
+# This is an example of a DHCP range which sets a tag, so that
+# some DHCP options may be set only for this network.
+#dhcp-range=set:red,192.168.0.50,192.168.0.150
+
+# Use this DHCP range only when the tag "green" is set.
+#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h
+
+# Specify a subnet which can't be used for dynamic address allocation,
+# is available for hosts with matching --dhcp-host lines. Note that
+# dhcp-host declarations will be ignored unless there is a dhcp-range
+# of some type for the subnet in question.
+# In this case the netmask is implied (it comes from the network
+# configuration on the machine running dnsmasq) it is possible to give
+# an explicit netmask instead.
+#dhcp-range=192.168.0.0,static
+
+# Enable DHCPv6. Note that the prefix-length does not need to be specified
+# and defaults to 64 if missing/
+#dhcp-range=1234::2, 1234::500, 64, 12h
+
+# Do Router Advertisements, BUT NOT DHCP for this subnet.
+#dhcp-range=1234::, ra-only
+
+# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and
+# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack
+# hosts. Use the DHCPv4 lease to derive the name, network segment and
+# MAC address and assume that the host will also have an
+# IPv6 address calculated using the SLAAC alogrithm.
+#dhcp-range=1234::, ra-names
+
+# Do Router Advertisements, BUT NOT DHCP for this subnet.
+# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.)
+#dhcp-range=1234::, ra-only, 48h
+
+# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA
+# so that clients can use SLAAC addresses as well as DHCP ones.
+#dhcp-range=1234::2, 1234::500, slaac
+
+# Do Router Advertisements and stateless DHCP for this subnet. Clients will
+# not get addresses from DHCP, but they will get other configuration information.
+# They will use SLAAC for addresses.
+#dhcp-range=1234::, ra-stateless
+
+# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses
+# from DHCPv4 leases.
+#dhcp-range=1234::, ra-stateless, ra-names
+
+dhcp-range=192.168.0.1,192.168.0.100,12h
+dhcp-range = 1230::1, 1230::64
+
+dhcp-range = 1235::2, 1235::500, ra-stateless
+dhcp-range=1234::, ra-stateless, ra-names
+dhcp-range=1234::, ra-stateless
+dhcp-range=1234::, ra-only, 48h
+
+dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.99
+dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1230::63]
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/.dnsmasq.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/.dnsmasq.conf
new file mode 100644
index 000000000..b9ca78218
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/.dnsmasq.conf
@@ -0,0 +1 @@
+dhcp-range=tag:green,192.168.11.1,192.168.11.100,12h \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv4.any b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv4.any
new file mode 100644
index 000000000..300faa28e
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv4.any
@@ -0,0 +1,10 @@
+dhcp-range=tag:green,192.168.1.1,192.168.1.100,12h
+
+dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.1.99
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv6.any b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv6.any
new file mode 100644
index 000000000..414d6819f
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv6.any
@@ -0,0 +1,10 @@
+dhcp-range = 1231::1, 1231::64
+
+dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1231::63]
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv4.any b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv4.any
new file mode 100644
index 000000000..24a742797
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv4.any
@@ -0,0 +1,10 @@
+dhcp-range=tag:green,192.168.2.1,192.168.2.100,12h
+
+dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.2.99
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv6.any b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv6.any
new file mode 100644
index 000000000..4ae70f0b2
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv6.any
@@ -0,0 +1,10 @@
+dhcp-range = 1232::1, 1232::64
+
+dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1232::63]
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/~dnsmasq.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/~dnsmasq.conf
new file mode 100644
index 000000000..dc58bf9d8
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/~dnsmasq.conf
@@ -0,0 +1 @@
+dhcp-range=192.168.22.0,192.168.22.255,12h \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasq.bak b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasq.bak
new file mode 100644
index 000000000..c3897671a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasq.bak
@@ -0,0 +1 @@
+dhcp-range=tag:green,192.168.33.1,192.168.33.100,12h \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv4.any b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv4.any
new file mode 100644
index 000000000..a55ac969a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv4.any
@@ -0,0 +1,10 @@
+dhcp-range=tag:green,192.168.3.1,192.168.3.100,12h
+
+dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.3.99
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv6.any b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv6.any
new file mode 100644
index 000000000..4bc6cf10f
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv6.any
@@ -0,0 +1,3 @@
+dhcp-range = 1233::1, 1233::64
+
+dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1233::63] \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasq.other b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasq.other
new file mode 100644
index 000000000..18fe1ac53
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasq.other
@@ -0,0 +1 @@
+dhcp-range=tag:green,192.168.44.1,192.168.44.100,12h \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv4.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv4.conf
new file mode 100644
index 000000000..1493b8009
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv4.conf
@@ -0,0 +1,10 @@
+dhcp-range=tag:green,192.168.4.1,192.168.4.100,12h
+
+dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.4.99
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv6.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv6.conf
new file mode 100644
index 000000000..389c2c95b
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv6.conf
@@ -0,0 +1,10 @@
+dhcp-range = 1234::1, 1234::64
+
+dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::63]
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf
+
+conf-dir=testdata/dnsmasq.d2
+conf-dir=testdata/dnsmasq.d3,.bak
+conf-dir=testdata/dnsmasq.d4,*.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.leases b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.leases
new file mode 100644
index 000000000..606e74fba
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq.leases
@@ -0,0 +1,19 @@
+1560300536 08:00:27:61:3c:ee 192.168.0.1 * *
+1560300536 08:00:27:61:3c:ee 192.168.0.2 * *
+1560300536 08:00:27:61:3c:ee 192.168.0.3 * *
+1560300536 08:00:27:61:3c:ee 192.168.0.4 * *
+1560300536 08:00:27:61:3c:ee 192.168.0.5 * *
+1560300536 08:00:27:61:3c:ee 192.168.1.1 * *
+1560300536 08:00:27:61:3c:ee 192.168.1.2 * *
+1560300536 08:00:27:61:3c:ee 192.168.1.3 * *
+1560300536 08:00:27:61:3c:ee 192.168.1.4 * *
+1560300536 08:00:27:61:3c:ee 192.168.2.1 * *
+1560300536 08:00:27:61:3c:ee 192.168.2.2 * *
+1560300536 08:00:27:61:3c:ee 192.168.2.3 * *
+duid 00:01:00:01:24:90:cf:5b:08:00:27:61:2e:2c
+1560300414 660684014 1230::1 * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
+1560300414 660684014 1230::2 * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
+1560300414 660684014 1230::3 * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
+1560300414 660684014 1230::4 * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
+1560300414 660684014 1230::5 * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
+1560300414 660684014 1230::6 * 00:01:00:01:24:90:cf:a3:08:00:27:61:3c:ee
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq2.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq2.conf
new file mode 100644
index 000000000..bd1766adb
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq2.conf
@@ -0,0 +1,6 @@
+dhcp-range=192.168.200.1,192.168.200.100,12h
+
+dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.200.99
+
+conf-file=testdata/dnsmasq.conf
+conf-file=testdata/dnsmasq2.conf \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq3.conf b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq3.conf
new file mode 100644
index 000000000..3475544b5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp/testdata/dnsmasq3.conf
@@ -0,0 +1,4 @@
+#dhcp-range=192.168.0.50,192.168.0.150,12h
+#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h
+#dhcp-range=set:red,192.168.0.50,192.168.0.150
+#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h \ No newline at end of file