diff --git a/datadog_checks_base/datadog_checks/checks/win/wmi/__init__.py b/datadog_checks_base/datadog_checks/checks/win/wmi/__init__.py new file mode 100644 index 0000000000000..3e29a81615551 --- /dev/null +++ b/datadog_checks_base/datadog_checks/checks/win/wmi/__init__.py @@ -0,0 +1,342 @@ +# (C) Datadog, Inc. 2018 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from collections import namedtuple + +from datadog_checks.checks import AgentCheck +from .sampler import WMISampler + +WMIMetric = namedtuple('WMIMetric', ['name', 'value', 'tags']) + + +class InvalidWMIQuery(Exception): + """ + Invalid WMI Query. + """ + pass + + +class MissingTagBy(Exception): + """ + WMI query returned multiple rows but no `tag_by` value was given. + """ + pass + + +class TagQueryUniquenessFailure(Exception): + """ + 'Tagging query' did not return or returned multiple results. + """ + pass + + +class WinWMICheck(AgentCheck): + """ + WMI check. + + Windows only. + """ + def __init__(self, name, init_config, agentConfig, instances): + AgentCheck.__init__(self, name, init_config, agentConfig, instances) + self.wmi_samplers = {} + self.wmi_props = {} + + def _format_tag_query(self, sampler, wmi_obj, tag_query): + """ + Format `tag_query` or raise on incorrect parameters. + """ + try: + link_source_property = int(wmi_obj[tag_query[0]]) + target_class = tag_query[1] + link_target_class_property = tag_query[2] + target_property = tag_query[3] + except IndexError: + self.log.error( + u"Wrong `tag_queries` parameter format. " + "Please refer to the configuration file for more information.") + raise + except TypeError: + self.log.error( + u"Incorrect 'link source property' in `tag_queries` parameter:" + " `{wmi_property}` is not a property of `{wmi_class}`".format( + wmi_property=tag_query[0], + wmi_class=sampler.class_name, + ) + ) + raise + + return target_class, target_property, [{link_target_class_property: link_source_property}] + + def _raise_on_invalid_tag_query_result(self, sampler, wmi_obj, tag_query): + """ + """ + target_property = sampler.property_names[0] + target_class = sampler.class_name + + if len(sampler) != 1: + message = "no result was returned" + if len(sampler): + message = "multiple results returned (one expected)" + + self.log.warning( + u"Failed to extract a tag from `tag_queries` parameter: {reason}." + " wmi_object={wmi_obj} - query={tag_query}".format( + reason=message, + wmi_obj=wmi_obj, tag_query=tag_query, + ) + ) + raise TagQueryUniquenessFailure + + if sampler[0][target_property] is None: + self.log.error( + u"Incorrect 'target property' in `tag_queries` parameter:" + " `{wmi_property}` is empty or is not a property" + "of `{wmi_class}`".format( + wmi_property=target_property, + wmi_class=target_class, + ) + ) + raise TypeError + + def _get_tag_query_tag(self, sampler, wmi_obj, tag_query): + """ + Design a query based on the given WMIObject to extract a tag. + + Returns: tag or TagQueryUniquenessFailure exception. + """ + self.log.debug( + u"`tag_queries` parameter found." + " wmi_object={wmi_obj} - query={tag_query}".format( + wmi_obj=wmi_obj, tag_query=tag_query, + ) + ) + + # Extract query information + target_class, target_property, filters = \ + self._format_tag_query(sampler, wmi_obj, tag_query) + + # Create a specific sampler + tag_query_sampler = WMISampler( + self.log, + target_class, [target_property], + filters=filters, + **sampler.connection + ) + + tag_query_sampler.sample() + + # Extract tag + self._raise_on_invalid_tag_query_result(tag_query_sampler, wmi_obj, tag_query) + + link_value = str(tag_query_sampler[0][target_property]).lower() + + tag = "{tag_name}:{tag_value}".format( + tag_name=target_property.lower(), + tag_value="_".join(link_value.split()) + ) + + self.log.debug(u"Extracted `tag_queries` tag: '{tag}'".format(tag=tag)) + return tag + + def _extract_metrics(self, wmi_sampler, tag_by, tag_queries, constant_tags): + """ + Extract and tag metrics from the WMISampler. + + Raise when multiple WMIObject were returned by the sampler with no `tag_by` specified. + + Returns: List of WMIMetric + ``` + [ + WMIMetric("freemegabytes", 19742, ["name:_total"]), + WMIMetric("avgdiskbytesperwrite", 1536, ["name:c:"]), + ] + ``` + """ + if len(wmi_sampler) > 1 and not tag_by: + raise MissingTagBy( + u"WMI query returned multiple rows but no `tag_by` value was given." + " class={wmi_class} - properties={wmi_properties} - filters={filters}".format( + wmi_class=wmi_sampler.class_name, wmi_properties=wmi_sampler.property_names, + filters=wmi_sampler.filters, + ) + ) + + metrics = [] + tag_by = tag_by.lower() + + for wmi_obj in wmi_sampler: + tags = list(constant_tags) if constant_tags else [] + + # Tag with `tag_queries` parameter + for query in tag_queries: + try: + tags.append(self._get_tag_query_tag(wmi_sampler, wmi_obj, query)) + except TagQueryUniquenessFailure: + continue + + for wmi_property, wmi_value in wmi_obj.iteritems(): + # Tag with `tag_by` parameter + if wmi_property == tag_by: + tag_value = str(wmi_value).lower() + if tag_queries and tag_value.find("#") > 0: + tag_value = tag_value[:tag_value.find("#")] + + tags.append( + "{name}:{value}".format( + name=tag_by, value=tag_value + ) + ) + continue + + # No metric extraction on 'Name' property + if wmi_property == 'name': + continue + + try: + metrics.append(WMIMetric(wmi_property, float(wmi_value), tags)) + except ValueError: + self.log.warning(u"When extracting metrics with WMI, found a non digit value" + " for property '{0}'.".format(wmi_property)) + continue + except TypeError: + self.log.warning(u"When extracting metrics with WMI, found a missing property" + " '{0}'".format(wmi_property)) + continue + return metrics + + def _submit_metrics(self, metrics, metric_name_and_type_by_property): + """ + Resolve metric names and types and submit it. + """ + for metric in metrics: + if metric.name not in metric_name_and_type_by_property: + # Only report the metrics that were specified in the configration + # Ignore added properties like 'Timestamp_Sys100NS', `Frequency_Sys100NS`, etc ... + continue + + metric_name, metric_type = metric_name_and_type_by_property[metric.name] + try: + func = getattr(self, metric_type.lower()) + except AttributeError: + raise Exception(u"Invalid metric type: {0}".format(metric_type)) + + func(metric_name, metric.value, metric.tags) + + def _get_instance_key(self, host, namespace, wmi_class, other=None): + """ + Return an index key for a given instance. Useful for caching. + """ + if other: + return "{host}:{namespace}:{wmi_class}-{other}".format( + host=host, namespace=namespace, wmi_class=wmi_class, other=other + ) + + return "{host}:{namespace}:{wmi_class}".format( + host=host, namespace=namespace, wmi_class=wmi_class, + ) + + def _get_wmi_sampler(self, instance_key, wmi_class, properties, tag_by="", **kwargs): + """ + Create and cache a WMISampler for the given (class, properties) + """ + properties = properties + [tag_by] if tag_by else properties + + if instance_key not in self.wmi_samplers: + wmi_sampler = WMISampler(self.log, wmi_class, properties, **kwargs) + self.wmi_samplers[instance_key] = wmi_sampler + + return self.wmi_samplers[instance_key] + + def _get_wmi_properties(self, instance_key, metrics, tag_queries): + """ + Create and cache a (metric name, metric type) by WMI property map and a property list. + """ + if instance_key not in self.wmi_props: + metric_name_by_property = dict( + (wmi_property.lower(), (metric_name, metric_type)) + for wmi_property, metric_name, metric_type in metrics + ) + properties = map(lambda x: x[0], metrics + tag_queries) + self.wmi_props[instance_key] = (metric_name_by_property, properties) + + return self.wmi_props[instance_key] + + +def from_time(year=None, month=None, day=None, hours=None, minutes=None, seconds=None, microseconds=None, timezone=None): + """Convenience wrapper to take a series of date/time elements and return a WMI time + of the form `yyyymmddHHMMSS.mmmmmm+UUU`. All elements may be int, string or + omitted altogether. If omitted, they will be replaced in the output string + by a series of stars of the appropriate length. + :param year: The year element of the date/time + :param month: The month element of the date/time + :param day: The day element of the date/time + :param hours: The hours element of the date/time + :param minutes: The minutes element of the date/time + :param seconds: The seconds element of the date/time + :param microseconds: The microseconds element of the date/time + :param timezone: The timeezone element of the date/time + :returns: A WMI datetime string of the form: `yyyymmddHHMMSS.mmmmmm+UUU` + """ + def str_or_stars(i, length): + if i is None: + return "*" * length + else: + return str(i).rjust(length, "0") + + wmi_time = "" + wmi_time += str_or_stars(year, 4) + wmi_time += str_or_stars(month, 2) + wmi_time += str_or_stars(day, 2) + wmi_time += str_or_stars(hours, 2) + wmi_time += str_or_stars(minutes, 2) + wmi_time += str_or_stars(seconds, 2) + wmi_time += "." + wmi_time += str_or_stars(microseconds, 6) + if timezone is None: + wmi_time += "+" + else: + try: + int(timezone) + except ValueError: + wmi_time += "+" + else: + if timezone >= 0: + wmi_time += "+" + else: + wmi_time += "-" + timezone = abs(timezone) + wmi_time += str_or_stars(timezone, 3) + + return wmi_time + + +def to_time(wmi_time): + """Convenience wrapper to take a WMI datetime string of the form + yyyymmddHHMMSS.mmmmmm+UUU and return a 9-tuple containing the + individual elements, or None where string contains placeholder + stars. + + :param wmi_time: The WMI datetime string in `yyyymmddHHMMSS.mmmmmm+UUU` format + + :returns: A 9-tuple of (year, month, day, hours, minutes, seconds, microseconds, timezone) + """ + + def int_or_none(s, start, end): + try: + return int(s[start:end]) + except ValueError: + return None + + year = int_or_none(wmi_time, 0, 4) + month = int_or_none(wmi_time, 4, 6) + day = int_or_none(wmi_time, 6, 8) + hours = int_or_none(wmi_time, 8, 10) + minutes = int_or_none(wmi_time, 10, 12) + seconds = int_or_none(wmi_time, 12, 14) + microseconds = int_or_none(wmi_time, 15, 21) + timezone = wmi_time[22:] + + if timezone == "***": + timezone = None + + return year, month, day, hours, minutes, seconds, microseconds, timezone diff --git a/datadog_checks_base/datadog_checks/checks/win/wmi/counter_type.py b/datadog_checks_base/datadog_checks/checks/win/wmi/counter_type.py new file mode 100644 index 0000000000000..db7e8f7112c06 --- /dev/null +++ b/datadog_checks_base/datadog_checks/checks/win/wmi/counter_type.py @@ -0,0 +1,143 @@ +# (C) Datadog, Inc. 2018 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +""" +Implementation of WMI calculators a few `CounterType`(s). + + +Protocol. + + Use MSDN to lookup the class, property and counter type to determine the + appropriate calculator. + + For example, "Win32_PerfRawData_PerfOS_Memory" is located at: + https://msdn.microsoft.com/en-us/library/aa394314(v=vs.85).aspx + and the "CacheBytes" property has a CounterType = 65792 which + can be determined from: + https://msdn.microsoft.com/en-us/library/aa389383(v=vs.85).aspx + The CounterType 65792 (PERF_COUNTER_LARGE_RAWCOUNT) is defined here: + https://technet.microsoft.com/en-us/library/cc780300(v=ws.10).aspx + + From: https://msdn.microsoft.com/en-us/library/aa389383(v=vs.85).aspx + +Original discussion thread: https://github.com/DataDog/dd-agent/issues/1952 +Credits to @TheCloudlessSky (https://github.com/TheCloudlessSky) +""" + +_counter_type_calculators = {} + + +class UndefinedCalculator(Exception): + """ + No calculator is defined for the given CounterType. + """ + pass + + +def calculator(counter_type): + """ + A decorator that assign a counter_type to its calculator. + """ + def set_calculator(func): + _counter_type_calculators[counter_type] = func + return func + return set_calculator + + +def get_calculator(counter_type): + """ + Return the calculator associated with the counter_type when it exists. + + Raise a UndefinedCalculator exception otherwise. + """ + try: + return _counter_type_calculators[counter_type] + except KeyError: + raise UndefinedCalculator + + +def get_raw(previous, current, property_name): + """ + Returns the vanilla RAW property value. + + Not associated with any counter_type. Used to fallback when no calculator + is defined for a given counter_type. + """ + return current[property_name] + + +@calculator(65536) +def calculate_perf_counter_rawcount(previous, current, property_name): + """ + PERF_COUNTER_RAWCOUNT + + https://technet.microsoft.com/en-us/library/cc757032(v=ws.10).aspx + """ + return current[property_name] + + +@calculator(65792) +def calculate_perf_counter_large_rawcount(previous, current, property_name): + """ + PERF_COUNTER_LARGE_RAWCOUNT + + https://technet.microsoft.com/en-us/library/cc780300(v=ws.10).aspx + """ + return current[property_name] + + +@calculator(542180608) +def calculate_perf_100nsec_timer(previous, current, property_name): + """ + PERF_100NSEC_TIMER + + https://technet.microsoft.com/en-us/library/cc728274(v=ws.10).aspx + """ + n0 = previous[property_name] + n1 = current[property_name] + d0 = previous["Timestamp_Sys100NS"] + d1 = current["Timestamp_Sys100NS"] + + if n0 is None or n1 is None: + return + + return (n1 - n0) / (d1 - d0) * 100 + + +@calculator(272696576) +def calculate_perf_counter_bulk_count(previous, current, property_name): + """ + PERF_COUNTER_BULK_COUNT + + https://technet.microsoft.com/en-us/library/cc757486(v=ws.10).aspx + """ + n0 = previous[property_name] + n1 = current[property_name] + d0 = previous["Timestamp_Sys100NS"] + d1 = current["Timestamp_Sys100NS"] + f = current["Frequency_Sys100NS"] + + if n0 is None or n1 is None: + return + + return (n1 - n0) / ((d1 - d0) / f) + + +@calculator(272696320) +def calculate_perf_counter_counter(previous, current, property_name): + """ + PERF_COUNTER_COUNTER + + https://technet.microsoft.com/en-us/library/cc740048(v=ws.10).aspx + """ + n0 = previous[property_name] + n1 = current[property_name] + d0 = previous["Timestamp_Sys100NS"] + d1 = current["Timestamp_Sys100NS"] + f = current["Frequency_Sys100NS"] + + if n0 is None or n1 is None: + return + + return (n1 - n0) / ((d1 - d0) / f) diff --git a/datadog_checks_base/datadog_checks/checks/win/wmi/sampler.py b/datadog_checks_base/datadog_checks/checks/win/wmi/sampler.py new file mode 100644 index 0000000000000..5e9162d3898fd --- /dev/null +++ b/datadog_checks_base/datadog_checks/checks/win/wmi/sampler.py @@ -0,0 +1,549 @@ +# (C) Datadog, Inc. 2018 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# pylint: disable=E0401 +""" +A lightweight Python WMI module wrapper built on top of `pywin32` and `win32com` extensions. + +**Specifications** +* Based on top of the `pywin32` and `win32com` third party extensions only +* Compatible with `Raw`* and `Formatted` Performance Data classes + * Dynamically resolve properties' counter types + * Hold the previous/current `Raw` samples to compute/format new values* +* Fast and lightweight + * Avoid queries overhead + * Cache connections and qualifiers + * Use `wbemFlagForwardOnly` flag to improve enumeration/memory performance + +*\* `Raw` data formatting relies on the avaibility of the corresponding calculator. +Please refer to `checks.lib.wmi.counter_type` for more information* + +Original discussion thread: https://github.com/DataDog/dd-agent/issues/1952 +Credits to @TheCloudlessSky (https://github.com/TheCloudlessSky) +""" +from copy import deepcopy +from itertools import izip +import pywintypes + +import pythoncom +from win32com.client import Dispatch + +from datadog_checks.utils.timeout import TimeoutException, timeout +from .counter_type import UndefinedCalculator, get_calculator, get_raw + + +class CaseInsensitiveDict(dict): + def __setitem__(self, key, value): + super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) + + def __getitem__(self, key): + return super(CaseInsensitiveDict, self).__getitem__(key.lower()) + + def __contains__(self, key): + return super(CaseInsensitiveDict, self).__contains__(key.lower()) + + def get(self, key): + return super(CaseInsensitiveDict, self).get(key.lower()) + + +class ProviderArchitectureMeta(type): + """ + Metaclass for ProviderArchitecture. + """ + def __contains__(cls, provider): + """ + Support `Enum` style `contains`. + """ + return provider in cls._AVAILABLE_PROVIDER_ARCHITECTURES + + +class ProviderArchitecture(object): + """ + Enumerate WMI Provider Architectures. + """ + __metaclass__ = ProviderArchitectureMeta + + # Available Provider Architecture(s) + DEFAULT = 0 + _32BIT = 32 + _64BIT = 64 + _AVAILABLE_PROVIDER_ARCHITECTURES = frozenset([DEFAULT, _32BIT, _64BIT]) + + +class WMISampler(object): + """ + WMI Sampler. + """ + # Properties + _provider = None + _formatted_filters = None + + # Type resolution state + _property_counter_types = None + + # Samples + _current_sample = None + _previous_sample = None + + # Sampling state + _sampling = False + + def __init__(self, logger, class_name, property_names, filters="", host="localhost", + namespace="root\\cimv2", provider=None, + username="", password="", and_props=[], timeout_duration=10): + self.logger = logger + + # Connection information + self.host = host + self.namespace = namespace + self.provider = provider + self.username = username + self.password = password + + self.is_raw_perf_class = "_PERFRAWDATA_" in class_name.upper() + + # Sampler settings + # WMI class, properties, filters and counter types + # Include required properties for making calculations with raw + # performance counters: + # https://msdn.microsoft.com/en-us/library/aa394299(v=vs.85).aspx + if self.is_raw_perf_class: + property_names.extend([ + "Timestamp_Sys100NS", + "Frequency_Sys100NS", + # IMPORTANT: To improve performance and since they're currently + # not needed, do not include the other Timestamp/Frequency + # properties: + # - Timestamp_PerfTime + # - Timestamp_Object + # - Frequency_PerfTime + # - Frequency_Object" + ]) + + self.class_name = class_name + self.property_names = property_names + self.filters = filters + self._and_props = and_props + self._timeout_duration = timeout_duration + self._query = timeout(timeout_duration)(self._query) + + @property + def provider(self): + """ + Return the WMI provider. + """ + return self._provider + + @provider.setter + def provider(self, value): + """ + Validate and set a WMI provider. Default to `ProviderArchitecture.DEFAULT` + """ + result = None + + # `None` defaults to `ProviderArchitecture.DEFAULT` + defaulted_value = value or ProviderArchitecture.DEFAULT + + try: + parsed_value = int(defaulted_value) + except ValueError: + pass + else: + if parsed_value in ProviderArchitecture: + result = parsed_value + + if result is None: + self.logger.error( + u"Invalid '%s' WMI Provider Architecture. The parameter is ignored.", value + ) + + self._provider = result or ProviderArchitecture.DEFAULT + + @property + def connection(self): + """ + A property to retrieve the sampler connection information. + """ + return { + 'host': self.host, + 'namespace': self.namespace, + 'username': self.username, + 'password': self.password, + } + + @property + def connection_key(self): + """ + Return an index key used to cache the sampler connection. + """ + return "{host}:{namespace}:{username}".format( + host=self.host, + namespace=self.namespace, + username=self.username + ) + + @property + def formatted_filters(self): + """ + Cache and return filters as a comprehensive WQL clause. + """ + if not self._formatted_filters: + filters = deepcopy(self.filters) + self._formatted_filters = self._format_filter(filters, self._and_props) + return self._formatted_filters + + def reset_filter(self, new_filters): + self.filters = new_filters + # get rid of the formatted filters so they'll be recalculated + self._formatted_filters = None + + def sample(self): + """ + Compute new samples. + """ + self._sampling = True + + try: + if self.is_raw_perf_class and not self._previous_sample: + self._current_sample = self._query() + + self._previous_sample = self._current_sample + self._current_sample = self._query() + except TimeoutException: + self.logger.debug( + u"Query timeout after {timeout}s".format( + timeout=self._timeout_duration + ) + ) + raise + else: + self._sampling = False + + def __len__(self): + """ + Return the number of WMI Objects in the current sample. + """ + # No data is returned while sampling + if self._sampling: + raise TypeError( + u"Sampling `WMISampler` object has no len()" + ) + + return len(self._current_sample) + + def __iter__(self): + """ + Iterate on the current sample's WMI Objects and format the property values. + """ + # No data is returned while sampling + if self._sampling: + raise TypeError( + u"Sampling `WMISampler` object is not iterable" + ) + + if self.is_raw_perf_class: + # Format required + for previous_wmi_object, current_wmi_object in \ + izip(self._previous_sample, self._current_sample): + formatted_wmi_object = self._format_property_values( + previous_wmi_object, + current_wmi_object + ) + yield formatted_wmi_object + else: + # No format required + for wmi_object in self._current_sample: + yield wmi_object + + def __getitem__(self, index): + """ + Get the specified formatted WMI Object from the current sample. + """ + if self.is_raw_perf_class: + previous_wmi_object = self._previous_sample[index] + current_wmi_object = self._current_sample[index] + formatted_wmi_object = self._format_property_values( + previous_wmi_object, + current_wmi_object + ) + return formatted_wmi_object + else: + return self._current_sample[index] + + def __eq__(self, other): + """ + Equality operator is based on the current sample. + """ + return self._current_sample == other + + def __str__(self): + """ + Stringify the current sample's WMI Objects. + """ + return str(self._current_sample) + + def _get_property_calculator(self, counter_type): + """ + Return the calculator for the given `counter_type`. + Fallback with `get_raw`. + """ + calculator = get_raw + try: + calculator = get_calculator(counter_type) + except UndefinedCalculator: + self.logger.warning( + u"Undefined WMI calculator for counter_type {counter_type}." + " Values are reported as RAW.".format( + counter_type=counter_type + ) + ) + + return calculator + + def _format_property_values(self, previous, current): + """ + Format WMI Object's RAW data based on the previous sample. + + Do not override the original WMI Object ! + """ + formatted_wmi_object = CaseInsensitiveDict() + + for property_name, property_raw_value in current.iteritems(): + counter_type = self._property_counter_types.get(property_name) + property_formatted_value = property_raw_value + + if counter_type: + calculator = self._get_property_calculator(counter_type) + property_formatted_value = calculator(previous, current, property_name) + + formatted_wmi_object[property_name] = property_formatted_value + + return formatted_wmi_object + + def get_connection(self): + """ + Create a new WMI connection + """ + self.logger.debug( + u"Connecting to WMI server " + u"(host={host}, namespace={namespace}, provider={provider}, username={username})." + .format( + host=self.host, namespace=self.namespace, + provider=self.provider, username=self.username + ) + ) + + # Initialize COM for the current thread + # WARNING: any python COM object (locator, connection, etc) created in a thread + # shouldn't be used in other threads (can lead to memory/handle leaks if done + # without a deep knowledge of COM's threading model). Because of this and given + # that we run each query in its own thread, we don't cache connections + additional_args = [] + pythoncom.CoInitialize() + + if self.provider != ProviderArchitecture.DEFAULT: + context = Dispatch("WbemScripting.SWbemNamedValueSet") + context.Add("__ProviderArchitecture", self.provider) + additional_args = [None, "", 128, context] + + locator = Dispatch("WbemScripting.SWbemLocator") + connection = locator.ConnectServer( + self.host, self.namespace, self.username, self.password, *additional_args + ) + + return connection + + @staticmethod + def _format_filter(filters, and_props=[]): + """ + Transform filters to a comprehensive WQL `WHERE` clause. + + Builds filter from a filter list. + - filters: expects a list of dicts, typically: + - [{'Property': value},...] or + - [{'Property': (comparison_op, value)},...] + + NOTE: If we just provide a value we defailt to '=' comparison operator. + Otherwise, specify the operator in a tuple as above: (comp_op, value) + If we detect a wildcard character ('%') we will override the operator + to use LIKE + """ + def build_where_clause(fltr): + f = fltr.pop() + wql = "" + while f: + prop, value = f.popitem() + + if isinstance(value, tuple): + oper = value[0] + value = value[1] + elif isinstance(value, basestring) and '%' in value: + oper = 'LIKE' + else: + oper = '=' + + if isinstance(value, list): + if not len(value): + continue + + internal_filter = map(lambda x: + (prop, x) if isinstance(x, tuple) + else (prop, ('LIKE', x)) if '%' in x + else (prop, (oper, x)), value) + + bool_op = ' OR ' + for p in and_props: + if p.lower() in prop.lower(): + bool_op = ' AND ' + break + + clause = bool_op.join(['{0} {1} \'{2}\''.format(k, v[0], v[1]) if isinstance(v,tuple) + else '{0} = \'{1}\''.format(k,v) + for k,v in internal_filter]) + + if bool_op.strip() == 'OR': + wql += "( {clause} )".format( + clause=clause) + else: + wql += "{clause}".format( + clause=clause) + + else: + wql += "{property} {cmp} '{constant}'".format( + property=prop, + cmp=oper, + constant=value) + if f: + wql += " AND " + + # empty list skipped + if wql.endswith(" AND "): + wql = wql[:-5] + + if len(fltr) == 0: + return "( {clause} )".format(clause=wql) + + return "( {clause} ) OR {more}".format( + clause=wql, + more=build_where_clause(fltr) + ) + + + if not filters: + return "" + + return " WHERE {clause}".format(clause=build_where_clause(filters)) + + def _query(self): # pylint: disable=E0202 + """ + Query WMI using WMI Query Language (WQL) & parse the results. + + Returns: List of WMI objects or `TimeoutException`. + """ + formated_property_names = ",".join(self.property_names) + wql = "Select {property_names} from {class_name}{filters}".format( + property_names=formated_property_names, + class_name=self.class_name, + filters=self.formatted_filters, + ) + 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 + + 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 + + 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) + results = [] + + return results + + def _parse_results(self, raw_results, includes_qualifiers): + """ + Parse WMI query results in a more comprehensive form. + + Returns: List of WMI objects + ``` + [ + { + 'freemegabytes': 19742.0, + 'name': 'C:', + 'avgdiskbytesperwrite': 1536.0 + }, { + 'freemegabytes': 19742.0, + 'name': 'D:', + 'avgdiskbytesperwrite': 1536.0 + } + ] + ``` + """ + results = [] + for res in raw_results: + # Ensure all properties are available. Use case-insensitivity + # because some properties are returned with different cases. + item = CaseInsensitiveDict() + for prop_name in self.property_names: + item[prop_name] = None + + for wmi_property in res.Properties_: + # IMPORTANT: To improve performance, only access the Qualifiers + # if the "CounterType" hasn't already been cached. + should_get_qualifier_type = ( + includes_qualifiers and + wmi_property.Name not in self._property_counter_types + ) + + if should_get_qualifier_type: + + # Can't index into "Qualifiers_" for keys that don't exist + # without getting an exception. + qualifiers = dict((q.Name, q.Value) for q in wmi_property.Qualifiers_) + + # Some properties like "Name" and "Timestamp_Sys100NS" do + # not have a "CounterType" (since they're not a counter). + # Therefore, they're ignored. + if "CounterType" in qualifiers: + counter_type = qualifiers["CounterType"] + self._property_counter_types[wmi_property.Name] = counter_type + + self.logger.debug( + u"Caching property qualifier CounterType: " + "{class_name}.{property_names} = {counter_type}" + .format( + class_name=self.class_name, + property_names=wmi_property.Name, + counter_type=counter_type, + ) + ) + else: + self.logger.debug( + u"CounterType qualifier not found for {class_name}.{property_names}" + .format( + class_name=self.class_name, + property_names=wmi_property.Name, + ) + ) + + try: + item[wmi_property.Name] = float(wmi_property.Value) + except (TypeError, ValueError): + item[wmi_property.Name] = wmi_property.Value + + results.append(item) + return results