From b0f1292a51be16e1f134f3c9ae6521f98204e434 Mon Sep 17 00:00:00 2001 From: Michael Boudreau Date: Fri, 2 Feb 2018 11:06:09 -0800 Subject: [PATCH] Add new ldap_response input plugin --- plugins/inputs/all/all.go | 1 + plugins/inputs/ldap_response/README.md | 84 ++++++++ plugins/inputs/ldap_response/ldap_response.go | 202 ++++++++++++++++++ .../ldap_response/ldap_response_test.go | 148 +++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 plugins/inputs/ldap_response/README.md create mode 100644 plugins/inputs/ldap_response/ldap_response.go create mode 100644 plugins/inputs/ldap_response/ldap_response_test.go diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index aaf5b6ae74dbc..85d210a15bf52 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -43,6 +43,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/kafka_consumer_legacy" _ "github.com/influxdata/telegraf/plugins/inputs/kapacitor" _ "github.com/influxdata/telegraf/plugins/inputs/kubernetes" + _ "github.com/influxdata/telegraf/plugins/inputs/ldap_response" _ "github.com/influxdata/telegraf/plugins/inputs/leofs" _ "github.com/influxdata/telegraf/plugins/inputs/logparser" _ "github.com/influxdata/telegraf/plugins/inputs/lustre2" diff --git a/plugins/inputs/ldap_response/README.md b/plugins/inputs/ldap_response/README.md new file mode 100644 index 0000000000000..e55d1650914fa --- /dev/null +++ b/plugins/inputs/ldap_response/README.md @@ -0,0 +1,84 @@ +# Openldap Input Plugin + +This plugin gathers metrics from OpenLDAP's cn=Monitor backend. + +### Configuration: + +To use this plugin you must enable the [monitoring](https://www.openldap.org/devel/admin/monitoringslapd.html) backend. + +```toml +[[inputs.openldap]] + host = "localhost" + port = 389 + + # ldaps, starttls, or no encryption. default is an empty string, disabling all encryption. + # note that port will likely need to be changed to 636 for ldaps + # valid options: "" | "starttls" | "ldaps" + ssl = "" + + # skip peer certificate verification. Default is false. + insecure_skip_verify = false + + # Path to PEM-encoded Root certificate to use to verify server certificate + ssl_ca = "/etc/ssl/certs.pem" + + # dn/password to bind with. If bind_dn is empty, an anonymous bind is performed. + bind_dn = "" + bind_password = "" +``` + +### Measurements & Fields: + +All **monitorCounter**, **monitorOpInitiated**, and **monitorOpCompleted** attributes are gathered based on this LDAP query: + +```(|(objectClass=monitorCounterObject)(objectClass=monitorOperation))``` + +Metric names are based on their entry DN. + +Metrics for the **monitorOp*** attributes have **_initiated** and **_completed** added to the base name. + +An OpenLDAP 2.4 server will provide these metrics: + +- openldap + - max_file_descriptors_connections + - current_connections + - total_connections + - abandon_operations_completed + - abandon_operations_initiated + - add_operations_completed + - add_operations_initiated + - bind_operations_completed + - bind_operations_initiated + - compare_operations_completed + - compare_operations_initiated + - delete_operations_completed + - delete_operations_initiated + - extended_operations_completed + - extended_operations_initiated + - modify_operations_completed + - modify_operations_initiated + - modrdn_operations_completed + - modrdn_operations_initiated + - search_operations_completed + - search_operations_initiated + - unbind_operations_completed + - unbind_operations_initiated + - bytes_statistics + - entries_statistics + - pdu_statistics + - referrals_statistics + - read_waiters + - write_waiters + +### Tags: + +- server= # value from config +- port= # value from config + +### Example Output: + +``` +$ telegraf -config telegraf.conf -input-filter openldap -test --debug +* Plugin: inputs.openldap, Collection 1 +> openldap,server=localhost,port=389,host=zirzla search_operations_completed=2i,delete_operations_completed=0i,read_waiters=1i,total_connections=1004i,bind_operations_completed=3i,unbind_operations_completed=3i,referrals_statistics=0i,current_connections=1i,bind_operations_initiated=3i,compare_operations_completed=0i,add_operations_completed=2i,delete_operations_initiated=0i,unbind_operations_initiated=3i,search_operations_initiated=3i,add_operations_initiated=2i,max_file_descriptors_connections=4096i,abandon_operations_initiated=0i,write_waiters=0i,modrdn_operations_completed=0i,abandon_operations_completed=0i,pdu_statistics=23i,modify_operations_initiated=0i,bytes_statistics=1660i,entries_statistics=17i,compare_operations_initiated=0i,modrdn_operations_initiated=0i,extended_operations_completed=0i,modify_operations_completed=0i,extended_operations_initiated=0i 1499990455000000000 +``` diff --git a/plugins/inputs/ldap_response/ldap_response.go b/plugins/inputs/ldap_response/ldap_response.go new file mode 100644 index 0000000000000..a2d1c7ae1d9be --- /dev/null +++ b/plugins/inputs/ldap_response/ldap_response.go @@ -0,0 +1,202 @@ +package ldap_response + +import ( + "fmt" + "strconv" + "strings" + "time" + + "gopkg.in/ldap.v2" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/plugins/inputs" +) + +type Ldap struct { + Host string + Port int + Ssl string + InsecureSkipVerify bool + SslCa string + BindDn string + BindPassword string + SearchBase string + SearchFilter string + SearchAttributes []string +} + +const sampleConfig string = ` + host = "localhost" + port = 389 + + # ldaps, starttls, or no encryption. default is an empty string, disabling all encryption. + # note that port will likely need to be changed to 636 for ldaps + # valid options: "" | "starttls" | "ldaps" + ssl = "" + + # skip peer certificate verification. Default is false. + insecure_skip_verify = false + + # Path to PEM-encoded Root certificate to use to verify server certificate + ssl_ca = "/etc/ssl/certs.pem" + + # dn/password to bind with. If bind_dn is empty, an anonymous bind is performed. + bind_dn = "" + bind_password = "" + + # base entry for searches + search_base = "" + + # ldap search to perform. If search_filter is empty, the bind_dn is used. + search_filter = "" + + # the attributes to return as fields + search_attributes = [ + "attribute1", + "attribute2", + ] +` + +var DefaultSearchFilter = "(objectClass=*)" +var DefaultSearchAttributes = []string{"objectclass"} + +func (l *Ldap) SampleConfig() string { + return sampleConfig +} + +func (l *Ldap) Description() string { + return "LDAP Response Input Plugin" +} + +// return an initialized Ldap +func NewLdap() *Ldap { + return &Ldap{ + Host: "localhost", + Port: 389, + } +} + +// gather metrics +func (l *Ldap) Gather(acc telegraf.Accumulator) error { + var err error + var server *ldap.Conn + beforeConnect := time.Now() + if l.Ssl != "" { + // build tls config + tlsConfig, err := internal.GetTLSConfig("", "", l.SslCa, l.InsecureSkipVerify) + if err != nil { + acc.AddError(err) + return nil + } + if l.Ssl == "ldaps" { + server, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port), tlsConfig) + if err != nil { + acc.AddError(err) + return nil + } + } else if l.Ssl == "starttls" { + server, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port)) + if err != nil { + acc.AddError(err) + return nil + } + err = server.StartTLS(tlsConfig) + } else { + acc.AddError(fmt.Errorf("Invalid setting for ssl: %s", l.Ssl)) + return nil + } + } else { + server, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port)) + } + afterConnect := time.Now() + + if err != nil { + acc.AddError(err) + return nil + } + defer server.Close() + + // username/password bind + beforeBind := time.Now() + if l.BindDn != "" && l.BindPassword != "" { + err = server.Bind(l.BindDn, l.BindPassword) + if err != nil { + acc.AddError(err) + return nil + } + } + afterBind := time.Now() + + if l.SearchFilter == "" { + l.SearchFilter = DefaultSearchFilter + } + if len(l.SearchAttributes) == 0 { + l.SearchAttributes = DefaultSearchAttributes + } + + searchRequest := ldap.NewSearchRequest( + l.SearchBase, + ldap.ScopeSingleLevel, + ldap.NeverDerefAliases, + 1000, + 60, + false, + l.SearchFilter, + l.SearchAttributes, + nil, + ) + + beforeSearch := time.Now() + sr, err := server.Search(searchRequest) + afterSearch := time.Now() + if err != nil { + acc.AddError(err) + return nil + } + + fields := map[string]interface{}{ + "connect_time_ms": float64(afterConnect.Sub(beforeConnect).Nanoseconds()) / 1000 / 1000, + "bind_time_ms": float64(afterBind.Sub(beforeBind).Nanoseconds()) / 1000 / 1000, + "query_time_ms": float64(afterSearch.Sub(beforeSearch).Nanoseconds()) / 1000 / 1000, + "total_time_ms": float64(afterSearch.Sub(beforeConnect).Nanoseconds()) / 1000 / 1000, + } + + gatherSearchResult(fields, sr, l, acc) + + return nil +} + +func gatherSearchResult(fields map[string]interface{}, sr *ldap.SearchResult, l *Ldap, acc telegraf.Accumulator) { + tags := map[string]string{ + "server": l.Host, + "port": strconv.Itoa(l.Port), + } + for _, entry := range sr.Entries { + metricName := dnToMetric(entry.DN, l.SearchBase) + for _, attr := range entry.Attributes { + if len(attr.Values[0]) >= 1 { + if v, err := strconv.ParseInt(attr.Values[0], 10, 64); err == nil { + fields[metricName+attr.Name] = v + } + } + } + } + acc.AddFields("ldap_response", fields, tags) + return +} + +// Convert a DN to metric name, eg cn=Read,cn=Waiters,cn=Monitor to read_waiters +func dnToMetric(dn, searchBase string) string { + metricName := strings.Trim(dn, " ") + metricName = strings.Replace(metricName, " ", "_", -1) + metricName = strings.ToLower(metricName) + metricName = strings.TrimPrefix(metricName, "cn=") + metricName = strings.Replace(metricName, strings.ToLower(searchBase), "", -1) + metricName = strings.Replace(metricName, "cn=", "_", -1) + return strings.Replace(metricName, ",", "", -1) +} + +func init() { + inputs.Add("ldap_response", func() telegraf.Input { return NewLdap() }) +} diff --git a/plugins/inputs/ldap_response/ldap_response_test.go b/plugins/inputs/ldap_response/ldap_response_test.go new file mode 100644 index 0000000000000..d15ed5103cfbf --- /dev/null +++ b/plugins/inputs/ldap_response/ldap_response_test.go @@ -0,0 +1,148 @@ +package ldap_response + +import ( + "fmt" + "strconv" + "testing" + + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + ldap "gopkg.in/ldap.v2" +) + +func TestLdapMockResult(t *testing.T) { + var acc testutil.Accumulator + + mockSearchResult := ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "cn=manager,cn=config", + Attributes: []*ldap.EntryAttribute{{Name: "someEntry", Values: []string{"1"}}}, + }, + }, + Referrals: []string{}, + Controls: []ldap.Control{}, + } + + l := &Ldap{ + Host: "localhost", + Port: 389, + SearchAttributes: []string{"someEntry"}, + } + + fields := map[string]interface{}{ + "query_time_ms": float64(2), + "connect_time_ms": float64(2), + "bind_time_ms": float64(2), + "total_time_ms": float64(2), + } + gatherSearchResult(fields, &mockSearchResult, l, &acc) + commonTests(t, l, &acc) +} + +func TestLdapBind(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + l := &Ldap{ + Host: testutil.GetLocalHost(), + Port: 389, + Ssl: "", + InsecureSkipVerify: true, + BindDn: "cn=manager,cn=config", + BindPassword: "secret", + SearchBase: "cn=Monitor", + } + + var acc testutil.Accumulator + err := l.Gather(&acc) + require.NoError(t, err) + commonTests(t, l, &acc) +} + +func commonTests(t *testing.T, l *Ldap, acc *testutil.Accumulator) { + assert.Empty(t, acc.Errors, "Expecting accumulator to have no errors") + assert.True(t, acc.HasFloatField("ldap_response", "connect_time_ms"), "Expeting connect_time_ms field to be present") + assert.True(t, acc.HasFloatField("ldap_response", "bind_time_ms"), "Expeting bind_time_ms field to be present") + assert.True(t, acc.HasFloatField("ldap_response", "query_time_ms"), "Expeting query_time_ms field to be present") + assert.True(t, acc.HasFloatField("ldap_response", "total_time_ms"), "Expeting total_time_ms field to be present") + assert.True(t, acc.HasMeasurement("ldap_response"), "Expecting a measurement called 'ldap_response'") + assert.Equal(t, l.Host, acc.TagValue("ldap_response", "server"), fmt.Sprintf("Expecting a tag value of server=%v", l.Host)) + assert.Equal(t, strconv.Itoa(l.Port), acc.TagValue("ldap_response", "port"), fmt.Sprintf("Expecting a tag value of port=%v", l.Port)) +} + +func TestLdapNoConnection(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Ldap{ + Host: "nosuchhost", + Port: 389, + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) // test that we didn't return an error + assert.Zero(t, acc.NFields()) // test that we didn't return any fields + assert.NotEmpty(t, acc.Errors) // test that we set an error +} +func TestLdapStartTLS(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Ldap{ + Host: testutil.GetLocalHost(), + Port: 389, + Ssl: "starttls", + InsecureSkipVerify: true, + SearchBase: "cn=Monitor", + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) + commonTests(t, o, &acc) +} + +func TestLdapLDAPS(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Ldap{ + Host: testutil.GetLocalHost(), + Port: 636, + Ssl: "ldaps", + InsecureSkipVerify: true, + SearchBase: "cn=Monitor", + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) + commonTests(t, o, &acc) +} + +func TestLdapInvalidSSL(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Ldap{ + Host: testutil.GetLocalHost(), + Port: 636, + Ssl: "invalid", + InsecureSkipVerify: true, + SearchBase: "cn=Monitor", + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) // test that we didn't return an error + assert.Zero(t, acc.NFields()) // test that we didn't return any fields + assert.NotEmpty(t, acc.Errors) // test that we set an error +}