Merge pull request #341 from replicatedhq/laverya/systemctl-services-collector

systemctl services collector
This commit is contained in:
Andrew Lavery
2021-03-30 16:41:30 -04:00
committed by GitHub
9 changed files with 467 additions and 1 deletions

View File

@@ -60,9 +60,16 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
}
func NewAnalyzeResultError(analyzer HostAnalyzer, err error) []*AnalyzeResult {
if analyzer != nil {
return []*AnalyzeResult{{
IsFail: true,
Title: analyzer.Title(),
Message: fmt.Sprintf("Analyzer Failed: %v", err),
}}
}
return []*AnalyzeResult{{
IsFail: true,
Title: analyzer.Title(),
Title: "nil analyzer",
Message: fmt.Sprintf("Analyzer Failed: %v", err),
}}
}

View File

@@ -36,6 +36,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
return &AnalyzeHostFilesystemPerformance{analyzer.FilesystemPerformance}, true
case analyzer.Certificate != nil:
return &AnalyzeHostCertificate{analyzer.Certificate}, true
case analyzer.HostServices != nil:
return &AnalyzeHostServices{analyzer.HostServices}, true
default:
return nil, false
}

View File

@@ -0,0 +1,150 @@
package analyzer
import (
"encoding/json"
"fmt"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeHostServices struct {
hostAnalyzer *troubleshootv1beta2.HostServicesAnalyze
}
func (a *AnalyzeHostServices) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Host Services")
}
func (a *AnalyzeHostServices) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostServices) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents(collect.HostServicesPath)
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
var services []collect.ServiceInfo
if err := json.Unmarshal(contents, &services); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal systemctl service info")
}
result := AnalyzeResult{}
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
isMatch, err := compareHostServicesConditionalToActual(outcome.Fail.When, services)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
}
if isMatch {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
} else if outcome.Warn != nil {
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
isMatch, err := compareHostServicesConditionalToActual(outcome.Warn.When, services)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
}
if isMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
} else if outcome.Pass != nil {
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
isMatch, err := compareHostServicesConditionalToActual(outcome.Pass.When, services)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
}
if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
}
}
return &result, nil
}
// <service> <op> <state>
// example: ufw.service = active
func compareHostServicesConditionalToActual(conditional string, services []collect.ServiceInfo) (res bool, err error) {
parts := strings.Split(conditional, " ")
if len(parts) != 3 {
return false, fmt.Errorf("expected exactly 3 parts, got %d", len(parts))
}
switch parts[1] {
case "=", "==":
for _, service := range services {
if isServiceMatch(service.Unit, parts[0]) {
return service.Active == parts[2], nil
}
}
return false, nil
case "!=", "<>":
for _, service := range services {
if isServiceMatch(service.Unit, parts[0]) {
return service.Active != parts[2], nil
}
}
return false, nil
}
return false, fmt.Errorf("unexpected operator %q", parts[1])
}
func isServiceMatch(serviceName string, matchName string) bool {
if serviceName == matchName {
return true
}
if strings.HasPrefix(serviceName, matchName) {
return true
}
return false
}

View File

@@ -0,0 +1,171 @@
package analyzer
import (
"encoding/json"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeHostServices(t *testing.T) {
tests := []struct {
name string
info []collect.ServiceInfo
hostAnalyzer *troubleshootv1beta2.HostServicesAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "service 'a' is active",
info: []collect.ServiceInfo{
{
Unit: "a.service",
Active: "active",
},
},
hostAnalyzer: &troubleshootv1beta2.HostServicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "a.service == active",
Message: "the service 'a' is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Host Services",
IsFail: true,
Message: "the service 'a' is active",
},
},
{
name: "connected, fail",
info: []collect.ServiceInfo{
{
Unit: "a.service",
Active: "active",
},
{
Unit: "b.service",
Active: "stopped",
},
},
hostAnalyzer: &troubleshootv1beta2.HostServicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "a.service != active",
Message: "service 'a' is active",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "b.service != active",
Message: "service 'b' is not active",
},
},
},
},
result: &AnalyzeResult{
Title: "Host Services",
IsPass: true,
Message: "service 'b' is not active",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.info)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostServices{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
func Test_compareHostServicesConditionalToActual(t *testing.T) {
tests := []struct {
name string
conditional string
services []collect.ServiceInfo
wantRes bool
wantErr bool
}{
{
name: "match second item",
conditional: "abc.service = active",
services: []collect.ServiceInfo{
{
Unit: "first",
},
{
Unit: "abc.service",
Active: "active",
},
},
wantRes: true,
},
{
name: "item not in list",
conditional: "abc = active",
services: []collect.ServiceInfo{
{
Unit: "first",
},
},
wantRes: false,
},
{
name: "item does not match",
conditional: "abc = active",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "stopped",
},
},
wantRes: false,
},
{
name: "other operator",
conditional: "abc * active",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "stopped",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := require.New(t)
gotRes, err := compareHostServicesConditionalToActual(tt.conditional, tt.services)
if tt.wantErr {
req.Error(err)
} else {
req.NoError(err)
req.Equal(tt.wantRes, gotRes)
}
})
}
}

