Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Python 3 #2760

Merged
merged 4 commits into from
Dec 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ jobs:
- stage: test
env: CHECK=ntp
- stage: test
env: CHECK=openldap
env: CHECK=openldap PYTHON3=true
- stage: test
env: CHECK=openstack
- stage: test
Expand Down
104 changes: 55 additions & 49 deletions openldap/datadog_checks/openldap/openldap.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
# (C) Datadog, Inc. 2018
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
from copy import copy
import ldap3
import os
import ssl

from datadog_checks.checks import AgentCheck
from datadog_checks.errors import CheckException
from datadog_checks.config import is_affirmative
import ldap3

from datadog_checks.base import AgentCheck, ConfigurationError, is_affirmative

SEARCH_BASE = "cn=Monitor"
SEARCH_FILTER = "(objectClass=*)"
ATTRS = ["*", "+"]

METRIC_PREFIX = "openldap"
class OpenLDAP(AgentCheck):
METRIC_PREFIX = 'openldap'
SERVICE_CHECK_CONNECT = '{}.can_connect'.format(METRIC_PREFIX)

# Some docs here https://www.openldap.org/doc/admin24/monitoringslapd.html#Monitor%20Information
CONNECTIONS_METRICS_DN = "cn=connections,cn=monitor"
OPERATIONS_METRICS_DN = "cn=operations,cn=monitor"
STATISTICS_METRICS_DN = "cn=statistics,cn=monitor"
THREADS_METRICS_DN = "cn=threads,cn=monitor"
TIME_METRICS_DN = "cn=time,cn=monitor"
WAITERS_METRICS_DN = "cn=waiters,cn=monitor"
SEARCH_BASE = 'cn=Monitor'
SEARCH_FILTER = '(objectClass=*)'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you move these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module constants are usually slower to access

ATTRS = ['*', '+']

# Some docs here https://www.openldap.org/doc/admin24/monitoringslapd.html#Monitor%20Information
CONNECTIONS_METRICS_DN = 'cn=connections,cn=monitor'
OPERATIONS_METRICS_DN = 'cn=operations,cn=monitor'
STATISTICS_METRICS_DN = 'cn=statistics,cn=monitor'
THREADS_METRICS_DN = 'cn=threads,cn=monitor'
TIME_METRICS_DN = 'cn=time,cn=monitor'
WAITERS_METRICS_DN = 'cn=waiters,cn=monitor'

class OpenLDAP(AgentCheck):
def check(self, instance):
url, username, password, ssl_params, custom_queries, tags = self._get_instance_params(instance)

Expand All @@ -40,16 +38,16 @@ def check(self, instance):
raise ldap3.core.exceptions.LDAPBindError("Error binding to server: {}".format(conn.result))
except ldap3.core.exceptions.LDAPExceptionError as e:
self.log.exception("Could not connect to server at %s: %s", url, e)
self.service_check("{}.can_connect".format(METRIC_PREFIX), self.CRITICAL, tags=tags)
self.service_check(self.SERVICE_CHECK_CONNECT, self.CRITICAL, tags=tags)
raise

self.service_check("{}.can_connect".format(METRIC_PREFIX), self.OK, tags=tags)
self.service_check(self.SERVICE_CHECK_CONNECT, self.OK, tags=tags)
bind_time = self._get_query_time(conn)
self.gauge("{}.bind_time".format(METRIC_PREFIX), bind_time, tags=tags)
self.gauge("{}.bind_time".format(self.METRIC_PREFIX), bind_time, tags=tags)

try:
# Search Monitor database to get all metrics
conn.search(SEARCH_BASE, SEARCH_FILTER, attributes=ATTRS)
conn.search(self.SEARCH_BASE, self.SEARCH_FILTER, attributes=self.ATTRS)
self._collect_monitor_metrics(conn, tags)

# Get additional custom metrics
Expand Down Expand Up @@ -88,17 +86,19 @@ def _get_tls_object(self, ssl_params):
validate=validate,
)
else:
raise CheckException("Invalid path {} for ssl_ca_certs: no such file or directory"
.format(ssl_params["ca_certs"]))
raise ConfigurationError(
'Invalid path {} for ssl_ca_certs: no such file or directory'.format(ssl_params['ca_certs'])
)
return tls

def _get_instance_params(self, instance):
@classmethod
def _get_instance_params(cls, instance):
"""
Parse instance configuration and perform minimal verification
"""
url = instance.get("url")
if url is None:
raise CheckException("You must specify a url for your instance in `conf.yaml`")
raise ConfigurationError("You must specify a url for your instance in `conf.yaml`")
username = instance.get("username")
password = instance.get("password")
ssl_params = None
Expand All @@ -110,7 +110,7 @@ def _get_instance_params(self, instance):
"verify": is_affirmative(instance.get("ssl_verify", True))
}
custom_queries = instance.get("custom_queries", [])
tags = copy(instance.get("tags", []))
tags = list(instance.get("tags", []))
tags.append("url:{}".format(url))

