Skip to content

Commit

Permalink
Add Windows service trigger_start filter to config (#16038)
Browse files Browse the repository at this point in the history
* added trigger_start option to services

* added e2e test and debug line

* added tests for trigger_start and trigger_count. added error handling

* edited tests and error handling. Added debug log lines

* edited some comments and imports

* small fixes for clarity

* edited formatting for Linters. Updated Readme and Changelog

* Changelog

* Update windows_service/README.md

edited README

Co-authored-by: Branden Clark <branden.clark@datadoghq.com>

* editing readme

* updated spec, added some comments

* updated Changelog, updated spec

---------

Co-authored-by: Branden Clark <branden.clark@datadoghq.com>
  • Loading branch information
mrafi97 and clarkb7 committed Oct 24, 2023
1 parent 67d4c0c commit d93e894
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 6 deletions.
4 changes: 4 additions & 0 deletions windows_service/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

***Added***:

* Add Windows service `trigger_start` filter to config ([#16038](https://github.com/DataDog/integrations-core/pull/16038))

## 4.6.1 / 2023-08-18 / Agent 7.48.0

***Fixed***:
Expand Down
20 changes: 20 additions & 0 deletions windows_service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ The possible values for `startup_type` are:
- `automatic`
- `automatic_delayed_start`

Beginning with Agent version 7.50, the check can select which services to monitor based on whether they have a [Service Trigger assigned][17].
Below are some examples showing possible configurations.
```yaml
# Matches all services that do not have a trigger
services:
- trigger_start: false
# Matches all services with an automatic startup type and excludes services with triggers
services:
- startup_type: automatic
trigger_start: false
# Only matches EventLog service when its startup type is automatic and has triggers
services:
- name: EventLog
startup_type: automatic
trigger_start: true
```

#### Tags

The check automatically tags the Windows service name to each service check in the `windows_service:<SERVICE>` tag. The `<SERVICE>` name in the tag uses lowercase and special characters are replaced with underscores. See [Getting Started with Tags][12] for more information.
Expand Down Expand Up @@ -114,3 +133,4 @@ If the service is present in the output, permissions are the issue. To give the
[14]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/windows-security/grant-users-rights-manage-services
[15]: https://docs.datadoghq.com/agent/guide/windows-agent-ddagent-user/
[16]: https://learn.microsoft.com/en-US/troubleshoot/windows-server/group-policy/configure-group-policies-set-security
[17]: https://learn.microsoft.com/en-us/windows/win32/services/service-trigger-events
7 changes: 6 additions & 1 deletion windows_service/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ files:
description: |
List of services to monitor e.g. Dnscache, wmiApSrv, etc.
Services can also be selectively monitored by their startup type.
Services can also be selectively monitored by their startup type and whether they have triggers
The possible values for `startup_type` are:
- disabled
- manual
- automatic
- automatic_delayed_start
The possible values for `trigger_start` are:
- true
- false
If any service is set to `ALL`, all services registered with the SCM will be monitored, and
all other patterns provided in this instance will be ignored.
Expand All @@ -38,6 +42,7 @@ files:
- <SERVICE_NAME_1>
- name: <SERVICE_NAME_2>
- startup_type: automatic
trigger_start: false
- name: disable_legacy_service_tag
description: |
Whether or not to stop submitting the tag `service` that has been renamed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ instances:
## @param services - (list of string or mapping) - required
## List of services to monitor e.g. Dnscache, wmiApSrv, etc.
##
## Services can also be selectively monitored by their startup type.
## Services can also be selectively monitored by their startup type and whether they have triggers
## The possible values for `startup_type` are:
## - disabled
## - manual
## - automatic
## - automatic_delayed_start
##
## The possible values for `trigger_start` are:
## - true
## - false
##
## If any service is set to `ALL`, all services registered with the SCM will be monitored, and
## all other patterns provided in this instance will be ignored.
##
Expand All @@ -36,6 +40,7 @@ instances:
- <SERVICE_NAME_1>
- name: <SERVICE_NAME_2>
- startup_type: automatic
trigger_start: false

## @param disable_legacy_service_tag - boolean - optional - default: false
## Whether or not to stop submitting the tag `service` that has been renamed
Expand Down
80 changes: 76 additions & 4 deletions windows_service/datadog_checks/windows_service/windows_service.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
# (C) Datadog, Inc. 2018-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import ctypes
import re

import pywintypes
import win32service
import winerror
from six import raise_from

from datadog_checks.base import AgentCheck

SERVICE_PATTERN_FLAGS = re.IGNORECASE

SERVICE_CONFIG_TRIGGER_INFO = 8


def QueryServiceConfig2W(*args):
"""
ctypes wrapper for info types not supported by pywin32
"""
if ctypes.windll.advapi32.QueryServiceConfig2W(*args) == 0:
raise ctypes.WinError()


class TriggerInfo(ctypes.Structure):
_fields_ = [("triggerCount", ctypes.c_uint32), ("pTriggers", ctypes.c_void_p), ("pReserved", ctypes.c_char_p)]


class ServiceFilter(object):
def __init__(self, name=None, startup_type=None):
def __init__(self, name=None, startup_type=None, trigger_start=None):
self.name = name
self.startup_type = startup_type
self.trigger_start = trigger_start

self._init_patterns()

Expand All @@ -34,6 +51,11 @@ def match(self, service_view):
if self.startup_type is not None:
if self.startup_type.lower() != service_view.startup_type_string().lower():
return False
if self.trigger_start is not None:
if not self.trigger_start and service_view.trigger_count > 0:
return False
elif self.trigger_start and service_view.trigger_count == 0:
return False
return True

def __str__(self):
Expand All @@ -42,6 +64,8 @@ def __str__(self):
vals.append('name={}'.format(self._name_re.pattern))
if self.startup_type is not None:
vals.append('startup_type={}'.format(self.startup_type))
if self.trigger_start is not None:
vals.append('trigger_start={}'.format(self.trigger_start))
# Example:
# - ServiceFilter(name=EventLog)
# - ServiceFilter(startup_type=automatic)
Expand Down Expand Up @@ -75,7 +99,8 @@ def from_config(cls, item, wmi_compat=False):
if name is not None and wmi_compat:
name = cls._wmi_compat_name(name)
startup_type = item.get('startup_type', None)
obj = cls(name=name, startup_type=startup_type)
trigger_start = item.get('trigger_start', None)
obj = cls(name=name, startup_type=startup_type, trigger_start=trigger_start)
else:
raise Exception("Invalid type '{}' for service".format(type(item).__name__))
return obj
Expand All @@ -99,6 +124,20 @@ def __init__(self, scm_handle, name):
self._startup_type = None
self._service_config = None
self._is_delayed_auto = None
self._trigger_count = None

def __str__(self):
vals = []
if self.name is not None:
vals.append('name={}'.format(self.name))
if self._startup_type is not None:
vals.append('startup_type={}'.format(self.startup_type_string()))
if self._trigger_count is not None:
vals.append('trigger_count={}'.format(self._trigger_count))
# Example:
# - Service(name=EventLog)
# - Service(name=Dnscache, startup_type=automatic, trigger_count=1)
return '{}({})'.format("Service", ', '.join(vals))

@property
def hSvc(self):
Expand Down Expand Up @@ -126,6 +165,37 @@ def is_delayed_auto(self):
)
return self._is_delayed_auto

@property
def trigger_count(self):
if self._trigger_count is None:
# find out how many bytes to allocate for buffer
# raise error if the error code is not ERROR_INSUFFICIENT_BUFFER
bytesneeded = ctypes.c_uint32(0)
try:
QueryServiceConfig2W(
ctypes.c_void_p(self.hSvc.handle), SERVICE_CONFIG_TRIGGER_INFO, None, 0, ctypes.byref(bytesneeded)
)
except OSError as e:
if e.winerror != winerror.ERROR_INSUFFICIENT_BUFFER:
raise

# allocate buffer and get trigger info
# raise any error from QueryServiceConfig2W
bytesBuffer = ctypes.create_string_buffer(bytesneeded.value)
QueryServiceConfig2W(
ctypes.c_void_p(self.hSvc.handle),
SERVICE_CONFIG_TRIGGER_INFO,
ctypes.byref(bytesBuffer),
bytesneeded,
ctypes.byref(bytesneeded),
)

# converting returned buffer into TriggerInfo to get trigger count
triggerStruct = TriggerInfo.from_buffer(bytesBuffer)
self._trigger_count = triggerStruct.triggerCount

return self._trigger_count

def startup_type_string(self):
startup_type_string = ''
startup_type_string = self.STARTUP_TYPE_TO_STRING.get(self.startup_type, self.STARTUP_TYPE_UNKNOWN)
Expand Down Expand Up @@ -185,12 +255,14 @@ def check(self, instance):

if 'ALL' not in services:
for service_filter in service_filters:
self.log.debug('Service Short Name: %s and Filter: %s', short_name, service_filter)
try:
if service_filter.match(service_view):
self.log.debug('Matched %s with %s', service_view, service_filter)
services_unseen.discard(service_filter.name)
break
except pywintypes.error as e:
else:
self.log.debug('Did not match %s with %s', service_view, service_filter)
except (pywintypes.error, OSError) as e:
self.log.exception("Exception at service match for %s", service_filter)
self.warning(
"Failed to query %s service config for filter %s: %s", short_name, service_filter, str(e)
Expand Down
9 changes: 9 additions & 0 deletions windows_service/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@
'disable_legacy_service_tag': True,
}
INSTANCE_ALL = {'services': ['ALL']}

INSTANCE_TRIGGER_START = {
'services': [
{'name': 'eventlog', 'startup_type': 'automatic', 'trigger_start': False},
{'name': 'dnscache', 'startup_type': 'automatic', 'trigger_start': False},
],
'tags': ['optional:tag1'],
'disable_legacy_service_tag': True,
}
5 changes: 5 additions & 0 deletions windows_service/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,8 @@ def instance_all():
@pytest.fixture
def instance_startup_type_filter():
return deepcopy(common.INSTANCE_STARTUP_TYPE_FILTER)


@pytest.fixture
def instance_trigger_start():
return deepcopy(common.INSTANCE_TRIGGER_START)
37 changes: 37 additions & 0 deletions windows_service/tests/test_windows_service.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# (C) Datadog, Inc. 2018-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import ctypes

import pytest
import pywintypes
import winerror
from mock import patch
from six import PY2

from datadog_checks.windows_service import WindowsService

Expand Down Expand Up @@ -255,6 +259,39 @@ def test_invalid_pattern_regex(aggregator, check, instance_basic_dict):
c.check(instance_basic_dict)


def test_trigger_start(aggregator, check, instance_trigger_start):
c = check(instance_trigger_start)
c.check(instance_trigger_start)
aggregator.assert_service_check(
WindowsService.SERVICE_CHECK_NAME,
status=WindowsService.OK,
tags=['windows_service:EventLog', 'optional:tag1'],
count=1,
)

aggregator.assert_service_check(
WindowsService.SERVICE_CHECK_NAME,
status=WindowsService.UNKNOWN,
tags=['windows_service:dnscache', 'optional:tag1'],
count=1,
)


def test_trigger_count_failure(aggregator, check, instance_trigger_start, caplog):
c = check(instance_trigger_start)

with patch(
'datadog_checks.windows_service.windows_service.QueryServiceConfig2W',
side_effect=[ctypes.WinError(winerror.ERROR_INSUFFICIENT_BUFFER), ctypes.WinError(1)] * 2,
):
c.check(instance_trigger_start)

if PY2:
assert 'WindowsError: [Error 1] Incorrect function' in caplog.text
else:
assert 'OSError: [WinError 1] Incorrect function' in caplog.text


@pytest.mark.e2e
def test_basic_e2e(dd_agent_check, check, instance_basic):
aggregator = dd_agent_check(instance_basic)
Expand Down

0 comments on commit d93e894

Please sign in to comment.