mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
Merge pull request #341 from replicatedhq/laverya/systemctl-services-collector
systemctl services collector
This commit is contained in:
@@ -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),
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
150
pkg/analyze/host_services.go
Normal file
150
pkg/analyze/host_services.go
Normal 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
|
||||
}
|
||||
171
pkg/analyze/host_services_test.go
Normal file
171
pkg/analyze/host_services_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
69
pkg/collect/host_services.go
Normal file
69
pkg/collect/host_services.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user