diff options
Diffstat (limited to '')
39 files changed, 2728 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/dnsmasq/README.md b/src/go/plugin/go.d/modules/dnsmasq/README.md new file mode 120000 index 000000000..a424dd9c6 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/README.md @@ -0,0 +1 @@ +integrations/dnsmasq.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/dnsmasq/charts.go b/src/go/plugin/go.d/modules/dnsmasq/charts.go new file mode 100644 index 000000000..403e7862c --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/charts.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dnsmasq + +import "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + +var cacheCharts = module.Charts{ + { + ID: "servers_queries", + Title: "Queries forwarded to the upstream servers", + Units: "queries/s", + Fam: "servers", + Ctx: "dnsmasq.servers_queries", + Dims: module.Dims{ + {ID: "queries", Name: "success", Algo: module.Incremental}, + {ID: "failed_queries", Name: "failed", Algo: module.Incremental}, + }, + }, + { + ID: "cache_performance", + Title: "Cache performance", + Units: "events/s", + Fam: "cache", + Ctx: "dnsmasq.cache_performance", + Dims: module.Dims{ + {ID: "hits", Algo: module.Incremental}, + {ID: "misses", Algo: module.Incremental}, + }, + }, + { + ID: "cache_operations", + Title: "Cache operations", + Units: "operations/s", + Fam: "cache", + Ctx: "dnsmasq.cache_operations", + Dims: module.Dims{ + {ID: "insertions", Algo: module.Incremental}, + {ID: "evictions", Algo: module.Incremental}, + }, + }, + { + ID: "cache_size", + Title: "Cache size", + Units: "entries", + Fam: "cache", + Ctx: "dnsmasq.cache_size", + Dims: module.Dims{ + {ID: "cachesize", Name: "size"}, + }, + }, +} diff --git a/src/go/plugin/go.d/modules/dnsmasq/collect.go b/src/go/plugin/go.d/modules/dnsmasq/collect.go new file mode 100644 index 000000000..9f3f963f0 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/collect.go @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dnsmasq + +import ( + "fmt" + "strconv" + "strings" + + "github.com/miekg/dns" +) + +func (d *Dnsmasq) collect() (map[string]int64, error) { + mx := make(map[string]int64) + + if err := d.collectCacheStatistics(mx); err != nil { + return nil, err + } + + return mx, nil +} + +func (d *Dnsmasq) collectCacheStatistics(mx map[string]int64) error { + /* + ;; flags: qr aa rd ra; QUERY: 7, ANSWER: 7, AUTHORITY: 0, ADDITIONAL: 0 + + ;; QUESTION SECTION: + ;cachesize.bind. CH TXT + ;insertions.bind. CH TXT + ;evictions.bind. CH TXT + ;hits.bind. CH TXT + ;misses.bind. CH TXT + ;auth.bind. CH TXT + ;servers.bind. CH TXT + + ;; ANSWER SECTION: + cachesize.bind. 0 CH TXT "150" + insertions.bind. 0 CH TXT "1" + evictions.bind. 0 CH TXT "0" + hits.bind. 0 CH TXT "176" + misses.bind. 0 CH TXT "4" + auth.bind. 0 CH TXT "0" + servers.bind. 0 CH TXT "10.0.0.1#53 0 0" "1.1.1.1#53 4 3" "1.0.0.1#53 3 0" + */ + + questions := []string{ + "servers.bind.", + "cachesize.bind.", + "insertions.bind.", + "evictions.bind.", + "hits.bind.", + "misses.bind.", + // auth.bind query is only supported if dnsmasq has been built to support running as an authoritative name server + // See https://github.com/netdata/netdata/issues/13766 + //"auth.bind.", + } + + for _, q := range questions { + resp, err := d.query(q) + if err != nil { + return err + } + + for _, a := range resp.Answer { + txt, ok := a.(*dns.TXT) + if !ok { + continue + } + + idx := strings.IndexByte(txt.Hdr.Name, '.') + if idx == -1 { + continue + } + + name := txt.Hdr.Name[:idx] + + switch name { + case "servers": + for _, entry := range txt.Txt { + parts := strings.Fields(entry) + if len(parts) != 3 { + return fmt.Errorf("parse %s (%s): unexpected format", txt.Hdr.Name, entry) + } + queries, err := strconv.ParseFloat(parts[1], 64) + if err != nil { + return fmt.Errorf("parse '%s' (%s): %v", txt.Hdr.Name, entry, err) + } + failedQueries, err := strconv.ParseFloat(parts[2], 64) + if err != nil { + return fmt.Errorf("parse '%s' (%s): %v", txt.Hdr.Name, entry, err) + } + + mx["queries"] += int64(queries) + mx["failed_queries"] += int64(failedQueries) + } + case "cachesize", "insertions", "evictions", "hits", "misses", "auth": + if len(txt.Txt) != 1 { + return fmt.Errorf("parse '%s' (%v): unexpected format", txt.Hdr.Name, txt.Txt) + } + v, err := strconv.ParseFloat(txt.Txt[0], 64) + if err != nil { + return fmt.Errorf("parse '%s' (%s): %v", txt.Hdr.Name, txt.Txt[0], err) + } + + mx[name] = int64(v) + } + } + } + + return nil +} + +func (d *Dnsmasq) query(question string) (*dns.Msg, error) { + msg := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{ + {Name: question, Qtype: dns.TypeTXT, Qclass: dns.ClassCHAOS}, + }, + } + + r, _, err := d.dnsClient.Exchange(msg, d.Address) + if err != nil { + return nil, err + } + + if r == nil { + return nil, fmt.Errorf("'%s' question '%s', returned an empty response", d.Address, question) + } + + if r.Rcode != dns.RcodeSuccess { + s := dns.RcodeToString[r.Rcode] + return nil, fmt.Errorf("'%s' question '%s' returned '%s' (%d) response code", d.Address, question, s, r.Rcode) + } + + return r, nil +} diff --git a/src/go/plugin/go.d/modules/dnsmasq/config_schema.json b/src/go/plugin/go.d/modules/dnsmasq/config_schema.json new file mode 100644 index 000000000..79396b364 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/config_schema.json @@ -0,0 +1,61 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Dnsmasq collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "address": { + "title": "Address", + "description": "The IP address and port where the Dnsmasq daemon listens for connections.", + "type": "string", + "default": "127.0.0.1:53" + }, + "protocol": { + "title": "Protocol", + "description": "DNS query transport protocol.", + "type": "string", + "enum": [ + "udp", + "tcp", + "tcp-tls" + ], + "default": "udp" + }, + "timeout": { + "title": "Timeout", + "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.", + "type": "number", + "default": 1 + } + }, + "required": [ + "address", + "protocol" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "protocol": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + } + } +} diff --git a/src/go/plugin/go.d/modules/dnsmasq/dnsmasq.go b/src/go/plugin/go.d/modules/dnsmasq/dnsmasq.go new file mode 100644 index 000000000..2d2112c05 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/dnsmasq.go @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dnsmasq + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" + + "github.com/miekg/dns" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("dnsmasq", module.Creator{ + JobConfigSchema: configSchema, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *Dnsmasq { + return &Dnsmasq{ + Config: Config{ + Protocol: "udp", + Address: "127.0.0.1:53", + Timeout: web.Duration(time.Second), + }, + + newDNSClient: func(network string, timeout time.Duration) dnsClient { + return &dns.Client{ + Net: network, + Timeout: timeout, + } + }, + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Address string `yaml:"address" json:"address"` + Protocol string `yaml:"protocol,omitempty" json:"protocol"` + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` +} + +type ( + Dnsmasq struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + dnsClient dnsClient + newDNSClient func(network string, timeout time.Duration) dnsClient + } + dnsClient interface { + Exchange(msg *dns.Msg, address string) (resp *dns.Msg, rtt time.Duration, err error) + } +) + +func (d *Dnsmasq) Configuration() any { + return d.Config +} + +func (d *Dnsmasq) Init() error { + err := d.validateConfig() + if err != nil { + d.Errorf("config validation: %v", err) + return err + } + + client, err := d.initDNSClient() + if err != nil { + d.Errorf("init DNS client: %v", err) + return err + } + d.dnsClient = client + + charts, err := d.initCharts() + if err != nil { + d.Errorf("init charts: %v", err) + return err + } + d.charts = charts + + return nil +} + +func (d *Dnsmasq) 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 *Dnsmasq) Charts() *module.Charts { + return d.charts +} + +func (d *Dnsmasq) Collect() map[string]int64 { + ms, err := d.collect() + if err != nil { + d.Error(err) + } + + if len(ms) == 0 { + return nil + } + return ms +} + +func (d *Dnsmasq) Cleanup() {} diff --git a/src/go/plugin/go.d/modules/dnsmasq/dnsmasq_test.go b/src/go/plugin/go.d/modules/dnsmasq/dnsmasq_test.go new file mode 100644 index 000000000..b3d54ac9c --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/dnsmasq_test.go @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dnsmasq + +import ( + "errors" + "fmt" + "os" + "testing" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + } +} + +func TestDnsmasq_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Dnsmasq{}, dataConfigJSON, dataConfigYAML) +} + +func TestDnsmasq_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "success on default config": { + config: New().Config, + }, + "fails on unset 'address'": { + wantFail: true, + config: Config{ + Protocol: "udp", + Address: "", + }, + }, + "fails on unset 'protocol'": { + wantFail: true, + config: Config{ + Protocol: "", + Address: "127.0.0.1:53", + }, + }, + "fails on invalid 'protocol'": { + wantFail: true, + config: Config{ + Protocol: "http", + Address: "127.0.0.1:53", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ns := New() + ns.Config = test.config + + if test.wantFail { + assert.Error(t, ns.Init()) + } else { + assert.NoError(t, ns.Init()) + } + }) + } +} + +func TestDnsmasq_Check(t *testing.T) { + tests := map[string]struct { + prepare func() *Dnsmasq + wantFail bool + }{ + "success on valid response": { + prepare: prepareOKDnsmasq, + }, + "fails on error on cache stats query": { + wantFail: true, + prepare: prepareErrorOnExchangeDnsmasq, + }, + "fails on response rcode is not success": { + wantFail: true, + prepare: prepareRcodeServerFailureOnExchangeDnsmasq, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + dnsmasq := test.prepare() + require.NoError(t, dnsmasq.Init()) + + if test.wantFail { + assert.Error(t, dnsmasq.Check()) + } else { + assert.NoError(t, dnsmasq.Check()) + } + }) + } +} + +func TestDnsmasq_Charts(t *testing.T) { + dnsmasq := New() + require.NoError(t, dnsmasq.Init()) + assert.NotNil(t, dnsmasq.Charts()) +} + +func TestDnsmasq_Cleanup(t *testing.T) { + assert.NotPanics(t, New().Cleanup) +} + +func TestDnsmasq_Collect(t *testing.T) { + tests := map[string]struct { + prepare func() *Dnsmasq + wantCollected map[string]int64 + }{ + "success on valid response": { + prepare: prepareOKDnsmasq, + wantCollected: map[string]int64{ + //"auth": 5, + "cachesize": 999, + "evictions": 5, + "failed_queries": 9, + "hits": 100, + "insertions": 10, + "misses": 50, + "queries": 17, + }, + }, + "fails on error on cache stats query": { + prepare: prepareErrorOnExchangeDnsmasq, + }, + "fails on response rcode is not success": { + prepare: prepareRcodeServerFailureOnExchangeDnsmasq, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + dnsmasq := test.prepare() + require.NoError(t, dnsmasq.Init()) + + collected := dnsmasq.Collect() + + assert.Equal(t, test.wantCollected, collected) + if len(test.wantCollected) > 0 { + ensureCollectedHasAllChartsDimsVarsIDs(t, dnsmasq, collected) + } + }) + } +} + +func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, dnsmasq *Dnsmasq, collected map[string]int64) { + for _, chart := range *dnsmasq.Charts() { + if chart.Obsolete { + continue + } + for _, dim := range chart.Dims { + _, ok := collected[dim.ID] + assert.Truef(t, ok, "chart '%s' dim '%s': no dim in collected", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := collected[v.ID] + assert.Truef(t, ok, "chart '%s' dim '%s': no dim in collected", v.ID, chart.ID) + } + } +} + +func prepareOKDnsmasq() *Dnsmasq { + dnsmasq := New() + dnsmasq.newDNSClient = func(network string, timeout time.Duration) dnsClient { + return &mockDNSClient{} + } + return dnsmasq +} + +func prepareErrorOnExchangeDnsmasq() *Dnsmasq { + dnsmasq := New() + dnsmasq.newDNSClient = func(network string, timeout time.Duration) dnsClient { + return &mockDNSClient{ + errOnExchange: true, + } + } + return dnsmasq +} + +func prepareRcodeServerFailureOnExchangeDnsmasq() *Dnsmasq { + dnsmasq := New() + dnsmasq.newDNSClient = func(network string, timeout time.Duration) dnsClient { + return &mockDNSClient{ + rcodeServerFailureOnExchange: true, + } + } + return dnsmasq +} + +type mockDNSClient struct { + errOnExchange bool + rcodeServerFailureOnExchange bool +} + +func (m mockDNSClient) Exchange(msg *dns.Msg, _ string) (*dns.Msg, time.Duration, error) { + if m.errOnExchange { + return nil, 0, errors.New("'Exchange' error") + } + if m.rcodeServerFailureOnExchange { + resp := &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure}} + return resp, 0, nil + } + + var answers []dns.RR + for _, q := range msg.Question { + a, err := prepareDNSAnswer(q) + if err != nil { + return nil, 0, err + } + answers = append(answers, a) + } + + resp := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Rcode: dns.RcodeSuccess, + }, + Answer: answers, + } + return resp, 0, nil +} + +func prepareDNSAnswer(q dns.Question) (dns.RR, error) { + if want, got := dns.TypeToString[dns.TypeTXT], dns.TypeToString[q.Qtype]; want != got { + return nil, fmt.Errorf("unexpected Qtype, want=%s, got=%s", want, got) + } + if want, got := dns.ClassToString[dns.ClassCHAOS], dns.ClassToString[q.Qclass]; want != got { + return nil, fmt.Errorf("unexpected Qclass, want=%s, got=%s", want, got) + } + + var txt []string + switch q.Name { + case "cachesize.bind.": + txt = []string{"999"} + case "insertions.bind.": + txt = []string{"10"} + case "evictions.bind.": + txt = []string{"5"} + case "hits.bind.": + txt = []string{"100"} + case "misses.bind.": + txt = []string{"50"} + case "auth.bind.": + txt = []string{"5"} + case "servers.bind.": + txt = []string{"10.0.0.1#53 10 5", "1.1.1.1#53 4 3", "1.0.0.1#53 3 1"} + default: + return nil, fmt.Errorf("unexpected question Name: %s", q.Name) + } + + rr := &dns.TXT{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeTXT, + Class: dns.ClassCHAOS, + }, + Txt: txt, + } + return rr, nil +} diff --git a/src/go/plugin/go.d/modules/dnsmasq/init.go b/src/go/plugin/go.d/modules/dnsmasq/init.go new file mode 100644 index 000000000..a660ac774 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/init.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dnsmasq + +import ( + "errors" + "fmt" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +func (d *Dnsmasq) validateConfig() error { + if d.Address == "" { + return errors.New("'address' parameter not set") + } + if !isProtocolValid(d.Protocol) { + return fmt.Errorf("'protocol' (%s) is not valid, expected one of %v", d.Protocol, validProtocols) + } + return nil +} + +func (d *Dnsmasq) initDNSClient() (dnsClient, error) { + return d.newDNSClient(d.Protocol, d.Timeout.Duration()), nil +} + +func (d *Dnsmasq) initCharts() (*module.Charts, error) { + return cacheCharts.Copy(), nil +} + +func isProtocolValid(protocol string) bool { + for _, v := range validProtocols { + if protocol == v { + return true + } + } + return false +} + +var validProtocols = []string{ + "udp", + "tcp", + "tcp-tls", +} diff --git a/src/go/plugin/go.d/modules/dnsmasq/integrations/dnsmasq.md b/src/go/plugin/go.d/modules/dnsmasq/integrations/dnsmasq.md new file mode 100644 index 000000000..d5c358a29 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/integrations/dnsmasq.md @@ -0,0 +1,230 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/dnsmasq/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/dnsmasq/metadata.yaml" +sidebar_label: "Dnsmasq" +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 + + +<img src="https://netdata.cloud/img/dnsmasq.svg" width="150"/> + + +Plugin: go.d.plugin +Module: dnsmasq + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors Dnsmasq servers. + + + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### 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 instance + +The metrics apply to the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| dnsmasq.servers_queries | success, failed | queries/s | +| dnsmasq.cache_performance | hist, misses | events/s | +| dnsmasq.cache_operations | insertions, evictions | operations/s | +| dnsmasq.cache_size | size | entries | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/dnsmasq.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.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 | +| address | Server address in `ip:port` format. | 127.0.0.1:53 | yes | +| protocol | DNS query transport protocol. Supported protocols: udp, tcp, tcp-tls. | udp | no | +| timeout | DNS query timeout (dial, write and read) in seconds. | 1 | no | + +</details> + +#### Examples + +##### Basic + +An example configuration. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + address: 127.0.0.1:53 + +``` +</details> + +##### Using TCP protocol + +Local server with specific DNS query transport protocol. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + address: 127.0.0.1:53 + protocol: tcp + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Collecting metrics from local and remote instances. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + address: 127.0.0.1:53 + + - name: remote + address: 203.0.113.0:53 + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +To troubleshoot issues with the `dnsmasq` 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 + ``` + +### Getting Logs + +If you're encountering problems with the `dnsmasq` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep dnsmasq +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep dnsmasq /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep dnsmasq +``` + + diff --git a/src/go/plugin/go.d/modules/dnsmasq/metadata.yaml b/src/go/plugin/go.d/modules/dnsmasq/metadata.yaml new file mode 100644 index 000000000..6911a323a --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/metadata.yaml @@ -0,0 +1,144 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-dnsmasq + plugin_name: go.d.plugin + module_name: dnsmasq + monitored_instance: + name: Dnsmasq + link: https://thekelleys.org.uk/dnsmasq/doc.html + icon_filename: dnsmasq.svg + categories: + - data-collection.dns-and-dhcp-servers + keywords: + - dnsmasq + - dns + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: | + This collector monitors Dnsmasq servers. + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: go.d/dnsmasq.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: address + description: Server address in `ip:port` format. + default_value: 127.0.0.1:53 + required: true + - name: protocol + description: 'DNS query transport protocol. Supported protocols: udp, tcp, tcp-tls.' + default_value: udp + required: false + - name: timeout + description: DNS query timeout (dial, write and read) in seconds. + default_value: 1 + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + description: An example configuration. + config: | + jobs: + - name: local + address: 127.0.0.1:53 + - name: Using TCP protocol + description: Local server with specific DNS query transport protocol. + config: | + jobs: + - name: local + address: 127.0.0.1:53 + protocol: tcp + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Collecting metrics from local and remote instances. + config: | + jobs: + - name: local + address: 127.0.0.1:53 + + - name: remote + address: 203.0.113.0:53 + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: global + description: The metrics apply to the entire monitored application. + labels: [] + metrics: + - name: dnsmasq.servers_queries + description: Queries forwarded to the upstream servers + unit: queries/s + chart_type: line + dimensions: + - name: success + - name: failed + - name: dnsmasq.cache_performance + description: Cache performance + unit: events/s + chart_type: line + dimensions: + - name: hist + - name: misses + - name: dnsmasq.cache_operations + description: Cache operations + unit: operations/s + chart_type: line + dimensions: + - name: insertions + - name: evictions + - name: dnsmasq.cache_size + description: Cache size + unit: entries + chart_type: line + dimensions: + - name: size diff --git a/src/go/plugin/go.d/modules/dnsmasq/testdata/config.json b/src/go/plugin/go.d/modules/dnsmasq/testdata/config.json new file mode 100644 index 000000000..4fff563b8 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/testdata/config.json @@ -0,0 +1,6 @@ +{ + "update_every": 123, + "protocol": "ok", + "address": "ok", + "timeout": 123.123 +} diff --git a/src/go/plugin/go.d/modules/dnsmasq/testdata/config.yaml b/src/go/plugin/go.d/modules/dnsmasq/testdata/config.yaml new file mode 100644 index 000000000..1a79b8773 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq/testdata/config.yaml @@ -0,0 +1,4 @@ +update_every: 123 +protocol: "ok" +address: "ok" +timeout: 123.123 diff --git a/src/go/plugin/go.d/modules/dnsmasq_dhcp/README.md b/src/go/plugin/go.d/modules/dnsmasq_dhcp/README.md new file mode 120000 index 000000000..ad22eb4ee --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq_dhcp/README.md @@ -0,0 +1 @@ +integrations/dnsmasq_dhcp.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/dnsmasq_dhcp/charts.go b/src/go/plugin/go.d/modules/dnsmasq_dhcp/charts.go new file mode 100644 index 000000000..bcef8aa3f --- /dev/null +++ b/src/go/plugin/go.d/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/plugins/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/collect.go b/src/go/plugin/go.d/modules/dnsmasq_dhcp/collect.go new file mode 100644 index 000000000..6de2fa215 --- /dev/null +++ b/src/go/plugin/go.d/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/plugins/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/config_schema.json b/src/go/plugin/go.d/modules/dnsmasq_dhcp/config_schema.json new file mode 100644 index 000000000..f51a3b2a2 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/dhcp.go b/src/go/plugin/go.d/modules/dnsmasq_dhcp/dhcp.go new file mode 100644 index 000000000..de56723f7 --- /dev/null +++ b/src/go/plugin/go.d/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/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/dhcp_test.go b/src/go/plugin/go.d/modules/dnsmasq_dhcp/dhcp_test.go new file mode 100644 index 000000000..a5774ae4a --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq_dhcp/dhcp_test.go @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dnsmasq_dhcp + +import ( + "os" + "testing" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/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()) +} + +func TestDnsmasqDHCP_parseDHCPRangeValue(t *testing.T) { + tests := map[string]struct { + input string + wantFail bool + }{ + "ipv4": { + input: "192.168.0.50,192.168.0.150,12h", + }, + "ipv4 with netmask": { + input: "192.168.0.50,192.168.0.150,255.255.255.0,12h", + }, + "ipv4 with netmask and tag": { + input: "set:red,1.1.1.50,1.1.2.150, 255.255.252.0", + }, + "ipv4 with iface": { + input: "enp3s0, 172.16.1.2, 172.16.1.254, 1h", + }, + "ipv4 with iface 2": { + input: "enp2s0.100, 192.168.100.2, 192.168.100.254, 1h", + }, + "ipv4 static": { + wantFail: true, + input: "192.168.0.0,static", + }, + "ipv6": { + input: "1234::2,1234::500", + }, + "ipv6 slacc": { + input: "1234::2,1234::500, slaac", + }, + "ipv6 with with prefix length and lease time": { + input: "1234::2,1234::500, 64, 12h", + }, + "ipv6 ra-only": { + wantFail: true, + input: "1234::,ra-only", + }, + "ipv6 ra-names": { + wantFail: true, + input: "1234::,ra-names", + }, + "ipv6 ra-stateless": { + wantFail: true, + input: "1234::,ra-stateless", + }, + "invalid": { + wantFail: true, + input: "192.168.0.0", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + v := parseDHCPRangeValue(test.input) + + if test.wantFail { + assert.Emptyf(t, v, "parsing '%s' must fail", test.input) + } else { + assert.NotEmptyf(t, v, "parsing '%s' must not fail", test.input) + } + }) + } +} diff --git a/src/go/plugin/go.d/modules/dnsmasq_dhcp/init.go b/src/go/plugin/go.d/modules/dnsmasq_dhcp/init.go new file mode 100644 index 000000000..6c74674a3 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md b/src/go/plugin/go.d/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md new file mode 100644 index 000000000..751ebf089 --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq_dhcp/integrations/dnsmasq_dhcp.md @@ -0,0 +1,240 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/dnsmasq_dhcp/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/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 + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +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 + ``` + +### Getting Logs + +If you're encountering problems with the `dnsmasq_dhcp` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep dnsmasq_dhcp +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep dnsmasq_dhcp /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep dnsmasq_dhcp +``` + + diff --git a/src/go/plugin/go.d/modules/dnsmasq_dhcp/metadata.yaml b/src/go/plugin/go.d/modules/dnsmasq_dhcp/metadata.yaml new file mode 100644 index 000000000..13b73336c --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/parse_configuration.go b/src/go/plugin/go.d/modules/dnsmasq_dhcp/parse_configuration.go new file mode 100644 index 000000000..5ef29f28e --- /dev/null +++ b/src/go/plugin/go.d/modules/dnsmasq_dhcp/parse_configuration.go @@ -0,0 +1,391 @@ +// 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/plugins/plugin/go.d/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 +*/ + +func parseDHCPRangeValue(s string) (r string) { + if strings.Contains(s, "ra-stateless") { + return "" + } + + s = strings.ReplaceAll(s, " ", "") + + var start, end net.IP + parts := strings.Split(s, ",") + + for _, v := range parts { + if start == nil { + start = net.ParseIP(v) + continue + } + if end = net.ParseIP(v); end == nil || iprange.New(start, end) == nil { + return "" + } + return fmt.Sprintf("%s-%s", start, end) + } + + return "" +} + +/* +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) { + s = strings.ReplaceAll(s, " ", "") + + 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/plugin/go.d/modules/dnsmasq_dhcp/testdata/config.json b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/config.json new file mode 100644 index 000000000..6df6faec6 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/config.yaml b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/config.yaml new file mode 100644 index 000000000..4a03e6db8 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.conf new file mode 100644 index 000000000..4cf77478e --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d/.dnsmasq.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d/.dnsmasq.conf new file mode 100644 index 000000000..b9ca78218 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv4.any b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv4.any new file mode 100644 index 000000000..300faa28e --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv6.any b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d/dnsmasqv6.any new file mode 100644 index 000000000..414d6819f --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv4.any b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv4.any new file mode 100644 index 000000000..24a742797 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv6.any b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/dnsmasqv6.any new file mode 100644 index 000000000..4ae70f0b2 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/~dnsmasq.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d2/~dnsmasq.conf new file mode 100644 index 000000000..dc58bf9d8 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasq.bak b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasq.bak new file mode 100644 index 000000000..c3897671a --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv4.any b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv4.any new file mode 100644 index 000000000..a55ac969a --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv6.any b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d3/dnsmasqv6.any new file mode 100644 index 000000000..4bc6cf10f --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasq.other b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasq.other new file mode 100644 index 000000000..18fe1ac53 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv4.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv4.conf new file mode 100644 index 000000000..1493b8009 --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv6.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.d4/dnsmasqv6.conf new file mode 100644 index 000000000..389c2c95b --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.leases b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq.leases new file mode 100644 index 000000000..606e74fba --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq2.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq2.conf new file mode 100644 index 000000000..bd1766adb --- /dev/null +++ b/src/go/plugin/go.d/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/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq3.conf b/src/go/plugin/go.d/modules/dnsmasq_dhcp/testdata/dnsmasq3.conf new file mode 100644 index 000000000..3475544b5 --- /dev/null +++ b/src/go/plugin/go.d/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 |