Skip to content

Commit 4b6606e

Browse files
authored
Merge pull request #341 from replicatedhq/laverya/systemctl-services-collector
systemctl services collector
2 parents 810b3cb + 559e18d commit 4b6606e

File tree

9 files changed

+467
-1
lines changed

9 files changed

+467
-1
lines changed

pkg/analyze/analyzer.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,16 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
6060
}
6161

6262
func NewAnalyzeResultError(analyzer HostAnalyzer, err error) []*AnalyzeResult {
63+
if analyzer != nil {
64+
return []*AnalyzeResult{{
65+
IsFail: true,
66+
Title: analyzer.Title(),
67+
Message: fmt.Sprintf("Analyzer Failed: %v", err),
68+
}}
69+
}
6370
return []*AnalyzeResult{{
6471
IsFail: true,
65-
Title: analyzer.Title(),
72+
Title: "nil analyzer",
6673
Message: fmt.Sprintf("Analyzer Failed: %v", err),
6774
}}
6875
}

pkg/analyze/host_analyzer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
3636
return &AnalyzeHostFilesystemPerformance{analyzer.FilesystemPerformance}, true
3737
case analyzer.Certificate != nil:
3838
return &AnalyzeHostCertificate{analyzer.Certificate}, true
39+
case analyzer.HostServices != nil:
40+
return &AnalyzeHostServices{analyzer.HostServices}, true
3941
default:
4042
return nil, false
4143
}

pkg/analyze/host_services.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/pkg/errors"
9+
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
10+
"github.com/replicatedhq/troubleshoot/pkg/collect"
11+
)
12+
13+
type AnalyzeHostServices struct {
14+
hostAnalyzer *troubleshootv1beta2.HostServicesAnalyze
15+
}
16+
17+
func (a *AnalyzeHostServices) Title() string {
18+
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Host Services")
19+
}
20+
21+
func (a *AnalyzeHostServices) IsExcluded() (bool, error) {
22+
return isExcluded(a.hostAnalyzer.Exclude)
23+
}
24+
25+
func (a *AnalyzeHostServices) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
26+
hostAnalyzer := a.hostAnalyzer
27+
28+
contents, err := getCollectedFileContents(collect.HostServicesPath)
29+
if err != nil {
30+
return nil, errors.Wrap(err, "failed to get collected file")
31+
}
32+
33+
var services []collect.ServiceInfo
34+
if err := json.Unmarshal(contents, &services); err != nil {
35+
return nil, errors.Wrap(err, "failed to unmarshal systemctl service info")
36+
}
37+
38+
result := AnalyzeResult{}
39+
40+
result.Title = a.Title()
41+
42+
for _, outcome := range hostAnalyzer.Outcomes {
43+
if outcome.Fail != nil {
44+
if outcome.Fail.When == "" {
45+
result.IsFail = true
46+
result.Message = outcome.Fail.Message
47+
result.URI = outcome.Fail.URI
48+
49+
return &result, nil
50+
}
51+
52+
isMatch, err := compareHostServicesConditionalToActual(outcome.Fail.When, services)
53+
if err != nil {
54+
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
55+
}
56+
57+
if isMatch {
58+
result.IsFail = true
59+
result.Message = outcome.Fail.Message
60+
result.URI = outcome.Fail.URI
61+
62+
return &result, nil
63+
}
64+
} else if outcome.Warn != nil {
65+
if outcome.Warn.When == "" {
66+
result.IsWarn = true
67+
result.Message = outcome.Warn.Message
68+
result.URI = outcome.Warn.URI
69+
70+
return &result, nil
71+
}
72+
73+
isMatch, err := compareHostServicesConditionalToActual(outcome.Warn.When, services)
74+
if err != nil {
75+
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
76+
}
77+
78+
if isMatch {
79+
result.IsWarn = true
80+
result.Message = outcome.Warn.Message
81+
result.URI = outcome.Warn.URI
82+
83+
return &result, nil
84+
}
85+
} else if outcome.Pass != nil {
86+
if outcome.Pass.When == "" {
87+
result.IsPass = true
88+
result.Message = outcome.Pass.Message
89+
result.URI = outcome.Pass.URI
90+
91+
return &result, nil
92+
}
93+
94+
isMatch, err := compareHostServicesConditionalToActual(outcome.Pass.When, services)
95+
if err != nil {
96+
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
97+
}
98+
99+
if isMatch {
100+
result.IsPass = true
101+
result.Message = outcome.Pass.Message
102+
result.URI = outcome.Pass.URI
103+
104+
return &result, nil
105+
}
106+
}
107+
}
108+
109+
return &result, nil
110+
}
111+
112+
// <service> <op> <state>
113+
// example: ufw.service = active
114+
func compareHostServicesConditionalToActual(conditional string, services []collect.ServiceInfo) (res bool, err error) {
115+
parts := strings.Split(conditional, " ")
116+
if len(parts) != 3 {
117+
return false, fmt.Errorf("expected exactly 3 parts, got %d", len(parts))
118+
}
119+
120+
switch parts[1] {
121+
case "=", "==":
122+
for _, service := range services {
123+
if isServiceMatch(service.Unit, parts[0]) {
124+
return service.Active == parts[2], nil
125+
}
126+
}
127+
return false, nil
128+
case "!=", "<>":
129+
for _, service := range services {
130+
if isServiceMatch(service.Unit, parts[0]) {
131+
return service.Active != parts[2], nil
132+
}
133+
}
134+
return false, nil
135+
}
136+
137+
return false, fmt.Errorf("unexpected operator %q", parts[1])
138+
}
139+
140+
func isServiceMatch(serviceName string, matchName string) bool {
141+
if serviceName == matchName {
142+
return true
143+
}
144+
145+
if strings.HasPrefix(serviceName, matchName) {
146+
return true
147+
}
148+
149+
return false
150+
}

