Skip to content

Commit

Permalink
Implement saphostctrl gatherer (#71)
Browse files Browse the repository at this point in the history
* 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
3 people authored Dec 23, 2022
1 parent 4c34887 commit 3798072
Show file tree
Hide file tree
Showing 3 changed files with 367 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 @@ -18,5 +18,6 @@ func StandardGatherers() map[string]FactGatherer {
PackageVersionGathererName: NewDefaultPackageVersionGatherer(),
SBDConfigGathererName: NewDefaultSBDGatherer(),
SBDDumpGathererName: NewDefaultSBDDumpGatherer(),
SapHostCtrlGathererName: NewDefaultSapHostCtrlGatherer(),
}
}
153 changes: 153 additions & 0 deletions internal/factsengine/gatherers/saphostctrl.go
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
}
213 changes: 213 additions & 0 deletions internal/factsengine/gatherers/saphostctrl_test.go
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)
}

0 comments on commit 3798072

Please sign in to comment.