View File

@@ -75,6 +75,11 @@ type CertificateAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostServicesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostAnalyze struct {
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
//
@@ -100,4 +105,6 @@ type HostAnalyze struct {
FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"`
HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
}

View File

@@ -88,6 +88,10 @@ type Certificate struct {
KeyPath string `json:"keyPath" yaml:"keyPath"`
}
type HostServices struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -103,6 +107,7 @@ type HostCollect struct {
TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"`
HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
}
func (c *HostCollect) GetName() string {

View File

@@ -1278,6 +1278,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(CertificateAnalyze)
(*in).DeepCopyInto(*out)
}
if in.HostServices != nil {
in, out := &in.HostServices, &out.HostServices
*out = new(HostServicesAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -1379,6 +1384,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(Certificate)
**out = **in
}
if in.HostServices != nil {
in, out := &in.HostServices, &out.HostServices
*out = new(HostServices)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
@@ -1549,6 +1559,49 @@ func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostServices) DeepCopyInto(out *HostServices) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostServices.
func (in *HostServices) DeepCopy() *HostServices {
if in == nil {
return nil
}
out := new(HostServices)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostServicesAnalyze) DeepCopyInto(out *HostServicesAnalyze) {
*out = *in
out.AnalyzeMeta = in.AnalyzeMeta
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostServicesAnalyze.
func (in *HostServicesAnalyze) DeepCopy() *HostServicesAnalyze {
if in == nil {
return nil
}
out := new(HostServicesAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostTime) DeepCopyInto(out *HostTime) {
*out = *in

View File

@@ -38,6 +38,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect) (HostCollector
return &CollectHostFilesystemPerformance{collector.FilesystemPerformance}, true
case collector.Certificate != nil:
return &CollectHostCertificate{collector.Certificate}, true
case collector.HostServices != nil:
return &CollectHostServices{collector.HostServices}, true
default:
return nil, false
}

View File

@@ -0,0 +1,69 @@
package collect
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os/exec"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type ServiceInfo struct {
Unit string `json:"Unit"`
Load string `json:"Load"`
Active string `json:"Active"`
Sub string `json:"Sub"`
}
const systemctlFormat = `%s %s %s %s` // this leaves off the description
const HostServicesPath = `system/systemctl_services.json`
type CollectHostServices struct {
hostCollector *troubleshootv1beta2.HostServices
}
func (c *CollectHostServices) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Block Devices")
}
func (c *CollectHostServices) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostServices) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
var devices []ServiceInfo
cmd := exec.Command("systemctl", "list-units", "--type=service", "--no-legend", "--all")
stdout, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(err, "failed to execute systemctl")
}
buf := bytes.NewBuffer(stdout)
scanner := bufio.NewScanner(buf)
for scanner.Scan() {
bdi := ServiceInfo{}
fmt.Sscanf(
scanner.Text(),
systemctlFormat,
&bdi.Unit,
&bdi.Load,
&bdi.Active,
&bdi.Sub,
)
devices = append(devices, bdi)
}
b, err := json.Marshal(devices)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal systemctl service info")
}
return map[string][]byte{
HostServicesPath: b,
}, nil
}