return url, username, password, ssl_params, custom_queries, tags
Expand All @@ -122,17 +122,17 @@ def _collect_monitor_metrics(self, conn, tags):
for entry in conn.entries:
# Get metrics from monitor backend
dn = entry.entry_dn.lower()
if dn.endswith(CONNECTIONS_METRICS_DN):
if dn.endswith(self.CONNECTIONS_METRICS_DN):
self._handle_connections_entry(entry, tags)
elif dn.endswith(OPERATIONS_METRICS_DN):
elif dn.endswith(self.OPERATIONS_METRICS_DN):
self._handle_operations_entry(entry, tags)
elif dn.endswith(STATISTICS_METRICS_DN):
elif dn.endswith(self.STATISTICS_METRICS_DN):
self._handle_statistics_entry(entry, tags)
elif dn.endswith(THREADS_METRICS_DN):
elif dn.endswith(self.THREADS_METRICS_DN):
self._handle_threads_entry(entry, tags)
elif dn.endswith(TIME_METRICS_DN):
elif dn.endswith(self.TIME_METRICS_DN):
self._handle_time_entry(entry, tags)
elif dn.endswith(WAITERS_METRICS_DN):
elif dn.endswith(self.WAITERS_METRICS_DN):
self._handle_waiters_entry(entry, tags)

def _perform_custom_queries(self, conn, custom_queries, tags, instance):
Expand Down Expand Up @@ -179,22 +179,24 @@ def _perform_custom_queries(self, conn, custom_queries, tags, instance):

try:
# Perform the search query
res = conn.search(search_base, search_filter, attributes=attrs)
conn.search(search_base, search_filter, attributes=attrs)
except ldap3.core.exceptions.LDAPException:
self.log.exception("Unable to perform search query for %s", name)
continue

query_tags = ['query:{}'.format(name)]
query_tags.extend(tags)
query_time = self._get_query_time(conn)
results = len(conn.entries)
self.gauge("{}.query.duration".format(METRIC_PREFIX), query_time, tags=tags + ["query:{}".format(name)])
self.gauge("{}.query.entries".format(METRIC_PREFIX), results, tags=tags + ["query:{}".format(name)])
self.gauge("{}.query.duration".format(self.METRIC_PREFIX), query_time, tags=query_tags)
self.gauge("{}.query.entries".format(self.METRIC_PREFIX), results, tags=query_tags)

