Skip to content

Commit

Permalink
sysctl gatherer (#268)
Browse files Browse the repository at this point in the history
* Add sysctl gatherer
  • Loading branch information
rtorrero authored Oct 9, 2023
1 parent 7a5c230 commit 0a5bf24
Show file tree
Hide file tree
Showing 4 changed files with 474 additions and 0 deletions.
1 change: 1 addition & 0 deletions internal/factsengine/gatherers/gatherer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func StandardGatherers() map[string]FactGatherer {
SaptuneGathererName: NewDefaultSaptuneGatherer(),
SBDConfigGathererName: NewDefaultSBDGatherer(),
SBDDumpGathererName: NewDefaultSBDDumpGatherer(),
SysctlGathererName: NewDefaultSysctlGatherer(),
SystemDGathererName: NewDefaultSystemDGatherer(),
VerifyPasswordGathererName: NewDefaultPasswordGatherer(),
}
Expand Down
108 changes: 108 additions & 0 deletions internal/factsengine/gatherers/sysctl.go
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
}
217 changes: 217 additions & 0 deletions internal/factsengine/gatherers/sysctl_test.go
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)
}
Loading

0 comments on commit 0a5bf24

Please sign in to comment.