Skip to content
Closed
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 checks.d/iis.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def check(self, instance):
self.CLASS, properties,
filters=filters,
host=host, namespace=self.NAMESPACE, provider=provider,
username=user, password=password
username=user, password=password, mute=False
)

# Sample, extract & submit metrics
Expand Down
2 changes: 1 addition & 1 deletion checks.d/wmi_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def check(self, instance):
wmi_class, properties,
tag_by=tag_by, filters=filters,
host=host, namespace=namespace, provider=provider,
username=username, password=password,
username=username, password=password, mute=False
)

# Sample, extract & submit metrics
Expand Down
43 changes: 43 additions & 0 deletions checks/libs/wmi/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
List WMI "known" `com_errors` errors.

Translate to user intelligible exceptions.
"""

_user_exception_by_com_errors = {}


def com_error(error_id):
"""
A decorator that assigns an `error_id` to an intelligible exception.
"""
def set_exception(exception):
_user_exception_by_com_errors[error_id] = exception
return exception
return set_exception


def raise_on_com_error(error):
"""
Raise the user exception associated with the given `com_error` or
fall back to the original exception.
"""
raise _user_exception_by_com_errors.get(error[0], error)


# List of user exceptions
class WMIException(Exception):
"""
Abtract exception for WMI.
"""
def __init__(self):
"""
Use the class docstring as an exception message.
"""
super(WMIException, self).__init__(self.__doc__)


@com_error(-2147217392)
class WMIInvalidClass(WMIException):
"""WMI class is invalid."""
pass
58 changes: 39 additions & 19 deletions checks/libs/wmi/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

# project
from checks.libs.wmi.counter_type import get_calculator, get_raw, UndefinedCalculator
from checks.libs.wmi.exceptions import raise_on_com_error, WMIException
from utils.timeout import timeout, TimeoutException


Expand Down Expand Up @@ -91,8 +92,10 @@ class WMISampler(object):

def __init__(self, logger, class_name, property_names, filters="", host="localhost",
namespace="root\\cimv2", provider=None,
username="", password="", and_props=[], timeout_duration=10):
username="", password="", and_props=[],
mute=True, timeout_duration=10):
Copy link
Member

@truthbk truthbk Aug 26, 2016

Choose a reason for hiding this comment

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

I get why you have implemented the mute feature. But do we really need it? None of the checks initiate the sampler with a different value here, no? Nevermind.

Copy link
Author

Choose a reason for hiding this comment

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

The Datadog integration checks relying on wmi have mute set False. This allows the check to raise, and the exception message to be used by the dd-agent info page.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I had looked at the top of the PR yesterday, and forgot about mute being False there. Thanks for the explanation though.

self.logger = logger
self.mute = mute

# Connection information
self.host = host
Expand Down Expand Up @@ -424,13 +427,12 @@ def build_where_clause(fltr):
more=build_where_clause(fltr)
)


if not filters:
return ""

return " WHERE {clause}".format(clause=build_where_clause(filters))

def _query(self): # pylint: disable=E0202
def _query(self): # pylint: disable=E0202
"""
Query WMI using WMI Query Language (WQL) & parse the results.

Expand All @@ -444,31 +446,49 @@ def _query(self): # pylint: disable=E0202
)
self.logger.debug(u"Querying WMI: {0}".format(wql))

try:
# From: https://msdn.microsoft.com/en-us/library/aa393866(v=vs.85).aspx
flag_return_immediately = 0x10 # Default flag.
flag_forward_only = 0x20
flag_use_amended_qualifiers = 0x20000
# From: https://msdn.microsoft.com/en-us/library/aa393866(v=vs.85).aspx
flag_return_immediately = 0x10 # Default flag.
flag_forward_only = 0x20
flag_use_amended_qualifiers = 0x20000

query_flags = flag_return_immediately | flag_forward_only
query_flags = flag_return_immediately | flag_forward_only

# For the first query, cache the qualifiers to determine each
# propertie's "CounterType"
includes_qualifiers = self.is_raw_perf_class and self._property_counter_types is None
if includes_qualifiers:
self._property_counter_types = CaseInsensitiveDict()
query_flags |= flag_use_amended_qualifiers
# For the first query, cache the qualifiers to determine each
# propertie's "CounterType"
includes_qualifiers = self.is_raw_perf_class and self._property_counter_types is None
if includes_qualifiers:
self._property_counter_types = CaseInsensitiveDict()
query_flags |= flag_use_amended_qualifiers

try:
raw_results = self.get_connection().ExecQuery(wql, "WQL", query_flags)

results = self._parse_results(raw_results, includes_qualifiers=includes_qualifiers)

except pywintypes.com_error:
self.logger.warning(u"Failed to execute WMI query (%s)", wql, exc_info=True)
except pywintypes.com_error as e:
self._handle_com_error(e, wql)
results = []

return results

def _handle_com_error(self, error, wql):
"""
Attempt to translate the WMI `com_error` to something intelligible.
Raise when needed or log a warning.
"""
warning_template = u"Failed to execute WMI query ({}).".format(wql)

try:
raise_on_com_error(error)

# Translate to user exceptions
except WMIException as e:
if not self.mute:
raise
self.logger.warning(u"%s Reason:%s", warning_template, e.message)

# Unknown exceptions
except Exception:
self.logger.exception(warning_template)

