From e21f2de8b83452608d847da47b09a2b22fedec98 Mon Sep 17 00:00:00 2001
From: Vlasta Hajek <vlastimil.hajek@bonitoo.io>
Date: Mon, 7 Aug 2017 23:36:15 +0200
Subject: [PATCH] Add Windows Services input plugin (#3023)

---
 Godeps                                        |   2 +-
 Makefile                                      |   1 +
 plugins/inputs/all/all.go                     |   1 +
 plugins/inputs/win_services/README.md         |  68 +++++++
 plugins/inputs/win_services/win_services.go   | 183 ++++++++++++++++++
 .../win_services_integration_test.go          | 115 +++++++++++
 .../win_services/win_services_notwindows.go   |   3 +
 .../inputs/win_services/win_services_test.go  | 179 +++++++++++++++++
 8 files changed, 551 insertions(+), 1 deletion(-)
 create mode 100644 plugins/inputs/win_services/README.md
 create mode 100644 plugins/inputs/win_services/win_services.go
 create mode 100644 plugins/inputs/win_services/win_services_integration_test.go
 create mode 100644 plugins/inputs/win_services/win_services_notwindows.go
 create mode 100644 plugins/inputs/win_services/win_services_test.go

diff --git a/Godeps b/Godeps
index 0fb97bc8..48f9138e 100644
--- a/Godeps
+++ b/Godeps
@@ -76,7 +76,7 @@ github.com/yuin/gopher-lua 66c871e454fcf10251c61bf8eff02d0978cae75a
 github.com/zensqlmonitor/go-mssqldb ffe5510c6fa5e15e6d983210ab501c815b56b363
 golang.org/x/crypto dc137beb6cce2043eb6b5f223ab8bf51c32459f4
 golang.org/x/net f2499483f923065a842d38eb4c7f1927e6fc6e6d
-golang.org/x/sys a646d33e2ee3172a661fc09bca23bb4889a41bc8
+golang.org/x/sys 739734461d1c916b6c72a63d7efda2b27edb369f
 golang.org/x/text 506f9d5c962f284575e88337e7d9296d27e729d3
 gopkg.in/asn1-ber.v1 4e86f4367175e39f69d9358a5f17b4dda270378d
 gopkg.in/fatih/pool.v2 6e328e67893eb46323ad06f0e92cb9536babbabc
diff --git a/Makefile b/Makefile
index 6e9a2b28..9e92de04 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,7 @@ test:
 test-windows:
 	go test ./plugins/inputs/ping/...
 	go test ./plugins/inputs/win_perf_counters/...
+	go test ./plugins/inputs/win_services/...
 
 lint:
 	go vet ./...
diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go
index 84c320fe..dd3c178d 100644
--- a/plugins/inputs/all/all.go
+++ b/plugins/inputs/all/all.go
@@ -88,6 +88,7 @@ import (
 	_ "github.com/influxdata/telegraf/plugins/inputs/varnish"
 	_ "github.com/influxdata/telegraf/plugins/inputs/webhooks"
 	_ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters"
+	_ "github.com/influxdata/telegraf/plugins/inputs/win_services"
 	_ "github.com/influxdata/telegraf/plugins/inputs/zfs"
 	_ "github.com/influxdata/telegraf/plugins/inputs/zipkin"
 	_ "github.com/influxdata/telegraf/plugins/inputs/zookeeper"
diff --git a/plugins/inputs/win_services/README.md b/plugins/inputs/win_services/README.md
new file mode 100644
index 00000000..4aa9e6b8
--- /dev/null
+++ b/plugins/inputs/win_services/README.md
@@ -0,0 +1,68 @@
+# Telegraf Plugin: win_services
+Input plugin to report Windows services info.
+
+It requires that Telegraf must be running under the administrator privileges.
+### Configuration:
+
+```toml
+[[inputs.win_services]]
+  ## Names of the services to monitor. Leave empty to monitor all the available services on the host
+  service_names = [
+    "LanmanServer",
+    "TermService",
+  ]
+```
+
+### Measurements & Fields:
+
+- win_services
+    - state : integer
+    - startup_mode : integer
+
+The `state` field can have the following values:
+- 1 - stopped
+- 2 - start pending
+- 3 - stop pending
+- 4 - running
+- 5 - continue pending
+- 6 - pause pending 
+- 7 - paused
+
+The `startup_mode` field can have the following values:
+- 0 - boot start
+- 1 - system start
+- 2 - auto start
+- 3 - demand start
+- 4 - disabled   
+
+### Tags:
+
+- All measurements have the following tags:
+    - service_name
+    - display_name
+
+### Example Output:
+```
+* Plugin: inputs.win_services, Collection 1
+> win_services,host=WIN2008R2H401,display_name=Server,service_name=LanmanServer state=4i,startup_mode=2i 1500040669000000000
+> win_services,display_name=Remote\ Desktop\ Services,service_name=TermService,host=WIN2008R2H401 state=1i,startup_mode=3i 1500040669000000000
+```
+### TICK Scripts
+
+A sample TICK script for a notification about a not running service.
+It sends a notification whenever any service changes its state to be not _running_ and when it changes that state back to _running_. 
+The notification is sent via an HTTP POST call.
+
+```
+stream
+    |from()
+        .database('telegraf')
+        .retentionPolicy('autogen')
+        .measurement('win_services')
+        .groupBy('host','service_name')
+    |alert()
+        .crit(lambda: "state" != 4)
+        .stateChangesOnly()
+        .message('Service {{ index .Tags "service_name" }} on Host {{ index .Tags "host" }} is in state {{ index .Fields "state" }} ')
+        .post('http://localhost:666/alert/service')
+```
diff --git a/plugins/inputs/win_services/win_services.go b/plugins/inputs/win_services/win_services.go
new file mode 100644
index 00000000..8e56a96d
--- /dev/null
+++ b/plugins/inputs/win_services/win_services.go
@@ -0,0 +1,183 @@
+// +build windows
+
+package win_services
+
+import (
+	"fmt"
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/plugins/inputs"
+	"golang.org/x/sys/windows/svc"
+	"golang.org/x/sys/windows/svc/mgr"
+)
+
+//WinService provides interface for svc.Service
+type WinService interface {
+	Close() error
+	Config() (mgr.Config, error)
+	Query() (svc.Status, error)
+}
+
+//WinServiceManagerProvider sets interface for acquiring manager instance, like mgr.Mgr
+type WinServiceManagerProvider interface {
+	Connect() (WinServiceManager, error)
+}
+
+//WinServiceManager provides interface for mgr.Mgr
+type WinServiceManager interface {
+	Disconnect() error
+	OpenService(name string) (WinService, error)
+	ListServices() ([]string, error)
+}
+
+//WinSvcMgr is wrapper for mgr.Mgr implementing WinServiceManager interface
+type WinSvcMgr struct {
+	realMgr *mgr.Mgr
+}
+
+func (m *WinSvcMgr) Disconnect() error {
+	return m.realMgr.Disconnect()
+}
+
+func (m *WinSvcMgr) OpenService(name string) (WinService, error) {
+	return m.realMgr.OpenService(name)
+}
+func (m *WinSvcMgr) ListServices() ([]string, error) {
+	return m.realMgr.ListServices()
+}
+
+//MgProvider is an implementation of WinServiceManagerProvider interface returning WinSvcMgr
+type MgProvider struct {
+}
+
+func (rmr *MgProvider) Connect() (WinServiceManager, error) {
+	scmgr, err := mgr.Connect()
+	if err != nil {
+		return nil, err
+	} else {
+		return &WinSvcMgr{scmgr}, nil
+	}
+}
+
+var sampleConfig = `
+  ## Names of the services to monitor. Leave empty to monitor all the available services on the host
+  service_names = [
+    "LanmanServer",
+    "TermService",
+  ]
+`
+
+var description = "Input plugin to report Windows services info."
+
+//WinServices is an implementation if telegraf.Input interface, providing info about Windows Services
+type WinServices struct {
+	ServiceNames []string `toml:"service_names"`
+	mgrProvider  WinServiceManagerProvider
+}
+
+type ServiceInfo struct {
+	ServiceName string
+	DisplayName string
+	State       int
+	StartUpMode int
+	Error       error
+}
+
+func (m *WinServices) Description() string {
+	return description
+}
+
+func (m *WinServices) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *WinServices) Gather(acc telegraf.Accumulator) error {
+
+	serviceInfos, err := listServices(m.mgrProvider, m.ServiceNames)
+
+	if err != nil {
+		return err
+	}
+
+	for _, service := range serviceInfos {
+		if service.Error == nil {
+			fields := make(map[string]interface{})
+			tags := make(map[string]string)
+
+			//display name could be empty, but still valid service
+			if len(service.DisplayName) > 0 {
+				tags["display_name"] = service.DisplayName
+			}
+			tags["service_name"] = service.ServiceName
+
+			fields["state"] = service.State
+			fields["startup_mode"] = service.StartUpMode
+
+			acc.AddFields("win_services", fields, tags)
+		} else {
+			acc.AddError(service.Error)
+		}
+	}
+
+	return nil
+}
+
+//listServices gathers info about given services. If userServices is empty, it return info about all services on current Windows host.  Any a critical error is returned.
+func listServices(mgrProv WinServiceManagerProvider, userServices []string) ([]ServiceInfo, error) {
+	scmgr, err := mgrProv.Connect()
+	if err != nil {
+		return nil, fmt.Errorf("Could not open service manager: %s", err)
+	}
+	defer scmgr.Disconnect()
+
+	var serviceNames []string
+	if len(userServices) == 0 {
+		//Listing service names from system
+		serviceNames, err = scmgr.ListServices()
+		if err != nil {
+			return nil, fmt.Errorf("Could not list services: %s", err)
+		}
+	} else {
+		serviceNames = userServices
+	}
+	serviceInfos := make([]ServiceInfo, len(serviceNames))
+
+	for i, srvName := range serviceNames {
+		serviceInfos[i] = collectServiceInfo(scmgr, srvName)
+	}
+
+	return serviceInfos, nil
+}
+
+//collectServiceInfo gathers info about a  service from WindowsAPI
+func collectServiceInfo(scmgr WinServiceManager, serviceName string) (serviceInfo ServiceInfo) {
+
+	serviceInfo.ServiceName = serviceName
+	srv, err := scmgr.OpenService(serviceName)
+	if err != nil {
+		serviceInfo.Error = fmt.Errorf("Could not open service '%s': %s", serviceName, err)
+		return
+	}
+	defer srv.Close()
+
+	srvStatus, err := srv.Query()
+	if err == nil {
+		serviceInfo.State = int(srvStatus.State)
+	} else {
+		serviceInfo.Error = fmt.Errorf("Could not query service '%s': %s", serviceName, err)
+		//finish collecting info on first found error
+		return
+	}
+
+	srvCfg, err := srv.Config()
+	if err == nil {
+		serviceInfo.DisplayName = srvCfg.DisplayName
+		serviceInfo.StartUpMode = int(srvCfg.StartType)
+	} else {
+		serviceInfo.Error = fmt.Errorf("Could not get config of service '%s': %s", serviceName, err)
+	}
+	return
+}
+
+func init() {
+	inputs.Add("win_services", func() telegraf.Input { return &WinServices{mgrProvider: &MgProvider{}} })
+}
diff --git a/plugins/inputs/win_services/win_services_integration_test.go b/plugins/inputs/win_services/win_services_integration_test.go
new file mode 100644
index 00000000..20174651
--- /dev/null
+++ b/plugins/inputs/win_services/win_services_integration_test.go
@@ -0,0 +1,115 @@
+// +build windows
+
+//these tests must be run under administrator account
+package win_services
+
+import (
+	"github.com/influxdata/telegraf/testutil"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/sys/windows/svc/mgr"
+	"testing"
+)
+
+var InvalidServices = []string{"XYZ1@", "ZYZ@", "SDF_@#"}
+var KnownServices = []string{"LanmanServer", "TermService"}
+
+func TestList(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+	services, err := listServices(&MgProvider{}, KnownServices)
+	require.NoError(t, err)
+	assert.Len(t, services, 2, "Different number of services")
+	assert.Equal(t, services[0].ServiceName, KnownServices[0])
+	assert.Nil(t, services[0].Error)
+	assert.Equal(t, services[1].ServiceName, KnownServices[1])
+	assert.Nil(t, services[1].Error)
+}
+
+func TestEmptyList(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+	services, err := listServices(&MgProvider{}, []string{})
+	require.NoError(t, err)
+	assert.Condition(t, func() bool { return len(services) > 20 }, "Too few service")
+}
+
+func TestListEr(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+	services, err := listServices(&MgProvider{}, InvalidServices)
+	require.NoError(t, err)
+	assert.Len(t, services, 3, "Different number of services")
+	for i := 0; i < 3; i++ {
+		assert.Equal(t, services[i].ServiceName, InvalidServices[i])
+		assert.NotNil(t, services[i].Error)
+	}
+}
+
+func TestGather(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+	ws := &WinServices{KnownServices, &MgProvider{}}
+	assert.Len(t, ws.ServiceNames, 2, "Different number of services")
+	var acc testutil.Accumulator
+	require.NoError(t, ws.Gather(&acc))
+	assert.Len(t, acc.Errors, 0, "There should be no errors after gather")
+
+	for i := 0; i < 2; i++ {
+		fields := make(map[string]interface{})
+		tags := make(map[string]string)
+		si := getServiceInfo(KnownServices[i])
+		fields["state"] = int(si.State)
+		fields["startup_mode"] = int(si.StartUpMode)
+		tags["service_name"] = si.ServiceName
+		tags["display_name"] = si.DisplayName
+		acc.AssertContainsTaggedFields(t, "win_services", fields, tags)
+	}
+}
+
+func TestGatherErrors(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+	ws := &WinServices{InvalidServices, &MgProvider{}}
+	assert.Len(t, ws.ServiceNames, 3, "Different number of services")
+	var acc testutil.Accumulator
+	require.NoError(t, ws.Gather(&acc))
+	assert.Len(t, acc.Errors, 3, "There should be 3 errors after gather")
+}
+
+func getServiceInfo(srvName string) *ServiceInfo {
+
+	scmgr, err := mgr.Connect()
+	if err != nil {
+		return nil
+	}
+	defer scmgr.Disconnect()
+
+	srv, err := scmgr.OpenService(srvName)
+	if err != nil {
+		return nil
+	}
+	var si ServiceInfo
+	si.ServiceName = srvName
+	srvStatus, err := srv.Query()
+	if err == nil {
+		si.State = int(srvStatus.State)
+	} else {
+		si.Error = err
+	}
+
+	srvCfg, err := srv.Config()
+	if err == nil {
+		si.DisplayName = srvCfg.DisplayName
+		si.StartUpMode = int(srvCfg.StartType)
+	} else {
+		si.Error = err
+	}
+	srv.Close()
+	return &si
+}
diff --git a/plugins/inputs/win_services/win_services_notwindows.go b/plugins/inputs/win_services/win_services_notwindows.go
new file mode 100644
index 00000000..062c11cf
--- /dev/null
+++ b/plugins/inputs/win_services/win_services_notwindows.go
@@ -0,0 +1,3 @@
+// +build !windows
+
+package win_services
diff --git a/plugins/inputs/win_services/win_services_test.go b/plugins/inputs/win_services/win_services_test.go
new file mode 100644
index 00000000..3c05e85c
--- /dev/null
+++ b/plugins/inputs/win_services/win_services_test.go
@@ -0,0 +1,179 @@
+// +build windows
+
+package win_services
+
+import (
+	"errors"
+	"fmt"
+	"github.com/influxdata/telegraf/testutil"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/sys/windows/svc"
+	"golang.org/x/sys/windows/svc/mgr"
+	"testing"
+)
+
+//testData is DD wrapper for unit testing of WinServices
+type testData struct {
+	//collection that will be returned in ListServices if service array passed into WinServices constructor is empty
+	queryServiceList     []string
+	mgrConnectError      error
+	mgrListServicesError error
+	services             []serviceTestInfo
+}
+
+type serviceTestInfo struct {
+	serviceOpenError   error
+	serviceQueryError  error
+	serviceConfigError error
+	serviceName        string
+	displayName        string
+	state              int
+	startUpMode        int
+}
+
+type FakeSvcMgr struct {
+	testData testData
+}
+
+func (m *FakeSvcMgr) Disconnect() error {
+	return nil
+}
+
+func (m *FakeSvcMgr) OpenService(name string) (WinService, error) {
+	for _, s := range m.testData.services {
+		if s.serviceName == name {
+			if s.serviceOpenError != nil {
+				return nil, s.serviceOpenError
+			} else {
+				return &FakeWinSvc{s}, nil
+			}
+		}
+	}
+	return nil, fmt.Errorf("Cannot find service %s", name)
+}
+
+func (m *FakeSvcMgr) ListServices() ([]string, error) {
+	if m.testData.mgrListServicesError != nil {
+		return nil, m.testData.mgrListServicesError
+	} else {
+		return m.testData.queryServiceList, nil
+	}
+}
+
+type FakeMgProvider struct {
+	testData testData
+}
+
+func (m *FakeMgProvider) Connect() (WinServiceManager, error) {
+	if m.testData.mgrConnectError != nil {
+		return nil, m.testData.mgrConnectError
+	} else {
+		return &FakeSvcMgr{m.testData}, nil
+	}
+}
+
+type FakeWinSvc struct {
+	testData serviceTestInfo
+}
+
+func (m *FakeWinSvc) Close() error {
+	return nil
+}
+func (m *FakeWinSvc) Config() (mgr.Config, error) {
+	if m.testData.serviceConfigError != nil {
+		return mgr.Config{}, m.testData.serviceConfigError
+	} else {
+		return mgr.Config{0, uint32(m.testData.startUpMode), 0, "", "", 0, nil, m.testData.serviceName, m.testData.displayName, "", ""}, nil
+	}
+}
+func (m *FakeWinSvc) Query() (svc.Status, error) {
+	if m.testData.serviceQueryError != nil {
+		return svc.Status{}, m.testData.serviceQueryError
+	} else {
+		return svc.Status{svc.State(m.testData.state), 0, 0, 0}, nil
+	}
+}
+
+var testErrors = []testData{
+	{nil, errors.New("Fake mgr connect error"), nil, nil},
+	{nil, nil, errors.New("Fake mgr list services error"), nil},
+	{[]string{"Fake service 1", "Fake service 2", "Fake service 3"}, nil, nil, []serviceTestInfo{
+		{errors.New("Fake srv open error"), nil, nil, "Fake service 1", "", 0, 0},
+		{nil, errors.New("Fake srv query error"), nil, "Fake service 2", "", 0, 0},
+		{nil, nil, errors.New("Fake srv config error"), "Fake service 3", "", 0, 0},
+	}},
+	{nil, nil, nil, []serviceTestInfo{
+		{errors.New("Fake srv open error"), nil, nil, "Fake service 1", "", 0, 0},
+	}},
+}
+
+func TestBasicInfo(t *testing.T) {
+
+	winServices := &WinServices{nil, &FakeMgProvider{testErrors[0]}}
+	assert.NotEmpty(t, winServices.SampleConfig())
+	assert.NotEmpty(t, winServices.Description())
+}
+
+func TestMgrErrors(t *testing.T) {
+	//mgr.connect error
+	winServices := &WinServices{nil, &FakeMgProvider{testErrors[0]}}
+	var acc1 testutil.Accumulator
+	err := winServices.Gather(&acc1)
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), testErrors[0].mgrConnectError.Error())
+
+	////mgr.listServices error
+	winServices = &WinServices{nil, &FakeMgProvider{testErrors[1]}}
+	var acc2 testutil.Accumulator
+	err = winServices.Gather(&acc2)
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), testErrors[1].mgrListServicesError.Error())
+
+	////mgr.listServices error 2
+	winServices = &WinServices{[]string{"Fake service 1"}, &FakeMgProvider{testErrors[3]}}
+	var acc3 testutil.Accumulator
+	err = winServices.Gather(&acc3)
+	require.NoError(t, err)
+	assert.Len(t, acc3.Errors, 1)
+
+}
+
+func TestServiceErrors(t *testing.T) {
+	winServices := &WinServices{nil, &FakeMgProvider{testErrors[2]}}
+	var acc1 testutil.Accumulator
+	require.NoError(t, winServices.Gather(&acc1))
+	assert.Len(t, acc1.Errors, 3)
+	//open service error
+	assert.Contains(t, acc1.Errors[0].Error(), testErrors[2].services[0].serviceOpenError.Error())
+	//query service error
+	assert.Contains(t, acc1.Errors[1].Error(), testErrors[2].services[1].serviceQueryError.Error())
+	//config service error
+	assert.Contains(t, acc1.Errors[2].Error(), testErrors[2].services[2].serviceConfigError.Error())
+
+}
+
+var testSimpleData = []testData{
+	{[]string{"Service 1", "Service 2"}, nil, nil, []serviceTestInfo{
+		{nil, nil, nil, "Service 1", "Fake service 1", 1, 2},
+		{nil, nil, nil, "Service 2", "Fake service 2", 1, 2},
+	}},
+}
+
+func TestGather2(t *testing.T) {
+	winServices := &WinServices{nil, &FakeMgProvider{testSimpleData[0]}}
+	var acc1 testutil.Accumulator
+	require.NoError(t, winServices.Gather(&acc1))
+	assert.Len(t, acc1.Errors, 0, "There should be no errors after gather")
+
+	for _, s := range testSimpleData[0].services {
+		fields := make(map[string]interface{})
+		tags := make(map[string]string)
+		fields["state"] = int(s.state)
+		fields["startup_mode"] = int(s.startUpMode)
+		tags["service_name"] = s.serviceName
+		tags["display_name"] = s.displayName
+		acc1.AssertContainsTaggedFields(t, "win_services", fields, tags)
+	}
+
+}
-- 
GitLab