diff --git a/.travis.yml b/.travis.yml index 0082d6bd4d2ea..fa135ed34acc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/openldap/datadog_checks/openldap/openldap.py b/openldap/datadog_checks/openldap/openldap.py index 5a7c81814e8b6..58d5f66f5c006 100644 --- a/openldap/datadog_checks/openldap/openldap.py +++ b/openldap/datadog_checks/openldap/openldap.py @@ -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=*)' + 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) @@ -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 @@ -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 @@ -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 @@ -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): @@ -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): @@ -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() diff --git a/openldap/setup.py b/openldap/setup.py index d527bc1dcba4b..8f66fed630852 100644 --- a/openldap/setup.py +++ b/openldap/setup.py @@ -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', diff --git a/openldap/tests/common.py b/openldap/tests/common.py index 7b783063292fb..40c6c55194331 100644 --- a/openldap/tests/common.py +++ b/openldap/tests/common.py @@ -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'] +} diff --git a/openldap/tests/compose/docker-compose.yaml b/openldap/tests/compose/docker-compose.yaml index 8d25233c42d25..07ec4766c068f 100644 --- a/openldap/tests/compose/docker-compose.yaml +++ b/openldap/tests/compose/docker-compose.yaml @@ -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" diff --git a/openldap/tests/conftest.py b/openldap/tests/conftest.py index b01b9e4020f7b..752c0d1ebdc2f 100644 --- a/openldap/tests/conftest.py +++ b/openldap/tests/conftest.py @@ -1,15 +1,33 @@ # (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 @@ -17,17 +35,14 @@ 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 diff --git a/openldap/tests/test_check.py b/openldap/tests/test_check.py index 1c9be4c94a0da..c3466a667bf96 100644 --- a/openldap/tests/test_check.py +++ b/openldap/tests/test_check.py @@ -1,38 +1,18 @@ # (C) Datadog, Inc. 2018 # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) +import os import ldap3 import pytest -from datadog_checks.dev.docker import get_docker_hostname from datadog_checks.utils.platform import Platform pytestmark = pytest.mark.integration -@pytest.fixture -def instance(): - return { - "url": "ldap://{}:3890".format(get_docker_hostname()), - "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"] - } - - -@pytest.fixture -def instance_ssl(instance): - instance["url"] = "ldaps://{}:6360".format(get_docker_hostname()) - return instance - - -def test_check(aggregator, check, openldap_server, instance): +@pytest.mark.usefixtures('dd_environment') +def test_check(aggregator, check, instance): tags = ["url:{}".format(instance["url"]), "test:integration"] check.check(instance) aggregator.assert_service_check("openldap.can_connect", check.OK, tags=tags) @@ -81,7 +61,8 @@ def test_check(aggregator, check, openldap_server, instance): aggregator.assert_all_metrics_covered() -def test_check_ssl(aggregator, check, openldap_server, instance_ssl): +@pytest.mark.usefixtures('dd_environment') +def test_check_ssl(aggregator, check, instance_ssl): tags = ["url:{}".format(instance_ssl["url"]), "test:integration"] # Should fail certificate verification with pytest.raises(ldap3.core.exceptions.LDAPExceptionError): @@ -93,7 +74,8 @@ def test_check_ssl(aggregator, check, openldap_server, instance_ssl): aggregator.assert_service_check("openldap.can_connect", check.OK, tags=tags) -def test_check_connection_failure(aggregator, check, openldap_server, instance): +@pytest.mark.usefixtures('dd_environment') +def test_check_connection_failure(aggregator, check, instance): instance["url"] = "bad_url" tags = ["url:{}".format(instance["url"]), "test:integration"] # Should fail certificate verification @@ -103,8 +85,10 @@ def test_check_connection_failure(aggregator, check, openldap_server, instance): @pytest.mark.skipif(not Platform.is_linux(), reason='Windows sockets are not file handles') -def test_check_socket(aggregator, check, openldap_server, instance): - instance["url"] = "ldapi://{}".format(openldap_server) +@pytest.mark.usefixtures('dd_environment') +def test_check_socket(aggregator, check, instance): + host_socket_path = os.path.join(os.environ['HOST_SOCKET_DIR'], 'ldapi') + instance["url"] = "ldapi://{}".format(host_socket_path) tags = ["url:{}".format(instance["url"]), "test:integration"] check.check(instance) aggregator.assert_service_check("openldap.can_connect", check.OK, tags=tags) diff --git a/openldap/tests/test_unit.py b/openldap/tests/test_unit.py index 1c69cd10231bf..e76c9aa91b7ef 100644 --- a/openldap/tests/test_unit.py +++ b/openldap/tests/test_unit.py @@ -1,7 +1,6 @@ # (C) Datadog, Inc. 2018 # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) - import datetime import os import pytest @@ -98,7 +97,7 @@ def test__get_tls_object(check, mocker): check.log = log_mock check._get_tls_object(ssl_params) log_mock.warning.assert_called_once() - assert "Incorrect configuration" in log_mock.warning.call_args[0][0] + assert b"Incorrect configuration" in log_mock.warning.call_args[0][0] ldap3_tls_mock.assert_called_once_with( local_private_key_file=None, local_certificate_file=None, diff --git a/openldap/tox.ini b/openldap/tox.ini index 2575c0ee5a30b..563f1ce4098ee 100644 --- a/openldap/tox.ini +++ b/openldap/tox.ini @@ -2,26 +2,26 @@ minversion = 2.0 basepython = py27 envlist = + py{27,36}-{2.4} unit flake8 - openldap_2_4 [testenv] usedevelop = true -platform = linux|darwin +platform = linux|darwin|win32 deps = -e../datadog_checks_base[deps] -rrequirements-dev.txt - -[testenv:unit] commands = pip install --require-hashes -r requirements.txt - pytest -v -m"not integration" + pytest -v -m"integration" +setenv = + 2.4: OPENLDAP_VERSION=2_4_44 -[testenv:openldap_2_4] +[testenv:unit] commands = pip install --require-hashes -r requirements.txt - pytest -v -m"integration" + pytest -v -m"not integration" [testenv:flake8] skip_install = true @@ -29,5 +29,5 @@ deps = flake8 commands = flake8 . [flake8] -exclude = .eggs,.tox +exclude = .eggs,.tox,build max-line-length = 120