def _parse_results(self, raw_results, includes_qualifiers):
"""
Parse WMI query results in a more comprehensive form.
Expand Down
14 changes: 7 additions & 7 deletions tests/checks/integration/test_wmi_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from nose.plugins.attrib import attr

# project
from checks.libs.wmi.exceptions import WMIInvalidClass
from tests.checks.common import AgentCheckTest

INSTANCE = {
Expand Down Expand Up @@ -66,17 +67,16 @@ def test_check_with_tag_queries(self):
self.assertMetricTagPrefix(metric, tag_prefix='creationdate:')

def test_invalid_class(self):
"""
WMI invalid classes raise an intelligible exception.
"""
instance = copy.deepcopy(INSTANCE)
instance['class'] = 'Unix'
logger = Mock()

self.run_check({'instances': [instance]}, mocks={'log': logger})

# A warning is logged
self.assertEquals(logger.warning.call_count, 1)

# No metrics/service check
self.coverage_report()
# An exception is raised
with self.assertRaises(WMIInvalidClass):
self.run_check({'instances': [instance]}, mocks={'log': logger})

def test_invalid_metrics(self):
instance = copy.deepcopy(INSTANCE)
Expand Down
79 changes: 78 additions & 1 deletion tests/core/test_wmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from mock import Mock, patch

# project
from checks.libs.wmi.exceptions import WMIInvalidClass
from tests.checks.common import Fixtures
from tests.core.test_wmi_exceptions import MockedWMICOMError
from utils.timeout import TimeoutException


Expand Down Expand Up @@ -349,7 +351,34 @@ def assertInPartial(self, first, second):

Note: needs to be defined for Python 2.6
"""
self.assertTrue(any(key for key in second if key.startswith(first)), "{0} not in {1}".format(first, second))
self.assertTrue(
any(key for key in second if key.startswith(first)),
"{0} not in {1}".format(first, second)
)

def _assertLogger(self, logger, message, level):
"""
Assert that the logger was called with the given level and submessage.
"""
logger_method = getattr(logger, level)

# Logger was called with the given level
self.assertTrue(logger_method.called)

# Submessage was logged
self.assertTrue([s for s in logger_method.call_args[0] if message in s])

def assertWarning(self, *args):
"""
Assert logging with the WARNING level.
"""
self._assertLogger(*args, level="warning")

def assertException(self, *args):
"""
Assert logging with the EXCEPTION level.
"""
self._assertLogger(*args, level="exception")

def getProp(self, dict, prefix):
"""
Expand Down Expand Up @@ -788,6 +817,54 @@ def test_missing_property(self):

self.assertWMISampler(wmi_raw_sampler, ["MissingProperty"], count=1)

def test_warnings_on_com_errors(self):
"""
Log a warning on WMI `com_errors`.
"""
# Mute to WMI Sampler
from checks.libs.wmi.sampler import WMISampler
logger = Mock()
wmi_sampler = WMISampler(logger, "WMI_Class", ["Property"], mute=True)

# Samples
invalid_class_com_error = MockedWMICOMError(-2147217392)
unknown_com_error = MockedWMICOMError(123456)

# Method to test
log_on_com_errors = partial(wmi_sampler._handle_com_error, wql="")

# Log WARNING an intelligible when possible
log_on_com_errors(invalid_class_com_error)
self.assertWarning(logger, "class is invalid")

# Or log ERROR the exception trace
log_on_com_errors(unknown_com_error)
self.assertException(logger, "Failed to execute WMI query")

def test_raise_on_com_errors(self):
"""
Log a warning on WMI `com_errors`.
"""
# Do not mute to WMI Sampler
from checks.libs.wmi.sampler import WMISampler
logger = Mock()
wmi_sampler = WMISampler(logger, "WMI_Class", ["Property"], mute=False)

# Samples
invalid_class_com_error = MockedWMICOMError(-2147217392)
unknown_com_error = MockedWMICOMError(123456)

# Method to test
raise_on_com_errors = partial(wmi_sampler._handle_com_error, wql="")

# Raise known exceptions
with self.assertRaises(WMIInvalidClass):
raise_on_com_errors(invalid_class_com_error)

# Log exceptions the others
raise_on_com_errors(unknown_com_error)
self.assertException(logger, "Failed to execute WMI query")


class TestIntegrationWMI(unittest.TestCase):
"""
Expand Down
55 changes: 55 additions & 0 deletions tests/core/test_wmi_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# stdlib
import unittest

# datadog
from checks.libs.wmi.exceptions import com_error, raise_on_com_error


class MockedWMICOMError(Exception):
"""
Mocking a WMI `com_error` error.
"""
def __init__(self, error_id):
super(MockedWMICOMError, self).__init__()
self.error_id = error_id

def __getitem__(self, index):
if index == 0:
return self.error_id

raise NotImplementedError


class TestWMIExceptions(unittest.TestCase):
"""
Unit testing for WMI `com_error` errors and user exceptions.
"""
def test_register_com_error(self):
"""
`com_error` decorator register and map a WMI `com_error` to an user exception.
"""
# Mock a WMI `com_error`
sample_error = MockedWMICOMError(123456)

# Map it to an user exception
@com_error(sample_error.error_id)
class WMISampleException(Exception):
"""
Sample WMI exception.
"""
pass

# Assert that the exception is raised
with self.assertRaises(WMISampleException):
raise_on_com_error(sample_error)

def test_unregistred_com_error(self):
"""
Unknown `com_error` errors remain intact.
"""
# Mock a WMI `com_error`, do not register it
sample_error = MockedWMICOMError(456789)

# Assert that the original exception is raised
with self.assertRaises(MockedWMICOMError):
raise_on_com_error(sample_error)