def _handle_connections_entry(self, entry, tags):
cn = self._extract_common_name(entry.entry_dn)
if cn in ["max_file_descriptors", "current"]:
self.gauge("{}.connections.{}".format(METRIC_PREFIX, cn), entry["monitorCounter"].value, tags=tags)
self.gauge("{}.connections.{}".format(self.METRIC_PREFIX, cn), entry["monitorCounter"].value, tags=tags)
elif cn == "total":
self.monotonic_count("{}.connections.{}".format(METRIC_PREFIX, cn),
self.monotonic_count("{}.connections.{}".format(self.METRIC_PREFIX, cn),
entry["monitorCounter"].value, tags=tags)

def _handle_operations_entry(self, entry, tags):
Expand All @@ -203,46 +205,50 @@ def _handle_operations_entry(self, entry, tags):
completed = entry["monitorOpCompleted"].value
if cn == "operations":
# the root of the "cn=operations,cn=monitor" has the total number of initiated and completed operations
self.monotonic_count("{}.operations.initiated.total".format(METRIC_PREFIX), initiated, tags=tags)
self.monotonic_count("{}.operations.completed.total".format(METRIC_PREFIX), completed, tags=tags)
self.monotonic_count("{}.operations.initiated.total".format(self.METRIC_PREFIX), initiated, tags=tags)
self.monotonic_count("{}.operations.completed.total".format(self.METRIC_PREFIX), completed, tags=tags)
else:
self.monotonic_count("{}.operations.initiated".format(METRIC_PREFIX),
self.monotonic_count("{}.operations.initiated".format(self.METRIC_PREFIX),
initiated, tags=tags + ["operation:{}".format(cn)])
self.monotonic_count("{}.operations.completed".format(METRIC_PREFIX),
self.monotonic_count("{}.operations.completed".format(self.METRIC_PREFIX),
completed, tags=tags + ["operation:{}".format(cn)])

def _handle_statistics_entry(self, entry, tags):
cn = self._extract_common_name(entry.entry_dn)
if cn != "statistics":
self.monotonic_count("{}.statistics.{}".format(METRIC_PREFIX, cn), entry["monitorCounter"].value, tags=tags)
self.monotonic_count(
'{}.statistics.{}'.format(self.METRIC_PREFIX, cn), entry['monitorCounter'].value, tags=tags
)

def _handle_threads_entry(self, entry, tags):
cn = self._extract_common_name(entry.entry_dn)
try:
value = entry["monitoredInfo"].value
except ldap3.core.exceptions.LDAPKeyError:
pass
return
if cn in ["max", "max_pending"]:
self.gauge("{}.threads.{}".format(METRIC_PREFIX, cn), value, tags=tags)
self.gauge("{}.threads.{}".format(self.METRIC_PREFIX, cn), value, tags=tags)
elif cn in ["open", "starting", "active", "pending", "backload"]:
self.gauge("{}.threads".format(METRIC_PREFIX), value, tags=tags + ["status:{}".format(cn)])
self.gauge("{}.threads".format(self.METRIC_PREFIX), value, tags=tags + ["status:{}".format(cn)])

def _handle_time_entry(self, entry, tags):
cn = self._extract_common_name(entry.entry_dn)
if cn == "uptime":
self.gauge("{}.uptime".format(METRIC_PREFIX), entry["monitoredInfo"].value, tags=tags)
self.gauge("{}.uptime".format(self.METRIC_PREFIX), entry["monitoredInfo"].value, tags=tags)

def _handle_waiters_entry(self, entry, tags):
cn = self._extract_common_name(entry.entry_dn)
if cn != "waiters":
self.gauge("{}.waiter.{}".format(METRIC_PREFIX, cn), entry["monitorCounter"].value, tags=tags)
self.gauge("{}.waiter.{}".format(self.METRIC_PREFIX, cn), entry["monitorCounter"].value, tags=tags)

def _extract_common_name(self, dn):
@classmethod
def _extract_common_name(cls, dn):
"""
extract first common name (cn) from DN that looks like "cn=max file descriptors,cn=connections,cn=monitor"
"""
dn = dn.lower().replace(" ", "_")
return dn.split(",")[0].split("=")[1]

def _get_query_time(self, conn):
@classmethod
def _get_query_time(cls, conn):
return (conn.usage.last_received_time - conn.usage.last_transmitted_time).total_seconds()
2 changes: 1 addition & 1 deletion openldap/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def get_requirements(fpath):
return f.readlines()


CHECKS_BASE_REQ = 'datadog_checks_base'
CHECKS_BASE_REQ = 'datadog-checks-base>=4.2.0'

setup(
name='datadog-openldap',
Expand Down
16 changes: 15 additions & 1 deletion openldap/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# (C) Datadog, Inc. 2018
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

import os

from datadog_checks.dev import get_docker_hostname

HERE = os.path.dirname(os.path.abspath(__file__))
HOST = get_docker_hostname()

DEFAULT_INSTANCE = {
'url': 'ldap://{}:3890'.format(HOST),
'username': 'cn=monitor,dc=example,dc=org',
'password': 'monitor',
'custom_queries': [{
'name': 'stats',
'search_base': 'cn=statistics,cn=monitor',
'search_filter': '(!(cn=Statistics))',
}],
'tags': ['test:integration']
}
2 changes: 1 addition & 1 deletion openldap/tests/compose/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.5"

services:
openldap:
image: "datadog/docker-library:openldap_2_4_44"
image: "datadog/docker-library:openldap_${OPENLDAP_VERSION}"
ports:
- "3890:389"
- "6360:636"
Expand Down
49 changes: 32 additions & 17 deletions openldap/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
# (C) Datadog, Inc. 2018
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

import os
from copy import deepcopy

import pytest

from datadog_checks.dev import docker_run, temp_dir
from datadog_checks.dev.utils import create_file
from datadog_checks.dev import TempDir, docker_run
from datadog_checks.dev.utils import create_file, file_exists
from datadog_checks.openldap import OpenLDAP
from .common import HERE
from .common import DEFAULT_INSTANCE, HERE, HOST


@pytest.fixture(scope='session')
def dd_environment():
with TempDir() as d:
host_socket_path = os.path.join(d, 'ldapi')

if not file_exists(host_socket_path):
os.chmod(d, 0o770)
create_file(host_socket_path)
os.chmod(host_socket_path, 0o640)

with docker_run(
compose_file=os.path.join(HERE, 'compose', 'docker-compose.yaml'),
env_vars={'HOST_SOCKET_DIR': d},
log_patterns='slapd starting',
):
yield DEFAULT_INSTANCE


@pytest.fixture
def check():
return OpenLDAP('openldap', {}, {})


@pytest.fixture(scope="session")
def openldap_server():
with temp_dir() as d:
host_socket_path = os.path.join(d, "ldapi")
os.chmod(d, 0o777)
create_file(host_socket_path)
os.chmod(host_socket_path, 0o777)
@pytest.fixture
def instance():
instance = deepcopy(DEFAULT_INSTANCE)
return instance

with docker_run(
compose_file=os.path.join(HERE, "compose", "docker-compose.yaml"),
env_vars={"HOST_SOCKET_DIR": d},
log_patterns="slapd starting",
):
yield host_socket_path

@pytest.fixture
def instance_ssl():
instance = deepcopy(DEFAULT_INSTANCE)
instance['url'] = 'ldaps://{}:6360'.format(HOST)
return instance
Loading