From 09b1f7e4684864db0a57a0fa8b3966c1079647f9 Mon Sep 17 00:00:00 2001 From: Vlasta Hajek 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 0fb97bc8a..48f9138e8 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 6e9a2b28b..9e92de04e 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 84c320fed..dd3c178d9 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 000000000..4aa9e6b86 --- /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 000000000..8e56a96d0 --- /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 000000000..201746514 --- /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 000000000..062c11cfc --- /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 000000000..3c05e85c5 --- /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) + } + +}