From d463caf7ee31f238c02703374f88a2895dd4df4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Torrero=20Marijnissen?= Date: Tue, 26 Sep 2023 09:07:05 +0100 Subject: [PATCH 1/5] Add saptune gatherer --- internal/factsengine/gatherers/gatherer.go | 1 + internal/factsengine/gatherers/saptune.go | 158 +++++ .../factsengine/gatherers/saptune_test.go | 630 ++++++++++++++++++ .../gatherers/saptune-note-list.output | 2 + .../gatherers/saptune-note-verify.output | 2 + .../gatherers/saptune-solution-list.output | 2 + .../gatherers/saptune-solution-verify.output | 2 + test/fixtures/gatherers/saptune-status.output | 2 + 8 files changed, 799 insertions(+) create mode 100644 internal/factsengine/gatherers/saptune.go create mode 100644 internal/factsengine/gatherers/saptune_test.go create mode 100644 test/fixtures/gatherers/saptune-note-list.output create mode 100644 test/fixtures/gatherers/saptune-note-verify.output create mode 100644 test/fixtures/gatherers/saptune-solution-list.output create mode 100644 test/fixtures/gatherers/saptune-solution-verify.output create mode 100644 test/fixtures/gatherers/saptune-status.output diff --git a/internal/factsengine/gatherers/gatherer.go b/internal/factsengine/gatherers/gatherer.go index 6fcb9e43..43ff501f 100644 --- a/internal/factsengine/gatherers/gatherer.go +++ b/internal/factsengine/gatherers/gatherer.go @@ -20,5 +20,6 @@ func StandardGatherers() map[string]FactGatherer { SBDDumpGathererName: NewDefaultSBDDumpGatherer(), SapHostCtrlGathererName: NewDefaultSapHostCtrlGatherer(), VerifyPasswordGathererName: NewDefaultPasswordGatherer(), + SaptuneGathererName: NewDefaultSaptuneGatherer(), } } diff --git a/internal/factsengine/gatherers/saptune.go b/internal/factsengine/gatherers/saptune.go new file mode 100644 index 00000000..9c58f319 --- /dev/null +++ b/internal/factsengine/gatherers/saptune.go @@ -0,0 +1,158 @@ +package gatherers + +import ( + "encoding/json" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/trento-project/agent/internal/core/saptune" + "github.com/trento-project/agent/pkg/factsengine/entities" + "github.com/trento-project/agent/pkg/utils" +) + +const ( + SaptuneGathererName = "saptune" +) + +// nolint:gochecknoglobals +var whitelistedArguments = map[string][]string{ + "status": {"status", "--non-compliance-check"}, + "solution-verify": {"solution", "verify"}, + "solution-list": {"solution", "list"}, + "note-verify": {"note", "verify"}, + "note-list": {"note", "list"}, +} + +// nolint:gochecknoglobals +var ( + SaptuneVersionUnsupported = entities.FactGatheringError{ + Type: "saptune-version-not-supported", + Message: "currently installed version of saptune is not supported", + } + + SaptuneUnknownArgument = entities.FactGatheringError{ + Type: "saptune-unknown-error", + Message: "the requested argument is not currently supported", + } + + SaptuneMissingArgument = entities.FactGatheringError{ + Type: "saptune-missing-argument", + Message: "missing required argument", + } + + SaptuneCommandError = entities.FactGatheringError{ + Type: "saptune-cmd-error", + Message: "error executing saptune command", + } +) + +type CachedFactValue struct { + factValue entities.FactValue + factValueErr *entities.FactGatheringError +} + +type SaptuneGatherer struct { + executor utils.CommandExecutor + cachedFactValues map[string]CachedFactValue +} + +func NewDefaultSaptuneGatherer() *SaptuneGatherer { + return NewSaptuneGatherer(utils.Executor{}) +} + +func NewSaptuneGatherer(executor utils.CommandExecutor) *SaptuneGatherer { + return &SaptuneGatherer{ + executor: executor, + } +} + +func parseJSONToFactValue(jsonStr json.RawMessage) (entities.FactValue, error) { + // Unmarshal the JSON into an interface{} type. + var jsonData interface{} + if err := json.Unmarshal([]byte(jsonStr), &jsonData); err != nil { + return nil, err + } + + // Convert the parsed jsonData into a FactValue using NewFactValue. + return entities.NewFactValue(jsonData) +} + +func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) { + s.cachedFactValues = make(map[string]CachedFactValue) + + facts := []entities.Fact{} + log.Infof("Starting %s facts gathering process", SaptuneGathererName) + saptuneRetriever, _ := saptune.NewSaptune(s.executor) + for _, factReq := range factsRequests { + var fact entities.Fact + + internalArguments, ok := whitelistedArguments[factReq.Argument] + + switch { + case !saptuneRetriever.IsJSONSupported: + log.Error(SaptuneVersionUnsupported.Message) + fact = entities.NewFactGatheredWithError(factReq, &SaptuneVersionUnsupported) + + case len(internalArguments) > 0 && !ok: + gatheringError := SaptuneUnknownArgument.Wrap(factReq.Argument) + log.Error(gatheringError) + fact = entities.NewFactGatheredWithError(factReq, gatheringError) + + case len(internalArguments) == 0: + log.Error(SaptuneMissingArgument.Message) + fact = entities.NewFactGatheredWithError(factReq, &SaptuneMissingArgument) + + default: + factValue, err := handleArgument(&saptuneRetriever, internalArguments, s.cachedFactValues) + if err != nil { + fact = entities.NewFactGatheredWithError(factReq, err) + } else { + fact = entities.NewFactGatheredWithRequest(factReq, factValue) + } + } + facts = append(facts, fact) + } + + log.Infof("Requested %s facts gathered", SaptuneGathererName) + return facts, nil +} + +func handleArgument( + saptuneRetriever *saptune.Saptune, + arguments []string, + cachedFactValues map[string]CachedFactValue, +) (entities.FactValue, *entities.FactGatheringError) { + cacheKey := strings.Join(arguments, "-") + if item, found := cachedFactValues[cacheKey]; found { + log.Info("Using cached fact value") + return item.factValue, item.factValueErr + } + + saptuneOutput, commandError := saptuneRetriever.RunCommandJSON(arguments...) + if commandError != nil { + gatheringError := SaptuneCommandError.Wrap(commandError.Error()) + log.Error(gatheringError) + updateCachedFactValue(nil, gatheringError, cacheKey, cachedFactValues) + return nil, gatheringError + } + + fv, err := parseJSONToFactValue(saptuneOutput) + if err != nil { + gatheringError := SaptuneCommandError.Wrap(err.Error()) + log.Error(gatheringError) + updateCachedFactValue(nil, gatheringError, cacheKey, cachedFactValues) + return nil, gatheringError + } + + updateCachedFactValue(fv, nil, cacheKey, cachedFactValues) + return fv, nil +} + +func updateCachedFactValue(factValue entities.FactValue, factValueErr *entities.FactGatheringError, key string, + cachedFactValues map[string]CachedFactValue) { + log.Info("Updating cached fact value") + cachedFactValues[key] = CachedFactValue{ + factValue: factValue, + factValueErr: factValueErr, + } +} diff --git a/internal/factsengine/gatherers/saptune_test.go b/internal/factsengine/gatherers/saptune_test.go new file mode 100644 index 00000000..bad54c5c --- /dev/null +++ b/internal/factsengine/gatherers/saptune_test.go @@ -0,0 +1,630 @@ +//nolint:dupl +package gatherers_test + +import ( + "io" + "os" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/trento-project/agent/internal/factsengine/gatherers" + "github.com/trento-project/agent/pkg/factsengine/entities" + utilsMocks "github.com/trento-project/agent/pkg/utils/mocks" + "github.com/trento-project/agent/test/helpers" +) + +type SaptuneTestSuite struct { + suite.Suite + mockExecutor *utilsMocks.CommandExecutor +} + +func TestSaptuneTestSuite(t *testing.T) { + suite.Run(t, new(SaptuneTestSuite)) +} + +func (suite *SaptuneTestSuite) SetupTest() { + suite.mockExecutor = new(utilsMocks.CommandExecutor) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererStatus() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/saptune-status.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "status", "--non-compliance-check").Return(mockOutput, nil) + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_status", + Gatherer: "saptune", + Argument: "status", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_status", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_status.schema.json"}, + "publish time": &entities.FactValueString{Value: "2023-09-15 15:15:14.599"}, + "argv": &entities.FactValueString{Value: "saptune --format json status"}, + "pid": &entities.FactValueInt{Value: 6593}, + "command": &entities.FactValueString{Value: "status"}, + "exit code": &entities.FactValueInt{Value: 1}, + "result": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "services": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "saptune": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueString{Value: "disabled"}, + &entities.FactValueString{Value: "inactive"}, + }, + }, + "sapconf": &entities.FactValueList{Value: []entities.FactValue{}}, + "tuned": &entities.FactValueList{Value: []entities.FactValue{}}, + }, + }, + "systemd system state": &entities.FactValueString{Value: "degraded"}, + "tuning state": &entities.FactValueString{Value: "compliant"}, + "virtualization": &entities.FactValueString{Value: "kvm"}, + "configured version": &entities.FactValueInt{Value: 3}, + "package version": &entities.FactValueString{Value: "3.1.0"}, + "Solution enabled": &entities.FactValueList{Value: []entities.FactValue{}}, + "Notes enabled by Solution": &entities.FactValueList{Value: []entities.FactValue{}}, + "Solution applied": &entities.FactValueList{Value: []entities.FactValue{}}, + "Notes applied by Solution": &entities.FactValueList{Value: []entities.FactValue{}}, + "Notes enabled additionally": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1410736}, + }, + }, + "Notes enabled": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1410736}, + }, + }, + "Notes applied": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1410736}, + }, + }, + "staging": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "staging enabled": &entities.FactValueBool{Value: false}, + "Notes staged": &entities.FactValueList{Value: []entities.FactValue{}}, + "Solutions staged": &entities.FactValueList{Value: []entities.FactValue{}}, + }, + }, + "remember message": &entities.FactValueString{Value: "This is a reminder"}, + }, + }, + "messages": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "actions.go:85: ATTENTION: You are running a test version"}, + }, + }, + }, + }, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererNoteVerify() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/saptune-note-verify.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "note", "verify").Return(mockOutput, nil) + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_note_verify", + Gatherer: "saptune", + Argument: "note-verify", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_note_verify", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "$schema": &entities.FactValueString{ + Value: "file:///usr/share/saptune/schemas/1.0/saptune_note_verify.schema.json", + }, + "publish time": &entities.FactValueString{ + Value: "2023-04-24 15:49:43.399", + }, + "argv": &entities.FactValueString{ + Value: "saptune --format json note verify", + }, + "pid": &entities.FactValueInt{ + Value: 25202, + }, + "command": &entities.FactValueString{ + Value: "note verify", + }, + "exit code": &entities.FactValueInt{ + Value: 1, + }, + "result": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "verifications": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Note ID": &entities.FactValueInt{Value: 1771258}, + "Note version": &entities.FactValueInt{Value: 6}, + "parameter": &entities.FactValueString{Value: "LIMIT_@dba_hard_nofile"}, + "compliant": &entities.FactValueBool{Value: true}, + "expected value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + "actual value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Note ID": &entities.FactValueInt{Value: 1771258}, + "Note version": &entities.FactValueInt{Value: 6}, + "parameter": &entities.FactValueString{Value: "LIMIT_@dba_soft_nofile"}, + "compliant": &entities.FactValueBool{Value: true}, + "expected value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + "actual value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + }, + }, + }, + }, + "attentions": &entities.FactValueList{ + Value: []entities.FactValue{}, + }, + "Notes enabled": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1771258}, + }, + }, + "system compliance": &entities.FactValueBool{Value: false}, + }, + }, + "messages": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "actions.go:85 You are running a test version"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "WARNING"}, + "message": &entities.FactValueString{Value: "sysctl.go:73: Parameter 'kernel.shmmax' redefined "}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "WARNING"}, + "message": &entities.FactValueString{Value: "sysctl.go:73: Parameter 'kernel.shmall' redefined"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "ini.go:308: block device related section settings detected"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "ERROR"}, + "message": &entities.FactValueString{Value: "system.go:148: The parameters have deviated from recommendations"}, + }, + }, + }, + }, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererSolutionVerify() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/saptune-solution-verify.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "solution", "verify").Return(mockOutput, nil) + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_solution_verify", + Gatherer: "saptune", + Argument: "solution-verify", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_solution_verify", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_solution_verify.schema.json"}, + "publish time": &entities.FactValueString{Value: "2023-04-27 17:17:23.743"}, + "argv": &entities.FactValueString{Value: "saptune --format json solution verify"}, + "pid": &entities.FactValueInt{Value: 2538}, + "command": &entities.FactValueString{Value: "solution verify"}, + "exit code": &entities.FactValueInt{Value: 1}, + "result": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "verifications": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Note ID": &entities.FactValueInt{Value: 1771258}, + "Note version": &entities.FactValueInt{Value: 6}, + "parameter": &entities.FactValueString{Value: "LIMIT_@dba_hard_nofile"}, + "compliant": &entities.FactValueBool{Value: true}, + "expected value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + "actual value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Note ID": &entities.FactValueInt{Value: 1771258}, + "Note version": &entities.FactValueInt{Value: 6}, + "parameter": &entities.FactValueString{Value: "LIMIT_@dba_soft_nofile"}, + "compliant": &entities.FactValueBool{Value: true}, + "expected value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + "actual value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + }, + }, + }, + }, + "attentions": &entities.FactValueList{ + Value: []entities.FactValue{}, + }, + "Notes enabled": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1771258}, + }, + }, + "system compliance": &entities.FactValueBool{Value: false}, + }, + }, + "messages": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "actions.go:85 You are running a test version"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "WARNING"}, + "message": &entities.FactValueString{Value: "sysctl.go:73: Parameter 'kernel.shmmax' redefined "}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "WARNING"}, + "message": &entities.FactValueString{Value: "sysctl.go:73: Parameter 'kernel.shmall' redefined"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "ini.go:308: block device related section settings detected"}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "ERROR"}, + "message": &entities.FactValueString{Value: "system.go:148: The parameters have deviated from recommendations"}, + }, + }, + }, + }, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererSolutionList() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/saptune-solution-list.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "solution", "list").Return(mockOutput, nil) + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_solution_list", + Gatherer: "saptune", + Argument: "solution-list", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_solution_list", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_solution_list.schema.json"}, + "publish time": &entities.FactValueString{Value: "2023-04-27 17:21:27.926"}, + "argv": &entities.FactValueString{Value: "saptune --format json solution list"}, + "pid": &entities.FactValueInt{Value: 2582}, + "command": &entities.FactValueString{Value: "solution list"}, + "exit code": &entities.FactValueInt{Value: 0}, + "result": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Solutions available": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Solution ID": &entities.FactValueString{Value: "BOBJ"}, + "Note list": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1771258}, + }, + }, + "Solution enabled": &entities.FactValueBool{Value: false}, + "Solution override exists": &entities.FactValueBool{Value: false}, + "custom Solution": &entities.FactValueBool{Value: false}, + "Solution deprecated": &entities.FactValueBool{Value: false}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Solution ID": &entities.FactValueString{Value: "DEMO"}, + "Note list": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueString{Value: "demo"}, + }, + }, + "Solution enabled": &entities.FactValueBool{Value: false}, + "Solution override exists": &entities.FactValueBool{Value: false}, + "custom Solution": &entities.FactValueBool{Value: true}, + "Solution deprecated": &entities.FactValueBool{Value: false}, + }, + }, + }, + }, + "remember message": &entities.FactValueString{Value: ""}, + }, + }, + "messages": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "actions.go:85 You are running a test version"}, + }, + }, + }, + }, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererNoteList() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/saptune-note-list.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "note", "list").Return(mockOutput, nil) + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_note_list", + Gatherer: "saptune", + Argument: "note-list", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_note_list", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_note_list.schema.json"}, + "publish time": &entities.FactValueString{Value: "2023-04-27 17:28:53.073"}, + "argv": &entities.FactValueString{Value: "saptune --format json note list"}, + "pid": &entities.FactValueInt{Value: 2604}, + "command": &entities.FactValueString{Value: "note list"}, + "exit code": &entities.FactValueInt{Value: 0}, + "result": &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Notes available": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Note ID": &entities.FactValueInt{Value: 1410736}, + "Note description": &entities.FactValueString{Value: "TCP/IP: setting keepalive interval"}, + "Note reference": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueString{Value: "https://launchpad.support.sap.com/#/notes/1410736"}, + }, + }, + "Note version": &entities.FactValueInt{Value: 6}, + "Note release date": &entities.FactValueString{Value: "13.01.2020"}, + "Note enabled manually": &entities.FactValueBool{Value: false}, + "Note enabled by Solution": &entities.FactValueBool{Value: false}, + "Note reverted manually": &entities.FactValueBool{Value: false}, + "Note override exists": &entities.FactValueBool{Value: false}, + "custom Note": &entities.FactValueBool{Value: false}, + }, + }, + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "Note ID": &entities.FactValueInt{Value: 1656250}, + "Note description": &entities.FactValueString{Value: "SAP on AWS: prerequisites - only Linux"}, + "Note reference": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueString{Value: "https://launchpad.support.sap.com/#/notes/1656250"}, + }, + }, + "Note version": &entities.FactValueInt{Value: 46}, + "Note release date": &entities.FactValueString{Value: "11.05.2022"}, + "Note enabled manually": &entities.FactValueBool{Value: false}, + "Note enabled by Solution": &entities.FactValueBool{Value: true}, + "Note reverted manually": &entities.FactValueBool{Value: false}, + "Note override exists": &entities.FactValueBool{Value: false}, + "custom Note": &entities.FactValueBool{Value: false}, + }, + }, + }, + }, + "Notes enabled": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueInt{Value: 1656250}, + }, + }, + "remember message": &entities.FactValueString{Value: ""}, + }, + }, + "messages": &entities.FactValueList{ + Value: []entities.FactValue{ + &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "priority": &entities.FactValueString{Value: "NOTICE"}, + "message": &entities.FactValueString{Value: "actions.go:85 You are running a test version"}, + }, + }, + }, + }, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererNoArgumentProvided() { + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "no_argument_fact", + Gatherer: "saptune", + }, + { + Name: "empty_argument_fact", + Gatherer: "saptune", + Argument: "", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "no_argument_fact", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "missing required argument", + Type: "saptune-missing-argument", + }, + }, + { + Name: "empty_argument_fact", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "missing required argument", + Type: "saptune-missing-argument", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererCommandCaching() { + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "status", "--non-compliance-check").Return([]byte("{\"some_json_key\": \"some_value\"}"), nil) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_repeated_argument_1", + Gatherer: "saptune", + Argument: "status", + }, + { + Name: "saptune_repeated_argument_2", + Gatherer: "saptune", + Argument: "status", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_repeated_argument_1", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "some_json_key": &entities.FactValueString{Value: "some_value"}, + }, + }, + }, + { + Name: "saptune_repeated_argument_2", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "some_json_key": &entities.FactValueString{Value: "some_value"}, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) + suite.mockExecutor.AssertNumberOfCalls(suite.T(), "Exec", 2) // 1 for rpm, 1 for saptune +} diff --git a/test/fixtures/gatherers/saptune-note-list.output b/test/fixtures/gatherers/saptune-note-list.output new file mode 100644 index 00000000..b5e2f956 --- /dev/null +++ b/test/fixtures/gatherers/saptune-note-list.output @@ -0,0 +1,2 @@ + +{"$schema":"file:///usr/share/saptune/schemas/1.0/saptune_note_list.schema.json","publish time":"2023-04-27 17:28:53.073","argv":"saptune --format json note list","pid":2604,"command":"note list","exit code":0,"result":{"Notes available":[{"Note ID":"1410736","Note description":"TCP/IP: setting keepalive interval","Note reference":["https://launchpad.support.sap.com/#/notes/1410736"],"Note version":"6","Note release date":"13.01.2020","Note enabled manually":false,"Note enabled by Solution":false,"Note reverted manually":false,"Note override exists":false,"custom Note":false},{"Note ID":"1656250","Note description":"SAP on AWS: prerequisites - only Linux","Note reference":["https://launchpad.support.sap.com/#/notes/1656250"],"Note version":"46","Note release date":"11.05.2022","Note enabled manually":false,"Note enabled by Solution":true,"Note reverted manually":false,"Note override exists":false,"custom Note":false}],"Notes enabled":["1656250"],"remember message":""},"messages":[{"priority":"NOTICE","message":"actions.go:85 You are running a test version"}]} \ No newline at end of file diff --git a/test/fixtures/gatherers/saptune-note-verify.output b/test/fixtures/gatherers/saptune-note-verify.output new file mode 100644 index 00000000..ebe5be87 --- /dev/null +++ b/test/fixtures/gatherers/saptune-note-verify.output @@ -0,0 +1,2 @@ + +{"$schema":"file:///usr/share/saptune/schemas/1.0/saptune_note_verify.schema.json","publish time":"2023-04-24 15:49:43.399","argv":"saptune --format json note verify","pid":25202,"command":"note verify","exit code":1,"result":{"verifications":[{"Note ID":"1771258","Note version":"6","parameter":"LIMIT_@dba_hard_nofile","compliant":true,"expected value":"@dba hard nofile 1048576","actual value":"@dba hard nofile 1048576"},{"Note ID":"1771258","Note version":"6","parameter":"LIMIT_@dba_soft_nofile","compliant":true,"expected value":"@dba soft nofile 1048576","actual value":"@dba soft nofile 1048576"}],"attentions":[],"Notes enabled":["1771258"],"system compliance":false},"messages":[{"priority":"NOTICE","message":"actions.go:85 You are running a test version"},{"priority":"WARNING","message":"sysctl.go:73: Parameter 'kernel.shmmax' redefined "},{"priority":"WARNING","message":"sysctl.go:73: Parameter 'kernel.shmall' redefined"},{"priority":"NOTICE","message":"ini.go:308: block device related section settings detected"},{"priority":"ERROR","message":"system.go:148: The parameters have deviated from recommendations"}]} \ No newline at end of file diff --git a/test/fixtures/gatherers/saptune-solution-list.output b/test/fixtures/gatherers/saptune-solution-list.output new file mode 100644 index 00000000..5b8975ac --- /dev/null +++ b/test/fixtures/gatherers/saptune-solution-list.output @@ -0,0 +1,2 @@ + +{"$schema":"file:///usr/share/saptune/schemas/1.0/saptune_solution_list.schema.json","publish time":"2023-04-27 17:21:27.926","argv":"saptune --format json solution list","pid":2582,"command":"solution list","exit code":0,"result":{"Solutions available":[{"Solution ID":"BOBJ","Note list":["1771258"],"Solution enabled":false,"Solution override exists":false,"custom Solution":false,"Solution deprecated":false},{"Solution ID":"DEMO","Note list":["demo"],"Solution enabled":false,"Solution override exists":false,"custom Solution":true,"Solution deprecated":false}],"remember message":""},"messages":[{"priority":"NOTICE","message":"actions.go:85 You are running a test version"}]} \ No newline at end of file diff --git a/test/fixtures/gatherers/saptune-solution-verify.output b/test/fixtures/gatherers/saptune-solution-verify.output new file mode 100644 index 00000000..83ac8063 --- /dev/null +++ b/test/fixtures/gatherers/saptune-solution-verify.output @@ -0,0 +1,2 @@ + +{"$schema":"file:///usr/share/saptune/schemas/1.0/saptune_solution_verify.schema.json","publish time":"2023-04-27 17:17:23.743","argv":"saptune --format json solution verify","pid":2538,"command":"solution verify","exit code":1,"result":{"verifications":[{"Note ID":"1771258","Note version":"6","parameter":"LIMIT_@dba_hard_nofile","compliant":true,"expected value":"@dba hard nofile 1048576","actual value":"@dba hard nofile 1048576"},{"Note ID":"1771258","Note version":"6","parameter":"LIMIT_@dba_soft_nofile","compliant":true,"expected value":"@dba soft nofile 1048576","actual value":"@dba soft nofile 1048576"}],"attentions":[],"Notes enabled":["1771258"],"system compliance":false},"messages":[{"priority":"NOTICE","message":"actions.go:85 You are running a test version"},{"priority":"WARNING","message":"sysctl.go:73: Parameter 'kernel.shmmax' redefined "},{"priority":"WARNING","message":"sysctl.go:73: Parameter 'kernel.shmall' redefined"},{"priority":"NOTICE","message":"ini.go:308: block device related section settings detected"},{"priority":"ERROR","message":"system.go:148: The parameters have deviated from recommendations"}]} \ No newline at end of file diff --git a/test/fixtures/gatherers/saptune-status.output b/test/fixtures/gatherers/saptune-status.output new file mode 100644 index 00000000..a0388080 --- /dev/null +++ b/test/fixtures/gatherers/saptune-status.output @@ -0,0 +1,2 @@ + +{"$schema":"file:///usr/share/saptune/schemas/1.0/saptune_status.schema.json","publish time":"2023-09-15 15:15:14.599","argv":"saptune --format json status","pid":6593,"command":"status","exit code":1,"result":{"services":{"saptune":["disabled","inactive"],"sapconf":[],"tuned":[]},"systemd system state":"degraded","tuning state":"compliant","virtualization":"kvm","configured version":"3","package version":"3.1.0","Solution enabled":[],"Notes enabled by Solution":[],"Solution applied":[],"Notes applied by Solution":[],"Notes enabled additionally":["1410736"],"Notes enabled":["1410736"],"Notes applied":["1410736"],"staging":{"staging enabled":false,"Notes staged":[],"Solutions staged":[]},"remember message":"This is a reminder"},"messages":[{"priority":"NOTICE","message":"actions.go:85: ATTENTION: You are running a test version"}]} \ No newline at end of file From 9d0c1764dad8ba62f35745a7c0ec0edcee42df5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Torrero=20Marijnissen?= Date: Tue, 26 Sep 2023 16:34:54 +0100 Subject: [PATCH 2/5] Apply @arbulu89 suggestions and adjust tests --- internal/factsengine/gatherers/saptune.go | 109 +++++------ .../factsengine/gatherers/saptune_test.go | 178 +++++++++--------- 2 files changed, 134 insertions(+), 153 deletions(-) diff --git a/internal/factsengine/gatherers/saptune.go b/internal/factsengine/gatherers/saptune.go index 9c58f319..7ebe318b 100644 --- a/internal/factsengine/gatherers/saptune.go +++ b/internal/factsengine/gatherers/saptune.go @@ -2,7 +2,6 @@ package gatherers import ( "encoding/json" - "strings" log "github.com/sirupsen/logrus" "github.com/trento-project/agent/internal/core/saptune" @@ -25,13 +24,18 @@ var whitelistedArguments = map[string][]string{ // nolint:gochecknoglobals var ( + SaptuneNotInstalled = entities.FactGatheringError{ + Type: "saptune-not-installed", + Message: "saptune is not installed", + } + SaptuneVersionUnsupported = entities.FactGatheringError{ Type: "saptune-version-not-supported", Message: "currently installed version of saptune is not supported", } - SaptuneUnknownArgument = entities.FactGatheringError{ - Type: "saptune-unknown-error", + SaptuneArgumentUnsupported = entities.FactGatheringError{ + Type: "saptune-argument-not-error", Message: "the requested argument is not currently supported", } @@ -46,14 +50,8 @@ var ( } ) -type CachedFactValue struct { - factValue entities.FactValue - factValueErr *entities.FactGatheringError -} - type SaptuneGatherer struct { - executor utils.CommandExecutor - cachedFactValues map[string]CachedFactValue + executor utils.CommandExecutor } func NewDefaultSaptuneGatherer() *SaptuneGatherer { @@ -66,49 +64,54 @@ func NewSaptuneGatherer(executor utils.CommandExecutor) *SaptuneGatherer { } } -func parseJSONToFactValue(jsonStr json.RawMessage) (entities.FactValue, error) { - // Unmarshal the JSON into an interface{} type. - var jsonData interface{} - if err := json.Unmarshal([]byte(jsonStr), &jsonData); err != nil { - return nil, err - } - - // Convert the parsed jsonData into a FactValue using NewFactValue. - return entities.NewFactValue(jsonData) -} - func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) { - s.cachedFactValues = make(map[string]CachedFactValue) + cachedFacts := make(map[string]entities.Fact) facts := []entities.Fact{} log.Infof("Starting %s facts gathering process", SaptuneGathererName) - saptuneRetriever, _ := saptune.NewSaptune(s.executor) + saptuneRetriever, err := saptune.NewSaptune(s.executor) + if err != nil { + return facts, &SaptuneNotInstalled + } + + if !saptuneRetriever.IsJSONSupported { + return facts, &SaptuneVersionUnsupported + } + for _, factReq := range factsRequests { var fact entities.Fact internalArguments, ok := whitelistedArguments[factReq.Argument] + cachedFact, cacheHit := cachedFacts[factReq.Argument] switch { - case !saptuneRetriever.IsJSONSupported: - log.Error(SaptuneVersionUnsupported.Message) - fact = entities.NewFactGatheredWithError(factReq, &SaptuneVersionUnsupported) + case len(factReq.Argument) == 0: + log.Error(SaptuneMissingArgument.Message) + fact = entities.NewFactGatheredWithError(factReq, &SaptuneMissingArgument) - case len(internalArguments) > 0 && !ok: - gatheringError := SaptuneUnknownArgument.Wrap(factReq.Argument) + case !ok: + gatheringError := SaptuneArgumentUnsupported.Wrap(factReq.Argument) log.Error(gatheringError) fact = entities.NewFactGatheredWithError(factReq, gatheringError) - case len(internalArguments) == 0: - log.Error(SaptuneMissingArgument.Message) - fact = entities.NewFactGatheredWithError(factReq, &SaptuneMissingArgument) + case cacheHit: + fact = entities.Fact{ + Name: factReq.Name, + CheckID: factReq.CheckID, + Value: cachedFact.Value, + Error: cachedFact.Error, + } default: - factValue, err := handleArgument(&saptuneRetriever, internalArguments, s.cachedFactValues) + factValue, err := runCommand(&saptuneRetriever, internalArguments) if err != nil { - fact = entities.NewFactGatheredWithError(factReq, err) + gatheringError := SaptuneCommandError.Wrap(err.Error()) + log.Error(gatheringError) + fact = entities.NewFactGatheredWithError(factReq, gatheringError) } else { fact = entities.NewFactGatheredWithRequest(factReq, factValue) } + cachedFacts[factReq.Argument] = fact } facts = append(facts, fact) } @@ -117,42 +120,20 @@ func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entiti return facts, nil } -func handleArgument( - saptuneRetriever *saptune.Saptune, - arguments []string, - cachedFactValues map[string]CachedFactValue, -) (entities.FactValue, *entities.FactGatheringError) { - cacheKey := strings.Join(arguments, "-") - if item, found := cachedFactValues[cacheKey]; found { - log.Info("Using cached fact value") - return item.factValue, item.factValueErr - } - +func runCommand(saptuneRetriever *saptune.Saptune, arguments []string) (entities.FactValue, error) { saptuneOutput, commandError := saptuneRetriever.RunCommandJSON(arguments...) if commandError != nil { - gatheringError := SaptuneCommandError.Wrap(commandError.Error()) - log.Error(gatheringError) - updateCachedFactValue(nil, gatheringError, cacheKey, cachedFactValues) - return nil, gatheringError + return nil, commandError } - fv, err := parseJSONToFactValue(saptuneOutput) - if err != nil { - gatheringError := SaptuneCommandError.Wrap(err.Error()) - log.Error(gatheringError) - updateCachedFactValue(nil, gatheringError, cacheKey, cachedFactValues) - return nil, gatheringError + log.Error(string(saptuneOutput)) + + var jsonData interface{} + if err := json.Unmarshal(saptuneOutput, &jsonData); err != nil { + return nil, err } - updateCachedFactValue(fv, nil, cacheKey, cachedFactValues) - return fv, nil -} + log.Error(jsonData) -func updateCachedFactValue(factValue entities.FactValue, factValueErr *entities.FactGatheringError, key string, - cachedFactValues map[string]CachedFactValue) { - log.Info("Updating cached fact value") - cachedFactValues[key] = CachedFactValue{ - factValue: factValue, - factValueErr: factValueErr, - } + return entities.NewFactValue(jsonData, entities.WithSnakeCaseKeys()) } diff --git a/internal/factsengine/gatherers/saptune_test.go b/internal/factsengine/gatherers/saptune_test.go index bad54c5c..06553aa2 100644 --- a/internal/factsengine/gatherers/saptune_test.go +++ b/internal/factsengine/gatherers/saptune_test.go @@ -51,11 +51,11 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererStatus() { Value: &entities.FactValueMap{ Value: map[string]entities.FactValue{ "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_status.schema.json"}, - "publish time": &entities.FactValueString{Value: "2023-09-15 15:15:14.599"}, + "publish_time": &entities.FactValueString{Value: "2023-09-15 15:15:14.599"}, "argv": &entities.FactValueString{Value: "saptune --format json status"}, "pid": &entities.FactValueInt{Value: 6593}, "command": &entities.FactValueString{Value: "status"}, - "exit code": &entities.FactValueInt{Value: 1}, + "exit_code": &entities.FactValueInt{Value: 1}, "result": &entities.FactValueMap{ Value: map[string]entities.FactValue{ "services": &entities.FactValueMap{ @@ -70,38 +70,38 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererStatus() { "tuned": &entities.FactValueList{Value: []entities.FactValue{}}, }, }, - "systemd system state": &entities.FactValueString{Value: "degraded"}, - "tuning state": &entities.FactValueString{Value: "compliant"}, + "systemd_system_state": &entities.FactValueString{Value: "degraded"}, + "tuning_state": &entities.FactValueString{Value: "compliant"}, "virtualization": &entities.FactValueString{Value: "kvm"}, - "configured version": &entities.FactValueInt{Value: 3}, - "package version": &entities.FactValueString{Value: "3.1.0"}, - "Solution enabled": &entities.FactValueList{Value: []entities.FactValue{}}, - "Notes enabled by Solution": &entities.FactValueList{Value: []entities.FactValue{}}, - "Solution applied": &entities.FactValueList{Value: []entities.FactValue{}}, - "Notes applied by Solution": &entities.FactValueList{Value: []entities.FactValue{}}, - "Notes enabled additionally": &entities.FactValueList{ + "configured_version": &entities.FactValueString{Value: "3"}, + "package_version": &entities.FactValueString{Value: "3.1.0"}, + "solution_enabled": &entities.FactValueList{Value: []entities.FactValue{}}, + "notes_enabled_by_solution": &entities.FactValueList{Value: []entities.FactValue{}}, + "solution_applied": &entities.FactValueList{Value: []entities.FactValue{}}, + "notes_applied_by_solution": &entities.FactValueList{Value: []entities.FactValue{}}, + "notes_enabled_additionally": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1410736}, + &entities.FactValueString{Value: "1410736"}, }, }, - "Notes enabled": &entities.FactValueList{ + "notes_enabled": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1410736}, + &entities.FactValueString{Value: "1410736"}, }, }, - "Notes applied": &entities.FactValueList{ + "notes_applied": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1410736}, + &entities.FactValueString{Value: "1410736"}, }, }, "staging": &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "staging enabled": &entities.FactValueBool{Value: false}, - "Notes staged": &entities.FactValueList{Value: []entities.FactValue{}}, - "Solutions staged": &entities.FactValueList{Value: []entities.FactValue{}}, + "staging_enabled": &entities.FactValueBool{Value: false}, + "notes_staged": &entities.FactValueList{Value: []entities.FactValue{}}, + "solutions_staged": &entities.FactValueList{Value: []entities.FactValue{}}, }, }, - "remember message": &entities.FactValueString{Value: "This is a reminder"}, + "remember_message": &entities.FactValueString{Value: "This is a reminder"}, }, }, "messages": &entities.FactValueList{ @@ -150,7 +150,7 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNoteVerify() { "$schema": &entities.FactValueString{ Value: "file:///usr/share/saptune/schemas/1.0/saptune_note_verify.schema.json", }, - "publish time": &entities.FactValueString{ + "publish_time": &entities.FactValueString{ Value: "2023-04-24 15:49:43.399", }, "argv": &entities.FactValueString{ @@ -162,7 +162,7 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNoteVerify() { "command": &entities.FactValueString{ Value: "note verify", }, - "exit code": &entities.FactValueInt{ + "exit_code": &entities.FactValueInt{ Value: 1, }, "result": &entities.FactValueMap{ @@ -171,22 +171,22 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNoteVerify() { Value: []entities.FactValue{ &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Note ID": &entities.FactValueInt{Value: 1771258}, - "Note version": &entities.FactValueInt{Value: 6}, + "note_id": &entities.FactValueString{Value: "1771258"}, + "note_version": &entities.FactValueString{Value: "6"}, "parameter": &entities.FactValueString{Value: "LIMIT_@dba_hard_nofile"}, "compliant": &entities.FactValueBool{Value: true}, - "expected value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, - "actual value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + "expected_value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + "actual_value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, }, }, &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Note ID": &entities.FactValueInt{Value: 1771258}, - "Note version": &entities.FactValueInt{Value: 6}, + "note_id": &entities.FactValueString{Value: "1771258"}, + "note_version": &entities.FactValueString{Value: "6"}, "parameter": &entities.FactValueString{Value: "LIMIT_@dba_soft_nofile"}, "compliant": &entities.FactValueBool{Value: true}, - "expected value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, - "actual value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + "expected_value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + "actual_value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, }, }, }, @@ -194,12 +194,12 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNoteVerify() { "attentions": &entities.FactValueList{ Value: []entities.FactValue{}, }, - "Notes enabled": &entities.FactValueList{ + "notes_enabled": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1771258}, + &entities.FactValueString{Value: "1771258"}, }, }, - "system compliance": &entities.FactValueBool{Value: false}, + "system_compliance": &entities.FactValueBool{Value: false}, }, }, "messages": &entities.FactValueList{ @@ -270,33 +270,33 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererSolutionVerify() { Value: &entities.FactValueMap{ Value: map[string]entities.FactValue{ "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_solution_verify.schema.json"}, - "publish time": &entities.FactValueString{Value: "2023-04-27 17:17:23.743"}, + "publish_time": &entities.FactValueString{Value: "2023-04-27 17:17:23.743"}, "argv": &entities.FactValueString{Value: "saptune --format json solution verify"}, "pid": &entities.FactValueInt{Value: 2538}, "command": &entities.FactValueString{Value: "solution verify"}, - "exit code": &entities.FactValueInt{Value: 1}, + "exit_code": &entities.FactValueInt{Value: 1}, "result": &entities.FactValueMap{ Value: map[string]entities.FactValue{ "verifications": &entities.FactValueList{ Value: []entities.FactValue{ &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Note ID": &entities.FactValueInt{Value: 1771258}, - "Note version": &entities.FactValueInt{Value: 6}, + "note_id": &entities.FactValueString{Value: "1771258"}, + "note_version": &entities.FactValueString{Value: "6"}, "parameter": &entities.FactValueString{Value: "LIMIT_@dba_hard_nofile"}, "compliant": &entities.FactValueBool{Value: true}, - "expected value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, - "actual value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + "expected_value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, + "actual_value": &entities.FactValueString{Value: "@dba hard nofile 1048576"}, }, }, &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Note ID": &entities.FactValueInt{Value: 1771258}, - "Note version": &entities.FactValueInt{Value: 6}, + "note_id": &entities.FactValueString{Value: "1771258"}, + "note_version": &entities.FactValueString{Value: "6"}, "parameter": &entities.FactValueString{Value: "LIMIT_@dba_soft_nofile"}, "compliant": &entities.FactValueBool{Value: true}, - "expected value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, - "actual value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + "expected_value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, + "actual_value": &entities.FactValueString{Value: "@dba soft nofile 1048576"}, }, }, }, @@ -304,12 +304,12 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererSolutionVerify() { "attentions": &entities.FactValueList{ Value: []entities.FactValue{}, }, - "Notes enabled": &entities.FactValueList{ + "notes_enabled": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1771258}, + &entities.FactValueString{Value: "1771258"}, }, }, - "system compliance": &entities.FactValueBool{Value: false}, + "system_compliance": &entities.FactValueBool{Value: false}, }, }, "messages": &entities.FactValueList{ @@ -380,46 +380,46 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererSolutionList() { Value: &entities.FactValueMap{ Value: map[string]entities.FactValue{ "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_solution_list.schema.json"}, - "publish time": &entities.FactValueString{Value: "2023-04-27 17:21:27.926"}, + "publish_time": &entities.FactValueString{Value: "2023-04-27 17:21:27.926"}, "argv": &entities.FactValueString{Value: "saptune --format json solution list"}, "pid": &entities.FactValueInt{Value: 2582}, "command": &entities.FactValueString{Value: "solution list"}, - "exit code": &entities.FactValueInt{Value: 0}, + "exit_code": &entities.FactValueInt{Value: 0}, "result": &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Solutions available": &entities.FactValueList{ + "solutions_available": &entities.FactValueList{ Value: []entities.FactValue{ &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Solution ID": &entities.FactValueString{Value: "BOBJ"}, - "Note list": &entities.FactValueList{ + "solution_id": &entities.FactValueString{Value: "BOBJ"}, + "note_list": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1771258}, + &entities.FactValueString{Value: "1771258"}, }, }, - "Solution enabled": &entities.FactValueBool{Value: false}, - "Solution override exists": &entities.FactValueBool{Value: false}, - "custom Solution": &entities.FactValueBool{Value: false}, - "Solution deprecated": &entities.FactValueBool{Value: false}, + "solution_enabled": &entities.FactValueBool{Value: false}, + "solution_override_exists": &entities.FactValueBool{Value: false}, + "custom_solution": &entities.FactValueBool{Value: false}, + "solution_deprecated": &entities.FactValueBool{Value: false}, }, }, &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Solution ID": &entities.FactValueString{Value: "DEMO"}, - "Note list": &entities.FactValueList{ + "solution_id": &entities.FactValueString{Value: "DEMO"}, + "note_list": &entities.FactValueList{ Value: []entities.FactValue{ &entities.FactValueString{Value: "demo"}, }, }, - "Solution enabled": &entities.FactValueBool{Value: false}, - "Solution override exists": &entities.FactValueBool{Value: false}, - "custom Solution": &entities.FactValueBool{Value: true}, - "Solution deprecated": &entities.FactValueBool{Value: false}, + "solution_enabled": &entities.FactValueBool{Value: false}, + "solution_override_exists": &entities.FactValueBool{Value: false}, + "custom_solution": &entities.FactValueBool{Value: true}, + "solution_deprecated": &entities.FactValueBool{Value: false}, }, }, }, }, - "remember message": &entities.FactValueString{Value: ""}, + "remember_message": &entities.FactValueString{Value: ""}, }, }, "messages": &entities.FactValueList{ @@ -466,59 +466,59 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNoteList() { Value: &entities.FactValueMap{ Value: map[string]entities.FactValue{ "$schema": &entities.FactValueString{Value: "file:///usr/share/saptune/schemas/1.0/saptune_note_list.schema.json"}, - "publish time": &entities.FactValueString{Value: "2023-04-27 17:28:53.073"}, + "publish_time": &entities.FactValueString{Value: "2023-04-27 17:28:53.073"}, "argv": &entities.FactValueString{Value: "saptune --format json note list"}, "pid": &entities.FactValueInt{Value: 2604}, "command": &entities.FactValueString{Value: "note list"}, - "exit code": &entities.FactValueInt{Value: 0}, + "exit_code": &entities.FactValueInt{Value: 0}, "result": &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Notes available": &entities.FactValueList{ + "notes_available": &entities.FactValueList{ Value: []entities.FactValue{ &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Note ID": &entities.FactValueInt{Value: 1410736}, - "Note description": &entities.FactValueString{Value: "TCP/IP: setting keepalive interval"}, - "Note reference": &entities.FactValueList{ + "note_id": &entities.FactValueString{Value: "1410736"}, + "note_description": &entities.FactValueString{Value: "TCP/IP: setting keepalive interval"}, + "note_reference": &entities.FactValueList{ Value: []entities.FactValue{ &entities.FactValueString{Value: "https://launchpad.support.sap.com/#/notes/1410736"}, }, }, - "Note version": &entities.FactValueInt{Value: 6}, - "Note release date": &entities.FactValueString{Value: "13.01.2020"}, - "Note enabled manually": &entities.FactValueBool{Value: false}, - "Note enabled by Solution": &entities.FactValueBool{Value: false}, - "Note reverted manually": &entities.FactValueBool{Value: false}, - "Note override exists": &entities.FactValueBool{Value: false}, - "custom Note": &entities.FactValueBool{Value: false}, + "note_version": &entities.FactValueString{Value: "6"}, + "note_release_date": &entities.FactValueString{Value: "13.01.2020"}, + "note_enabled_manually": &entities.FactValueBool{Value: false}, + "note_enabled_by_solution": &entities.FactValueBool{Value: false}, + "note_reverted_manually": &entities.FactValueBool{Value: false}, + "note_override_exists": &entities.FactValueBool{Value: false}, + "custom_note": &entities.FactValueBool{Value: false}, }, }, &entities.FactValueMap{ Value: map[string]entities.FactValue{ - "Note ID": &entities.FactValueInt{Value: 1656250}, - "Note description": &entities.FactValueString{Value: "SAP on AWS: prerequisites - only Linux"}, - "Note reference": &entities.FactValueList{ + "note_id": &entities.FactValueString{Value: "1656250"}, + "note_description": &entities.FactValueString{Value: "SAP on AWS: prerequisites - only Linux"}, + "note_reference": &entities.FactValueList{ Value: []entities.FactValue{ &entities.FactValueString{Value: "https://launchpad.support.sap.com/#/notes/1656250"}, }, }, - "Note version": &entities.FactValueInt{Value: 46}, - "Note release date": &entities.FactValueString{Value: "11.05.2022"}, - "Note enabled manually": &entities.FactValueBool{Value: false}, - "Note enabled by Solution": &entities.FactValueBool{Value: true}, - "Note reverted manually": &entities.FactValueBool{Value: false}, - "Note override exists": &entities.FactValueBool{Value: false}, - "custom Note": &entities.FactValueBool{Value: false}, + "note_version": &entities.FactValueString{Value: "46"}, + "note_release_date": &entities.FactValueString{Value: "11.05.2022"}, + "note_enabled_manually": &entities.FactValueBool{Value: false}, + "note_enabled_by_solution": &entities.FactValueBool{Value: true}, + "note_reverted_manually": &entities.FactValueBool{Value: false}, + "note_override_exists": &entities.FactValueBool{Value: false}, + "custom_note": &entities.FactValueBool{Value: false}, }, }, }, }, - "Notes enabled": &entities.FactValueList{ + "notes_enabled": &entities.FactValueList{ Value: []entities.FactValue{ - &entities.FactValueInt{Value: 1656250}, + &entities.FactValueString{Value: "1656250"}, }, }, - "remember message": &entities.FactValueString{Value: ""}, + "remember_message": &entities.FactValueString{Value: ""}, }, }, "messages": &entities.FactValueList{ From 49e6d14e41478365ed4d1d3baad8b1ae8d8652ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Torrero=20Marijnissen?= Date: Tue, 26 Sep 2023 17:17:23 +0100 Subject: [PATCH 3/5] Improve error handling and add missing tests --- internal/factsengine/gatherers/saptune.go | 17 +-- .../factsengine/gatherers/saptune_test.go | 128 ++++++++++++++++++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/internal/factsengine/gatherers/saptune.go b/internal/factsengine/gatherers/saptune.go index 7ebe318b..7c175177 100644 --- a/internal/factsengine/gatherers/saptune.go +++ b/internal/factsengine/gatherers/saptune.go @@ -35,7 +35,7 @@ var ( } SaptuneArgumentUnsupported = entities.FactGatheringError{ - Type: "saptune-argument-not-error", + Type: "saptune-unsupported-argument", Message: "the requested argument is not currently supported", } @@ -70,13 +70,6 @@ func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entiti facts := []entities.Fact{} log.Infof("Starting %s facts gathering process", SaptuneGathererName) saptuneRetriever, err := saptune.NewSaptune(s.executor) - if err != nil { - return facts, &SaptuneNotInstalled - } - - if !saptuneRetriever.IsJSONSupported { - return facts, &SaptuneVersionUnsupported - } for _, factReq := range factsRequests { var fact entities.Fact @@ -85,6 +78,14 @@ func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entiti cachedFact, cacheHit := cachedFacts[factReq.Argument] switch { + case err != nil: + log.Error(err) + fact = entities.NewFactGatheredWithError(factReq, &SaptuneNotInstalled) + + case !saptuneRetriever.IsJSONSupported: + log.Error(SaptuneVersionUnsupported.Message) + fact = entities.NewFactGatheredWithError(factReq, &SaptuneVersionUnsupported) + case len(factReq.Argument) == 0: log.Error(SaptuneMissingArgument.Message) fact = entities.NewFactGatheredWithError(factReq, &SaptuneMissingArgument) diff --git a/internal/factsengine/gatherers/saptune_test.go b/internal/factsengine/gatherers/saptune_test.go index 06553aa2..281023f8 100644 --- a/internal/factsengine/gatherers/saptune_test.go +++ b/internal/factsengine/gatherers/saptune_test.go @@ -2,6 +2,7 @@ package gatherers_test import ( + "errors" "io" "os" "testing" @@ -583,6 +584,133 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNoArgumentProvided() { suite.ElementsMatch(expectedResults, factResults) } +func (suite *SaptuneTestSuite) TestSaptuneGathererUnsupportedArgument() { + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "unsupported_argument_fact", + Gatherer: "saptune", + Argument: "unsupported", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "unsupported_argument_fact", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "the requested argument is not currently supported: unsupported", + Type: "saptune-unsupported-argument", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererVersionUnsupported() { + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("2.0.0"), nil, + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_status", + Gatherer: "saptune", + Argument: "status", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_status", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "currently installed version of saptune is not supported", + Type: "saptune-version-not-supported", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererNotInstalled() { + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + nil, errors.New("exit status 1"), + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_status", + Gatherer: "saptune", + Argument: "status", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_status", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "saptune is not installed", + Type: "saptune-not-installed", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SaptuneTestSuite) TestSaptuneGathererCommandError() { + suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( + []byte("3.1.0"), nil, + ) + suite.mockExecutor.On("Exec", "saptune", "--format", "json", "status", "--non-compliance-check").Return( + nil, errors.New("exit status 1"), + ) + c := gatherers.NewSaptuneGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "saptune_status", + Gatherer: "saptune", + Argument: "status", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "saptune_status", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "error executing saptune command: unexpected end of JSON input", + Type: "saptune-cmd-error", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + func (suite *SaptuneTestSuite) TestSaptuneGathererCommandCaching() { suite.mockExecutor.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return( []byte("3.1.0"), nil, From b6be25250a3ca1434a04e7991e355ab87d9b6c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Torrero=20Marijnissen?= Date: Wed, 27 Sep 2023 09:27:26 +0100 Subject: [PATCH 4/5] Last fixes --- internal/factsengine/gatherers/saptune.go | 21 ++++++--------- .../factsengine/gatherers/saptune_test.go | 26 +++---------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/internal/factsengine/gatherers/saptune.go b/internal/factsengine/gatherers/saptune.go index 7c175177..70d5279e 100644 --- a/internal/factsengine/gatherers/saptune.go +++ b/internal/factsengine/gatherers/saptune.go @@ -70,7 +70,14 @@ func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entiti facts := []entities.Fact{} log.Infof("Starting %s facts gathering process", SaptuneGathererName) saptuneRetriever, err := saptune.NewSaptune(s.executor) - + if err != nil { + return facts, &SaptuneNotInstalled + } + + if !saptuneRetriever.IsJSONSupported { + return facts, &SaptuneVersionUnsupported + } + for _, factReq := range factsRequests { var fact entities.Fact @@ -78,14 +85,6 @@ func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entiti cachedFact, cacheHit := cachedFacts[factReq.Argument] switch { - case err != nil: - log.Error(err) - fact = entities.NewFactGatheredWithError(factReq, &SaptuneNotInstalled) - - case !saptuneRetriever.IsJSONSupported: - log.Error(SaptuneVersionUnsupported.Message) - fact = entities.NewFactGatheredWithError(factReq, &SaptuneVersionUnsupported) - case len(factReq.Argument) == 0: log.Error(SaptuneMissingArgument.Message) fact = entities.NewFactGatheredWithError(factReq, &SaptuneMissingArgument) @@ -127,14 +126,10 @@ func runCommand(saptuneRetriever *saptune.Saptune, arguments []string) (entities return nil, commandError } - log.Error(string(saptuneOutput)) - var jsonData interface{} if err := json.Unmarshal(saptuneOutput, &jsonData); err != nil { return nil, err } - log.Error(jsonData) - return entities.NewFactValue(jsonData, entities.WithSnakeCaseKeys()) } diff --git a/internal/factsengine/gatherers/saptune_test.go b/internal/factsengine/gatherers/saptune_test.go index 281023f8..683943e6 100644 --- a/internal/factsengine/gatherers/saptune_test.go +++ b/internal/factsengine/gatherers/saptune_test.go @@ -631,18 +631,9 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererVersionUnsupported() { factResults, err := c.Gather(factRequests) - expectedResults := []entities.Fact{ - { - Name: "saptune_status", - Value: nil, - Error: &entities.FactGatheringError{ - Message: "currently installed version of saptune is not supported", - Type: "saptune-version-not-supported", - }, - }, - } + expectedResults := []entities.Fact{} - suite.NoError(err) + suite.EqualError(err, "fact gathering error: saptune-version-not-supported - currently installed version of saptune is not supported") suite.ElementsMatch(expectedResults, factResults) } @@ -662,18 +653,9 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNotInstalled() { factResults, err := c.Gather(factRequests) - expectedResults := []entities.Fact{ - { - Name: "saptune_status", - Value: nil, - Error: &entities.FactGatheringError{ - Message: "saptune is not installed", - Type: "saptune-not-installed", - }, - }, - } + expectedResults := []entities.Fact{} - suite.NoError(err) + suite.EqualError(err, "fact gathering error: saptune-not-installed - saptune is not installed") suite.ElementsMatch(expectedResults, factResults) } From 05ac7a8ddcd527a75e63834bb5e11b1cf7a03a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Torrero=20Marijnissen?= Date: Wed, 27 Sep 2023 11:03:48 +0100 Subject: [PATCH 5/5] Return explicit nil Fact on critical errors --- internal/factsengine/gatherers/saptune.go | 8 ++++---- internal/factsengine/gatherers/saptune_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/factsengine/gatherers/saptune.go b/internal/factsengine/gatherers/saptune.go index 70d5279e..5951f6f2 100644 --- a/internal/factsengine/gatherers/saptune.go +++ b/internal/factsengine/gatherers/saptune.go @@ -71,13 +71,13 @@ func (s *SaptuneGatherer) Gather(factsRequests []entities.FactRequest) ([]entiti log.Infof("Starting %s facts gathering process", SaptuneGathererName) saptuneRetriever, err := saptune.NewSaptune(s.executor) if err != nil { - return facts, &SaptuneNotInstalled + return nil, SaptuneNotInstalled.Wrap(err.Error()) } - + if !saptuneRetriever.IsJSONSupported { - return facts, &SaptuneVersionUnsupported + return nil, &SaptuneVersionUnsupported } - + for _, factReq := range factsRequests { var fact entities.Fact diff --git a/internal/factsengine/gatherers/saptune_test.go b/internal/factsengine/gatherers/saptune_test.go index 683943e6..914785b2 100644 --- a/internal/factsengine/gatherers/saptune_test.go +++ b/internal/factsengine/gatherers/saptune_test.go @@ -655,7 +655,7 @@ func (suite *SaptuneTestSuite) TestSaptuneGathererNotInstalled() { expectedResults := []entities.Fact{} - suite.EqualError(err, "fact gathering error: saptune-not-installed - saptune is not installed") + suite.EqualError(err, "fact gathering error: saptune-not-installed - saptune is not installed: could not determine saptune version: exit status 1") suite.ElementsMatch(expectedResults, factResults) }