diff --git a/Godeps b/Godeps index a4f2725a2..aa4a5b0e8 100644 --- a/Godeps +++ b/Godeps @@ -59,8 +59,10 @@ github.com/zensqlmonitor/go-mssqldb ffe5510c6fa5e15e6d983210ab501c815b56b363 golang.org/x/crypto dc137beb6cce2043eb6b5f223ab8bf51c32459f4 golang.org/x/net f2499483f923065a842d38eb4c7f1927e6fc6e6d golang.org/x/text 506f9d5c962f284575e88337e7d9296d27e729d3 +gopkg.in/asn1-ber.v1 4e86f4367175e39f69d9358a5f17b4dda270378d gopkg.in/fatih/pool.v2 6e328e67893eb46323ad06f0e92cb9536babbabc gopkg.in/gorethink/gorethink.v3 7ab832f7b65573104a555d84a27992ae9ea1f659 +gopkg.in/ldap.v2 8168ee085ee43257585e50c6441aadf54ecb2c9f gopkg.in/mgo.v2 3f83fa5005286a7fe593b055f0d7771a7dce4655 gopkg.in/olivere/elastic.v5 3113f9b9ad37509fe5f8a0e5e91c96fdc4435e26 gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6 diff --git a/Makefile b/Makefile index a22d2bf1a..17a947f56 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,11 @@ docker-run: docker run --name mqtt -p "1883:1883" -d ncarlier/mqtt docker run --name riemann -p "5555:5555" -d stealthly/docker-riemann docker run --name nats -p "4222:4222" -d nats + docker run --name openldap \ + -e SLAPD_CONFIG_ROOTDN="cn=manager,cn=config" \ + -e SLAPD_CONFIG_ROOTPW="secret" \ + -p "389:389" -p "636:636" \ + -d cobaugh/openldap-alpine # Run docker containers necessary for CircleCI unit tests docker-run-circle: @@ -88,11 +93,16 @@ docker-run-circle: docker run --name mqtt -p "1883:1883" -d ncarlier/mqtt docker run --name riemann -p "5555:5555" -d stealthly/docker-riemann docker run --name nats -p "4222:4222" -d nats + docker run --name openldap \ + -e SLAPD_CONFIG_ROOTDN="cn=manager,cn=config" \ + -e SLAPD_CONFIG_ROOTPW="secret" \ + -p "389:389" -p "636:636" \ + -d cobaugh/openldap-alpine # Kill all docker containers, ignore errors docker-kill: - -docker kill nsq aerospike redis rabbitmq postgres memcached mysql zookeeper kafka mqtt riemann nats elasticsearch - -docker rm nsq aerospike redis rabbitmq postgres memcached mysql zookeeper kafka mqtt riemann nats elasticsearch + -docker kill nsq aerospike redis rabbitmq postgres memcached mysql zookeeper kafka mqtt riemann nats elasticsearch openldap + -docker rm nsq aerospike redis rabbitmq postgres memcached mysql zookeeper kafka mqtt riemann nats elasticsearch openldap # Run full unit tests using docker containers (includes setup and teardown) test: vet docker-kill docker-run diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index 72663069f..da0312201 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -76,8 +76,10 @@ works: - golang.org/x/crypto [BSD](https://github.com/golang/crypto/blob/master/LICENSE) - golang.org/x/net [BSD](https://go.googlesource.com/net/+/master/LICENSE) - golang.org/x/text [BSD](https://go.googlesource.com/text/+/master/LICENSE) +- gopkg.in/asn1-ber.v1 [MIT](https://github.com/go-asn1-ber/asn1-ber/blob/v1.2/LICENSE) - gopkg.in/dancannon/gorethink.v1 [APACHE](https://github.com/dancannon/gorethink/blob/v1.1.2/LICENSE) - gopkg.in/fatih/pool.v2 [MIT](https://github.com/fatih/pool/blob/v2.0.0/LICENSE) +- gopkg.in/ldap.v2 [MIT](https://github.com/go-ldap/ldap/blob/v2.5.0/LICENSE) - gopkg.in/mgo.v2 [BSD](https://github.com/go-mgo/mgo/blob/v2/LICENSE) - gopkg.in/olivere/elastic.v5 [MIT](https://github.com/olivere/elastic/blob/v5.0.38/LICENSE) - gopkg.in/yaml.v2 [APACHE](https://github.com/go-yaml/yaml/blob/v2/LICENSE) diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 9bc7afaff..0a68235b8 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -57,6 +57,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/nsq_consumer" _ "github.com/influxdata/telegraf/plugins/inputs/nstat" _ "github.com/influxdata/telegraf/plugins/inputs/ntpq" + _ "github.com/influxdata/telegraf/plugins/inputs/openldap" _ "github.com/influxdata/telegraf/plugins/inputs/passenger" _ "github.com/influxdata/telegraf/plugins/inputs/phpfpm" _ "github.com/influxdata/telegraf/plugins/inputs/ping" diff --git a/plugins/inputs/openldap/README.md b/plugins/inputs/openldap/README.md new file mode 100644 index 000000000..a57aabb84 --- /dev/null +++ b/plugins/inputs/openldap/README.md @@ -0,0 +1,82 @@ +# Openldap Input Plugin + +This plugin gathers metrics from OpenLDAP's cn=Monitor backend. + +### Configuration: + +```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/openldap/openldap.go b/plugins/inputs/openldap/openldap.go new file mode 100644 index 000000000..a70cfd13a --- /dev/null +++ b/plugins/inputs/openldap/openldap.go @@ -0,0 +1,178 @@ +package openldap + +import ( + "fmt" + "strconv" + "strings" + + "gopkg.in/ldap.v2" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/plugins/inputs" +) + +type Openldap struct { + Host string + Port int + Ssl string + InsecureSkipVerify bool + SslCa string + BindDn string + BindPassword 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 = "" +` + +var searchBase = "cn=Monitor" +var searchFilter = "(|(objectClass=monitorCounterObject)(objectClass=monitorOperation))" +var searchAttrs = []string{"monitorCounter", "monitorOpInitiated", "monitorOpCompleted"} +var attrTranslate = map[string]string{ + "monitorCounter": "", + "monitorOpInitiated": "_initiated", + "monitorOpCompleted": "_completed", +} + +func (o *Openldap) SampleConfig() string { + return sampleConfig +} + +func (o *Openldap) Description() string { + return "OpenLDAP cn=Monitor plugin" +} + +// return an initialized Openldap +func NewOpenldap() *Openldap { + return &Openldap{ + Host: "localhost", + Port: 389, + Ssl: "", + InsecureSkipVerify: false, + SslCa: "", + BindDn: "", + BindPassword: "", + } +} + +// gather metrics +func (o *Openldap) Gather(acc telegraf.Accumulator) error { + var err error + var l *ldap.Conn + if o.Ssl != "" { + // build tls config + tlsConfig, err := internal.GetTLSConfig("", "", o.SslCa, o.InsecureSkipVerify) + if err != nil { + acc.AddError(err) + return nil + } + if o.Ssl == "ldaps" { + l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port), tlsConfig) + if err != nil { + acc.AddError(err) + return nil + } + } else if o.Ssl == "starttls" { + l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port)) + if err != nil { + acc.AddError(err) + return nil + } + err = l.StartTLS(tlsConfig) + } else { + acc.AddError(fmt.Errorf("Invalid setting for ssl: %s", o.Ssl)) + return nil + } + } else { + l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port)) + } + + if err != nil { + acc.AddError(err) + return nil + } + defer l.Close() + + // username/password bind + if o.BindDn != "" && o.BindPassword != "" { + err = l.Bind(o.BindDn, o.BindPassword) + if err != nil { + acc.AddError(err) + return nil + } + } + + searchRequest := ldap.NewSearchRequest( + searchBase, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + searchFilter, + searchAttrs, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + acc.AddError(err) + return nil + } + + gatherSearchResult(sr, o, acc) + + return nil +} + +func gatherSearchResult(sr *ldap.SearchResult, o *Openldap, acc telegraf.Accumulator) { + fields := map[string]interface{}{} + tags := map[string]string{ + "server": o.Host, + "port": strconv.Itoa(o.Port), + } + for _, entry := range sr.Entries { + metricName := dnToMetric(entry.DN, 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+attrTranslate[attr.Name]] = v + } + } + } + } + acc.AddFields("openldap", 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("openldap", func() telegraf.Input { return NewOpenldap() }) +} diff --git a/plugins/inputs/openldap/openldap_test.go b/plugins/inputs/openldap/openldap_test.go new file mode 100644 index 000000000..489ef8636 --- /dev/null +++ b/plugins/inputs/openldap/openldap_test.go @@ -0,0 +1,150 @@ +package openldap + +import ( + "gopkg.in/ldap.v2" + "strconv" + "testing" + + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOpenldapMockResult(t *testing.T) { + var acc testutil.Accumulator + + mockSearchResult := ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "cn=Total,cn=Connections,cn=Monitor", + Attributes: []*ldap.EntryAttribute{{Name: "monitorCounter", Values: []string{"1"}}}, + }, + }, + Referrals: []string{}, + Controls: []ldap.Control{}, + } + + o := &Openldap{ + Host: "localhost", + Port: 389, + } + + gatherSearchResult(&mockSearchResult, o, &acc) + commonTests(t, o, &acc) +} + +func TestOpenldapNoConnection(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Openldap{ + 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 TestOpenldapGeneratesMetrics(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Openldap{ + Host: testutil.GetLocalHost(), + Port: 389, + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) + commonTests(t, o, &acc) +} + +func TestOpenldapStartTLS(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Openldap{ + Host: testutil.GetLocalHost(), + Port: 389, + Ssl: "starttls", + InsecureSkipVerify: true, + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) + commonTests(t, o, &acc) +} + +func TestOpenldapLDAPS(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Openldap{ + Host: testutil.GetLocalHost(), + Port: 636, + Ssl: "ldaps", + InsecureSkipVerify: true, + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) + commonTests(t, o, &acc) +} + +func TestOpenldapInvalidSSL(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Openldap{ + Host: testutil.GetLocalHost(), + Port: 636, + Ssl: "invalid", + InsecureSkipVerify: true, + } + + 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 TestOpenldapBind(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + o := &Openldap{ + Host: testutil.GetLocalHost(), + Port: 389, + Ssl: "", + InsecureSkipVerify: true, + BindDn: "cn=manager,cn=config", + BindPassword: "secret", + } + + var acc testutil.Accumulator + err := o.Gather(&acc) + require.NoError(t, err) + commonTests(t, o, &acc) +} + +func commonTests(t *testing.T, o *Openldap, acc *testutil.Accumulator) { + assert.Empty(t, acc.Errors, "accumulator had no errors") + assert.True(t, acc.HasMeasurement("openldap"), "Has a measurement called 'openldap'") + assert.Equal(t, o.Host, acc.TagValue("openldap", "server"), "Has a tag value of server=o.Host") + assert.Equal(t, strconv.Itoa(o.Port), acc.TagValue("openldap", "port"), "Has a tag value of port=o.Port") + assert.True(t, acc.HasInt64Field("openldap", "total_connections"), "Has an integer field called total_connections") +}