-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add sysctl gatherer
- Loading branch information
Showing
4 changed files
with
474 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package gatherers | ||
|
||
import ( | ||
"strings" | ||
|
||
log "github.com/sirupsen/logrus" | ||
"github.com/trento-project/agent/pkg/factsengine/entities" | ||
"github.com/trento-project/agent/pkg/utils" | ||
) | ||
|
||
const ( | ||
SysctlGathererName = "sysctl" | ||
) | ||
|
||
// nolint:gochecknoglobals | ||
var ( | ||
SysctlValueNotFound = entities.FactGatheringError{ | ||
Type: "sysctl-value-not-found", | ||
Message: "requested value not found in sysctl output", | ||
} | ||
|
||
SysctlCommandError = entities.FactGatheringError{ | ||
Type: "sysctl-cmd-error", | ||
Message: "error executing sysctl command", | ||
} | ||
|
||
SysctlMissingArgument = entities.FactGatheringError{ | ||
Type: "sysctl-missing-argument", | ||
Message: "missing required argument", | ||
} | ||
) | ||
|
||
type SysctlGatherer struct { | ||
executor utils.CommandExecutor | ||
} | ||
|
||
func NewDefaultSysctlGatherer() *SysctlGatherer { | ||
return NewSysctlGatherer(utils.Executor{}) | ||
} | ||
|
||
func NewSysctlGatherer(executor utils.CommandExecutor) *SysctlGatherer { | ||
return &SysctlGatherer{ | ||
executor: executor, | ||
} | ||
} | ||
|
||
func (s *SysctlGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) { | ||
facts := []entities.Fact{} | ||
log.Infof("Starting %s facts gathering process", SysctlGathererName) | ||
|
||
output, err := s.executor.Exec("sysctl", "-a") | ||
if err != nil { | ||
return nil, SysctlCommandError.Wrap(err.Error()) | ||
} | ||
|
||
sysctlMap := sysctlOutputToMap(output) | ||
for _, factReq := range factsRequests { | ||
var fact entities.Fact | ||
|
||
if len(factReq.Argument) == 0 { | ||
log.Error(SysctlMissingArgument.Message) | ||
fact = entities.NewFactGatheredWithError(factReq, &SysctlMissingArgument) | ||
} else if value, err := sysctlMap.GetValue(factReq.Argument); err == nil { | ||
fact = entities.NewFactGatheredWithRequest(factReq, value) | ||
} else { | ||
gatheringError := SysctlValueNotFound.Wrap(factReq.Argument) | ||
log.Error(gatheringError) | ||
fact = entities.NewFactGatheredWithError(factReq, gatheringError) | ||
} | ||
|
||
facts = append(facts, fact) | ||
} | ||
|
||
log.Infof("Requested %s facts gathered", SysctlGathererName) | ||
return facts, nil | ||
} | ||
|
||
func sysctlOutputToMap(output []byte) *entities.FactValueMap { | ||
outputMap := &entities.FactValueMap{Value: make(map[string]entities.FactValue)} | ||
|
||
for _, line := range strings.Split(string(output), "\n") { | ||
parts := strings.SplitN(line, "=", 2) | ||
if len(line) == 0 || len(parts) != 2 { | ||
log.Error("Invalid sysctl output line: ", line) | ||
continue | ||
} | ||
|
||
key := strings.TrimSpace(parts[0]) | ||
value := strings.TrimSpace(parts[1]) | ||
|
||
cursor := outputMap | ||
pathComponents := strings.Split(key, ".") | ||
|
||
for i, component := range pathComponents { | ||
if i == len(pathComponents)-1 { | ||
cursor.Value[component] = entities.ParseStringToFactValue(value) | ||
} else if nestedMap, ok := cursor.Value[component].(*entities.FactValueMap); !ok { | ||
newMap := &entities.FactValueMap{Value: make(map[string]entities.FactValue)} | ||
cursor.Value[component] = newMap | ||
cursor = newMap | ||
} else { | ||
cursor = nestedMap | ||
} | ||
} | ||
} | ||
|
||
return outputMap | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
package gatherers_test | ||
|
||
import ( | ||
"io" | ||
"os" | ||
"os/exec" | ||
"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 SysctlTestSuite struct { | ||
suite.Suite | ||
mockExecutor *utilsMocks.CommandExecutor | ||
} | ||
|
||
func TestSysctlTestSuite(t *testing.T) { | ||
suite.Run(t, new(SysctlTestSuite)) | ||
} | ||
|
||
func (suite *SysctlTestSuite) SetupTest() { | ||
suite.mockExecutor = new(utilsMocks.CommandExecutor) | ||
} | ||
|
||
func (suite *SysctlTestSuite) TestSysctlGathererNoArgumentProvided() { | ||
mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) | ||
mockOutput, _ := io.ReadAll(mockOutputFile) | ||
suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) | ||
|
||
c := gatherers.NewSysctlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "no_argument_fact", | ||
Gatherer: "sysctl", | ||
}, | ||
{ | ||
Name: "empty_argument_fact", | ||
Gatherer: "sysctl", | ||
Argument: "", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "no_argument_fact", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "missing required argument", | ||
Type: "sysctl-missing-argument", | ||
}, | ||
}, | ||
{ | ||
Name: "empty_argument_fact", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "missing required argument", | ||
Type: "sysctl-missing-argument", | ||
}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SysctlTestSuite) TestSysctlGathererNonExistingKey() { | ||
mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) | ||
mockOutput, _ := io.ReadAll(mockOutputFile) | ||
suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) | ||
|
||
c := gatherers.NewSysctlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "madeup_fact", | ||
Gatherer: "sysctl", | ||
Argument: "madeup.fact", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "madeup_fact", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "requested value not found in sysctl output: madeup.fact", | ||
Type: "sysctl-value-not-found", | ||
}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SysctlTestSuite) TestSysctlCommandNotFound() { | ||
suite.mockExecutor.On("Exec", "sysctl", "-a").Return(nil, exec.ErrNotFound) | ||
|
||
c := gatherers.NewSysctlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "fs.inotify.max_user_watches", | ||
Gatherer: "sysctl", | ||
Argument: "fs.inotify.max_user_watches", | ||
}, | ||
} | ||
|
||
expectedError := &entities.FactGatheringError{ | ||
Message: "error executing sysctl command: executable file not found in $PATH", | ||
Type: "sysctl-cmd-error", | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
suite.EqualError(err, expectedError.Error()) | ||
|
||
suite.Empty(factResults) | ||
} | ||
|
||
func (suite *SysctlTestSuite) TestSysctlGatherer() { | ||
mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) | ||
mockOutput, _ := io.ReadAll(mockOutputFile) | ||
suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) | ||
|
||
c := gatherers.NewSysctlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "simple_value", | ||
Gatherer: "sysctl", | ||
Argument: "fs.inotify.max_user_watches", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "simple_value", | ||
Value: &entities.FactValueInt{Value: 65536}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SysctlTestSuite) TestSysctlGathererPartialKey() { | ||
mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) | ||
mockOutput, _ := io.ReadAll(mockOutputFile) | ||
suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) | ||
|
||
c := gatherers.NewSysctlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "partial_key", | ||
Gatherer: "sysctl", | ||
Argument: "debug", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "partial_key", | ||
Value: &entities.FactValueMap{ | ||
Value: map[string]entities.FactValue{ | ||
"exception-trace": &entities.FactValueInt{Value: 1}, | ||
"kprobes-optimization": &entities.FactValueInt{Value: 1}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SysctlTestSuite) TestSysctlGathererEmptyValue() { | ||
mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) | ||
mockOutput, _ := io.ReadAll(mockOutputFile) | ||
suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) | ||
|
||
c := gatherers.NewSysctlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "empty_value", | ||
Gatherer: "sysctl", | ||
Argument: "kernel.domainname", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "empty_value", | ||
Value: &entities.FactValueString{Value: ""}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} |
Oops, something went wrong.