pkg/analyze/host_services_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
8+
"github.com/replicatedhq/troubleshoot/pkg/collect"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestAnalyzeHostServices(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
info []collect.ServiceInfo
17+
hostAnalyzer *troubleshootv1beta2.HostServicesAnalyze
18+
result *AnalyzeResult
19+
expectErr bool
20+
}{
21+
{
22+
name: "service 'a' is active",
23+
info: []collect.ServiceInfo{
24+
{
25+
Unit: "a.service",
26+
Active: "active",
27+
},
28+
},
29+
hostAnalyzer: &troubleshootv1beta2.HostServicesAnalyze{
30+
Outcomes: []*troubleshootv1beta2.Outcome{
31+
{
32+
Fail: &troubleshootv1beta2.SingleOutcome{
33+
When: "a.service == active",
34+
Message: "the service 'a' is active",
35+
},
36+
},
37+
},
38+
},
39+
result: &AnalyzeResult{
40+
Title: "Host Services",
41+
IsFail: true,
42+
Message: "the service 'a' is active",
43+
},
44+
},
45+
{
46+
name: "connected, fail",
47+
info: []collect.ServiceInfo{
48+
{
49+
Unit: "a.service",
50+
Active: "active",
51+
},
52+
{
53+
Unit: "b.service",
54+
Active: "stopped",
55+
},
56+
},
57+
hostAnalyzer: &troubleshootv1beta2.HostServicesAnalyze{
58+
Outcomes: []*troubleshootv1beta2.Outcome{
59+
{
60+
Fail: &troubleshootv1beta2.SingleOutcome{
61+
When: "a.service != active",
62+
Message: "service 'a' is active",
63+
},
64+
},
65+
{
66+
Pass: &troubleshootv1beta2.SingleOutcome{
67+
When: "b.service != active",
68+
Message: "service 'b' is not active",
69+
},
70+
},
71+
},
72+
},
73+
result: &AnalyzeResult{
74+
Title: "Host Services",
75+
IsPass: true,
76+
Message: "service 'b' is not active",
77+
},
78+
},
79+
}
80+
for _, test := range tests {
81+
t.Run(test.name, func(t *testing.T) {
82+
req := require.New(t)
83+
b, err := json.Marshal(test.info)
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
88+
getCollectedFileContents := func(filename string) ([]byte, error) {
89+
return b, nil
90+
}
91+
92+
result, err := (&AnalyzeHostServices{test.hostAnalyzer}).Analyze(getCollectedFileContents)
93+
if test.expectErr {
94+
req.Error(err)
95+
} else {
96+
req.NoError(err)
97+
}
98+
99+
assert.Equal(t, test.result, result)
100+
})
101+
}
102+
}
103+
104+
func Test_compareHostServicesConditionalToActual(t *testing.T) {
105+
tests := []struct {
106+
name string
107+
conditional string
108+
services []collect.ServiceInfo
109+
wantRes bool
110+
wantErr bool
111+
}{
112+
{
113+
name: "match second item",
114+
conditional: "abc.service = active",
115+
services: []collect.ServiceInfo{
116+
{
117+
Unit: "first",
118+
},
119+
{
120+
Unit: "abc.service",
121+
Active: "active",
122+
},
123+
},
124+
wantRes: true,
125+
},
126+
{
127+
name: "item not in list",
128+
conditional: "abc = active",
129+
services: []collect.ServiceInfo{
130+
{
131+
Unit: "first",
132+
},
133+
},
134+
wantRes: false,
135+
},
136+
{
137+
name: "item does not match",
138+
conditional: "abc = active",
139+
services: []collect.ServiceInfo{
140+
{
141+
Unit: "abc.service",
142+
Active: "stopped",
143+
},
144+
},
145+
wantRes: false,
146+
},
147+
{
148+
name: "other operator",
149+
conditional: "abc * active",
150+
services: []collect.ServiceInfo{
151+
{
152+
Unit: "abc.service",
153+
Active: "stopped",
154+
},
155+
},
156+
wantErr: true,
157+
},
158+
}
159+
for _, tt := range tests {
160+
t.Run(tt.name, func(t *testing.T) {
161+
req := require.New(t)
162+
gotRes, err := compareHostServicesConditionalToActual(tt.conditional, tt.services)
163+
if tt.wantErr {
164+
req.Error(err)
165+
} else {
166+
req.NoError(err)
167+
req.Equal(tt.wantRes, gotRes)
168+
}
169+
})
170+
}
171+
}

pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ type CertificateAnalyze struct {
7575
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
7676
}
7777

78+
type HostServicesAnalyze struct {
79+
AnalyzeMeta `json:",inline" yaml:",inline"`
80+
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
81+
}
82+
7883
type HostAnalyze struct {
7984
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
8085
//
@@ -100,4 +105,6 @@ type HostAnalyze struct {
100105
FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
101106

102107
Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"`
108+
109+
HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
103110
}

pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ type Certificate struct {
8888
KeyPath string `json:"keyPath" yaml:"keyPath"`
8989
}
9090

91+
type HostServices struct {
92+
HostCollectorMeta `json:",inline" yaml:",inline"`
93+
}
94+
9195
type HostCollect struct {
9296
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
9397
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -103,6 +107,7 @@ type HostCollect struct {
103107
TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
104108
FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
105109
Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"`
110+
HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
106111
}
107112

108113
func (c *HostCollect) GetName() string {

0 commit comments

Comments
 (0)