-
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.
Implement saphostctrl gatherer (#71)
* Implement saphostctrl gatherer * Add whitelisting of Ping and ListInstances command * Add support to parse FAILED ping Co-authored-by: Rubén Torrero Marijnissen <[email protected]> Co-authored-by: Nelson Kopliku <[email protected]> Co-authored-by: Xabier Arbulu <[email protected]>
- Loading branch information
1 parent
4c34887
commit 3798072
Showing
3 changed files
with
367 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,153 @@ | ||
package gatherers | ||
|
||
import ( | ||
"regexp" | ||
"strings" | ||
|
||
log "github.com/sirupsen/logrus" | ||
"github.com/trento-project/agent/pkg/factsengine/entities" | ||
"github.com/trento-project/agent/pkg/utils" | ||
) | ||
|
||
const ( | ||
SapHostCtrlGathererName = "saphostctrl" | ||
) | ||
|
||
// nolint:gochecknoglobals | ||
var ( | ||
saphostCtrlListInstancesParsingRegexp = regexp.MustCompile(`^\s+Inst Info\s*` + | ||
`:\s*([^-]+?)\s*-\s*(\d+)\s*-\s*([^,]+?)` + | ||
`\s*-\s*(\d+),\s*patch\s*(\d+),\s*changelist\s*(\d+)$`) | ||
saphostCtrlPingParsingRegexp = regexp.MustCompile(`(SUCCESS|FAILED) \( *(\d+) usec\)`) | ||
) | ||
|
||
// nolint:gochecknoglobals | ||
var whitelistedWebmethods = map[string]func(string) (entities.FactValue, *entities.FactGatheringError){ | ||
"Ping": parsePing, | ||
"ListInstances": parseInstances, | ||
} | ||
|
||
// nolint:gochecknoglobals | ||
var ( | ||
SapHostCtrlCommandError = entities.FactGatheringError{ | ||
Type: "saphostctrl-cmd-error", | ||
Message: "error executing saphostctrl command", | ||
} | ||
SapHostCtrlUnsupportedFunction = entities.FactGatheringError{ | ||
Type: "saphostctrl-webmethod-error", | ||
Message: "requested webmethod not supported", | ||
} | ||
SapHostCtrlParseError = entities.FactGatheringError{ | ||
Type: "saphostctrl-parse-error", | ||
Message: "error while parsing saphostctrl output", | ||
} | ||
) | ||
|
||
type SapHostCtrlGatherer struct { | ||
executor utils.CommandExecutor | ||
} | ||
|
||
func NewDefaultSapHostCtrlGatherer() *SapHostCtrlGatherer { | ||
return NewSapHostCtrlGatherer(utils.Executor{}) | ||
} | ||
|
||
func NewSapHostCtrlGatherer(executor utils.CommandExecutor) *SapHostCtrlGatherer { | ||
return &SapHostCtrlGatherer{ | ||
executor: executor, | ||
} | ||
} | ||
|
||
func (g *SapHostCtrlGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) { | ||
facts := []entities.Fact{} | ||
log.Infof("Starting saphostctrl facts gathering process") | ||
|
||
for _, factReq := range factsRequests { | ||
var fact entities.Fact | ||
|
||
if factValue, err := handleWebmethod(g.executor, factReq.Argument); err != nil { | ||
fact = entities.NewFactGatheredWithError(factReq, err) | ||
} else { | ||
fact = entities.NewFactGatheredWithRequest(factReq, factValue) | ||
} | ||
|
||
facts = append(facts, fact) | ||
} | ||
|
||
log.Infof("Requested %s facts gathered", SapHostCtrlGathererName) | ||
return facts, nil | ||
} | ||
|
||
func handleWebmethod( | ||
executor utils.CommandExecutor, | ||
webMethod string, | ||
) (entities.FactValue, *entities.FactGatheringError) { | ||
webMethodHandler, ok := whitelistedWebmethods[webMethod] | ||
|
||
if !ok { | ||
gatheringError := SapHostCtrlUnsupportedFunction.Wrap(webMethod) | ||
log.Error(gatheringError) | ||
return nil, gatheringError | ||
} | ||
|
||
saphostctlOutput, commandError := executeSapHostCtrlCommand(executor, webMethod) | ||
if commandError != nil { | ||
return nil, commandError | ||
} | ||
|
||
return webMethodHandler(saphostctlOutput) | ||
} | ||
|
||
func executeSapHostCtrlCommand(executor utils.CommandExecutor, command string) (string, *entities.FactGatheringError) { | ||
saphostctlOutput, err := executor.Exec("/usr/sap/hostctrl/exe/saphostctrl", "-function", command) | ||
if err != nil { | ||
gatheringError := SapHostCtrlCommandError.Wrap(err.Error()) | ||
log.Error(gatheringError) | ||
return "", gatheringError | ||
} | ||
|
||
return string(saphostctlOutput), nil | ||
} | ||
|
||
func parsePing(commandOutput string) (entities.FactValue, *entities.FactGatheringError) { | ||
pingData := map[string]entities.FactValue{} | ||
|
||
matches := saphostCtrlPingParsingRegexp.FindStringSubmatch(commandOutput) | ||
if len(matches) < 2 { | ||
return nil, SapHostCtrlParseError.Wrap(commandOutput) | ||
} | ||
|
||
pingData["status"] = &entities.FactValueString{Value: matches[1]} | ||
pingData["elapsed"] = entities.ParseStringToFactValue(matches[2]) | ||
|
||
result := &entities.FactValueMap{Value: pingData} | ||
|
||
return result, nil | ||
} | ||
|
||
func parseInstances(commandOutput string) (entities.FactValue, *entities.FactGatheringError) { | ||
lines := strings.Split(commandOutput, "\n") | ||
instances := []entities.FactValue{} | ||
|
||
for _, line := range lines { | ||
instance := map[string]entities.FactValue{} | ||
if saphostCtrlListInstancesParsingRegexp.MatchString(line) { | ||
fields := saphostCtrlListInstancesParsingRegexp.FindStringSubmatch(line) | ||
if len(fields) < 6 { | ||
return nil, SapHostCtrlParseError.Wrap(commandOutput) | ||
} | ||
|
||
instance["system"] = &entities.FactValueString{Value: fields[1]} | ||
instance["instance"] = &entities.FactValueString{Value: fields[2]} | ||
instance["hostname"] = &entities.FactValueString{Value: fields[3]} | ||
instance["sapkernel"] = entities.ParseStringToFactValue(fields[4]) | ||
instance["patch"] = entities.ParseStringToFactValue(fields[5]) | ||
instance["changelist"] = entities.ParseStringToFactValue(fields[6]) | ||
|
||
instances = append(instances, &entities.FactValueMap{Value: instance}) | ||
} | ||
} | ||
|
||
result := &entities.FactValueList{Value: instances} | ||
|
||
return result, nil | ||
} |
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,213 @@ | ||
package gatherers_test | ||
|
||
import ( | ||
"errors" | ||
"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" | ||
) | ||
|
||
type SapHostCtrlTestSuite struct { | ||
suite.Suite | ||
mockExecutor *utilsMocks.CommandExecutor | ||
} | ||
|
||
func TestSapHostCtrlTestSuite(t *testing.T) { | ||
suite.Run(t, new(SapHostCtrlTestSuite)) | ||
} | ||
|
||
func (suite *SapHostCtrlTestSuite) SetupTest() { | ||
suite.mockExecutor = new(utilsMocks.CommandExecutor) | ||
} | ||
|
||
func (suite *SapHostCtrlTestSuite) TestSapHostCtrlGatherListInstances() { | ||
suite.mockExecutor.On("Exec", "/usr/sap/hostctrl/exe/saphostctrl", "-function", "ListInstances").Return( | ||
[]byte(" Inst Info : S41 - 41 - s41app - 785, patch 50, changelist 2091708\n"+ | ||
" Inst Info : S41 - 40 - s41app - 785, patch 50, changelist 2091708\n"+ | ||
" Inst Info : HS1 - 11 - s41db - 753, patch 819, changelist 2069355\n"), nil) | ||
|
||
p := gatherers.NewSapHostCtrlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "listinstances", | ||
Gatherer: "saphostctrl", | ||
Argument: "ListInstances", | ||
CheckID: "check2", | ||
}, | ||
} | ||
|
||
factResults, err := p.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "listinstances", | ||
CheckID: "check2", | ||
Value: &entities.FactValueList{Value: []entities.FactValue{ | ||
&entities.FactValueMap{Value: map[string]entities.FactValue{ | ||
"changelist": &entities.FactValueInt{Value: 2091708}, | ||
"hostname": &entities.FactValueString{Value: "s41app"}, | ||
"instance": &entities.FactValueString{Value: "41"}, | ||
"patch": &entities.FactValueInt{Value: 50}, | ||
"sapkernel": &entities.FactValueInt{Value: 785}, | ||
"system": &entities.FactValueString{Value: "S41"}, | ||
}}, | ||
&entities.FactValueMap{Value: map[string]entities.FactValue{ | ||
"changelist": &entities.FactValueInt{Value: 2091708}, | ||
"hostname": &entities.FactValueString{Value: "s41app"}, | ||
"instance": &entities.FactValueString{Value: "40"}, | ||
"patch": &entities.FactValueInt{Value: 50}, | ||
"sapkernel": &entities.FactValueInt{Value: 785}, | ||
"system": &entities.FactValueString{Value: "S41"}, | ||
}}, | ||
&entities.FactValueMap{Value: map[string]entities.FactValue{ | ||
"changelist": &entities.FactValueInt{Value: 2069355}, | ||
"hostname": &entities.FactValueString{Value: "s41db"}, | ||
"instance": &entities.FactValueString{Value: "11"}, | ||
"patch": &entities.FactValueInt{Value: 819}, | ||
"sapkernel": &entities.FactValueInt{Value: 753}, | ||
"system": &entities.FactValueString{Value: "HS1"}, | ||
}}, | ||
}}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SapHostCtrlTestSuite) TestSapHostCtrlGatherPingSuccess() { | ||
suite.mockExecutor.On("Exec", "/usr/sap/hostctrl/exe/saphostctrl", "-function", "Ping").Return( | ||
[]byte("SUCCESS ( 543341 usec)\n"), nil) | ||
|
||
p := gatherers.NewSapHostCtrlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "ping", | ||
Gatherer: "saphostctrl", | ||
Argument: "Ping", | ||
CheckID: "check1", | ||
}, | ||
} | ||
|
||
factResults, err := p.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "ping", | ||
CheckID: "check1", | ||
Value: &entities.FactValueMap{ | ||
Value: map[string]entities.FactValue{ | ||
"status": &entities.FactValueString{Value: "SUCCESS"}, | ||
"elapsed": &entities.FactValueInt{Value: 543341}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SapHostCtrlTestSuite) TestSapHostCtrlGatherPingFailed() { | ||
suite.mockExecutor.On("Exec", "/usr/sap/hostctrl/exe/saphostctrl", "-function", "Ping").Return( | ||
[]byte("FAILED ( 497 usec)\n"), nil) | ||
|
||
p := gatherers.NewSapHostCtrlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "ping", | ||
Gatherer: "saphostctrl", | ||
Argument: "Ping", | ||
CheckID: "check1", | ||
}, | ||
} | ||
|
||
factResults, err := p.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "ping", | ||
CheckID: "check1", | ||
Value: &entities.FactValueMap{ | ||
Value: map[string]entities.FactValue{ | ||
"status": &entities.FactValueString{Value: "FAILED"}, | ||
"elapsed": &entities.FactValueInt{Value: 497}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *SapHostCtrlTestSuite) TestSapHostCtrlGatherError() { | ||
suite.mockExecutor.On("Exec", "/usr/sap/hostctrl/exe/saphostctrl", "-function", "Ping").Return( | ||
[]byte("Unexpected output\n"), nil) | ||
suite.mockExecutor.On("Exec", "/usr/sap/hostctrl/exe/saphostctrl", "-function", "ListInstances").Return( | ||
nil, errors.New("some error")) | ||
|
||
p := gatherers.NewSapHostCtrlGatherer(suite.mockExecutor) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "ping", | ||
Gatherer: "saphostctrl", | ||
Argument: "Ping", | ||
CheckID: "check1", | ||
}, | ||
{ | ||
Name: "start_instance", | ||
Gatherer: "saphostctrl", | ||
Argument: "StartInstance", | ||
CheckID: "check2", | ||
}, | ||
{ | ||
Name: "list_instances", | ||
Gatherer: "saphostctrl", | ||
Argument: "ListInstances", | ||
CheckID: "check3", | ||
}, | ||
} | ||
|
||
factResults, err := p.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "ping", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "error while parsing saphostctrl output: Unexpected output\n", | ||
Type: "saphostctrl-parse-error", | ||
}, | ||
CheckID: "check1", | ||
}, | ||
{ | ||
Name: "start_instance", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "requested webmethod not supported: StartInstance", | ||
Type: "saphostctrl-webmethod-error", | ||
}, | ||
CheckID: "check2", | ||
}, | ||
{ | ||
Name: "list_instances", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "error executing saphostctrl command: some error", | ||
Type: "saphostctrl-cmd-error", | ||
}, | ||
CheckID: "check3", | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} |