Skip to content

Commit

Permalink
Add saptune discovery (#253)
Browse files Browse the repository at this point in the history
* Add saptune core functionality

* Add saptune discovery
  • Loading branch information
rtorrero committed Sep 19, 2023
1 parent bc65214 commit c802521
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func LoadConfig(fileSystem afero.Fs) (*agent.Config, error) {
"cloud-discovery-period": discovery.CloudDiscoveryMinPeriod,
"host-discovery-period": discovery.HostDiscoveryMinPeriod,
"subscription-discovery-period": discovery.SubscriptionDiscoveryMinPeriod,
"saptune-discovery-period": discovery.SaptuneDiscoveryMinPeriod,
}

for flagName, minPeriodValue := range minPeriodValues {
Expand Down Expand Up @@ -68,6 +69,7 @@ func LoadConfig(fileSystem afero.Fs) (*agent.Config, error) {
Cloud: viper.GetDuration("cloud-discovery-period"),
Host: viper.GetDuration("host-discovery-period"),
Subscription: viper.GetDuration("subscription-discovery-period"),
Saptune: viper.GetDuration("saptune-discovery-period"),
}

discoveriesConfig := &discovery.DiscoveriesConfig{
Expand Down
3 changes: 3 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (suite *AgentCmdTestSuite) SetupTest() {
Cloud: 10 * time.Second,
Host: 10 * time.Second,
Subscription: 900 * time.Second,
Saptune: 10 * time.Second,
},
CollectorConfig: &collector.Config{
ServerURL: "http://serverurl",
Expand All @@ -78,6 +79,7 @@ func (suite *AgentCmdTestSuite) TestConfigFromFlags() {
"--sapsystem-discovery-period=10s",
"--host-discovery-period=10s",
"--subscription-discovery-period=900s",
"--saptune-discovery-period=10s",
"--server-url=http://serverurl",
"--api-key=some-api-key",
"--force-agent-id=some-agent-id",
Expand All @@ -99,6 +101,7 @@ func (suite *AgentCmdTestSuite) TestConfigFromEnv() {
os.Setenv("TRENTO_SAPSYSTEM_DISCOVERY_PERIOD", "10s")
os.Setenv("TRENTO_HOST_DISCOVERY_PERIOD", "10s")
os.Setenv("TRENTO_SUBSCRIPTION_DISCOVERY_PERIOD", "900s")
os.Setenv("TRENTO_SAPTUNE_DISCOVERY_PERIOD", "10s")
os.Setenv("TRENTO_SERVER_URL", "http://serverurl")
os.Setenv("TRENTO_API_KEY", "some-api-key")
os.Setenv("TRENTO_FORCE_AGENT_ID", "some-agent-id")
Expand Down
10 changes: 10 additions & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func NewStartCmd() *cobra.Command {
var cloudDiscoveryPeriod time.Duration
var hostDiscoveryPeriod time.Duration
var subscriptionDiscoveryPeriod time.Duration
var saptuneDiscoveryPeriod time.Duration

startCmd := &cobra.Command{ //nolint
Use: "start",
Expand Down Expand Up @@ -103,6 +104,15 @@ func NewStartCmd() *cobra.Command {
panic(err)
}

startCmd.Flags().
DurationVarP(
&saptuneDiscoveryPeriod,
"saptune-discovery-period",
"",
10*time.Second,
"Saptune discovery mechanism loop period in seconds",
)

startCmd.Flags().
String("force-agent-id", "", "Agent ID. Used to mock the real ID for development purposes")
err = startCmd.Flags().
Expand Down
1 change: 1 addition & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func NewAgent(config *Config) (*Agent, error) {
discovery.NewCloudDiscovery(collectorClient, *config.DiscoveriesConfig),
discovery.NewSubscriptionDiscovery(collectorClient, config.InstanceName, *config.DiscoveriesConfig),
discovery.NewHostDiscovery(collectorClient, config.InstanceName, *config.DiscoveriesConfig),
discovery.NewSaptuneDiscovery(collectorClient, *config.DiscoveriesConfig),
}

agent := &Agent{
Expand Down
77 changes: 77 additions & 0 deletions internal/core/saptune/saptune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package saptune

import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/trento-project/agent/pkg/utils"
"golang.org/x/mod/semver"
)

var (
ErrSaptuneVersionUnknown = errors.New("could not determine saptune version")
ErrUnsupportedSaptuneVer = errors.New("saptune version is not supported")
)

const (
MinimalSaptuneVersion = "v3.1.0"
)

type Saptune struct {
Version string
IsJSONSupported bool
executor utils.CommandExecutor
}

func getSaptuneVersion(commandExecutor utils.CommandExecutor) (string, error) {
log.Info("Requesting Saptune version...")
versionOutput, err := commandExecutor.Exec("rpm", "-q", "--qf", "%{VERSION}", "saptune")
if err != nil {
return "", errors.Wrap(err, ErrSaptuneVersionUnknown.Error())
}

log.Infof("saptune version output: %s", string(versionOutput))

return string(versionOutput), nil
}

func isSaptuneVersionSupported(version string) bool {
compareOutput := semver.Compare(MinimalSaptuneVersion, "v"+version)

return compareOutput != 1
}

func NewSaptune(commandExecutor utils.CommandExecutor) (Saptune, error) {
saptuneVersion, err := getSaptuneVersion(commandExecutor)
if err != nil {
return Saptune{}, err
}

saptune := Saptune{
Version: saptuneVersion,
executor: commandExecutor,
IsJSONSupported: isSaptuneVersionSupported(saptuneVersion),
}

return saptune, nil
}

func (s *Saptune) RunCommand(args ...string) ([]byte, error) {
log.Infof("Running saptune command: saptune %v", args)
output, err := s.executor.Exec("saptune", args...)
if err != nil {
log.Debugf(err.Error())
}
log.Debugf("saptune output: %s", string(output))
log.Infof("Saptune command executed")

return output, nil
}

func (s *Saptune) RunCommandJSON(args ...string) ([]byte, error) {
if !s.IsJSONSupported {
return nil, ErrUnsupportedSaptuneVer
}

prependedArgs := append([]string{"--format", "json"}, args...)
return s.RunCommand(prependedArgs...)
}
135 changes: 135 additions & 0 deletions internal/core/saptune/saptune_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package saptune

import (
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/suite"
"github.com/trento-project/agent/pkg/utils/mocks"
)

type SaptuneTestSuite struct {
suite.Suite
}

func TestSaptuneTestSuite(t *testing.T) {
suite.Run(t, new(SaptuneTestSuite))
}

func (suite *SaptuneTestSuite) TestNewSaptune() {
mockCommand := new(mocks.CommandExecutor)

mockCommand.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return(
[]byte("3.1.0"), nil,
)

saptuneRetriever, err := NewSaptune(mockCommand)

expectedSaptune := Saptune{
Version: "3.1.0",
IsJSONSupported: true,
executor: mockCommand,
}

suite.NoError(err)
suite.Equal(expectedSaptune, saptuneRetriever)
}

func (suite *SaptuneTestSuite) TestNewSaptuneUnsupportedSaptuneVer() {
mockCommand := new(mocks.CommandExecutor)

mockCommand.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return(
[]byte("3.0.0"), nil,
)

saptuneRetriever, err := NewSaptune(mockCommand)

expectedSaptune := Saptune{
Version: "3.0.0",
IsJSONSupported: false,
executor: mockCommand,
}

suite.NoError(err)
suite.Equal(expectedSaptune, saptuneRetriever)
}

func (suite *SaptuneTestSuite) TestNewSaptuneSaptuneVersionUnknownErr() {
mockCommand := new(mocks.CommandExecutor)

mockCommand.On("Exec", "rpm", "-q", "--qf", "%{VERSION}", "saptune").Return(
nil, errors.New("Error: exec: \"rpm\": executable file not found in $PATH"),
)

saptuneRetriever, err := NewSaptune(mockCommand)

expectedSaptune := Saptune{
Version: "",
IsJSONSupported: false,
}

suite.EqualError(err, ErrSaptuneVersionUnknown.Error()+": Error: exec: \"rpm\": executable file not found in $PATH")
suite.Equal(expectedSaptune, saptuneRetriever)
}

func (suite *SaptuneTestSuite) TestRunCommand() {
mockCommand := new(mocks.CommandExecutor)

saptuneRetriever := Saptune{
Version: "3.0.0",
IsJSONSupported: false,
executor: mockCommand,
}

saptuneOutput := []byte("some_output")

mockCommand.On("Exec", "saptune", "some_command").Return(
saptuneOutput, nil,
)

statusOutput, err := saptuneRetriever.RunCommand("some_command")

expectedOutput := []byte("some_output")

suite.NoError(err)
suite.Equal(expectedOutput, statusOutput)
}

func (suite *SaptuneTestSuite) TestRunCommandJSON() {
mockCommand := new(mocks.CommandExecutor)

saptuneRetriever := Saptune{
Version: "3.1.0",
IsJSONSupported: true,
executor: mockCommand,
}

saptuneOutput := []byte("{\"some_json_key\": \"some_value\"}")

mockCommand.On("Exec", "saptune", "--format", "json", "status").Return(
saptuneOutput, nil,
)

statusOutput, err := saptuneRetriever.RunCommandJSON("status")

expectedOutput := []byte("{\"some_json_key\": \"some_value\"}")

suite.NoError(err)
suite.Equal(expectedOutput, statusOutput)
}

func (suite *SaptuneTestSuite) TestRunCommandJSONNoJSONSupported() {
mockCommand := new(mocks.CommandExecutor)

saptuneRetriever := Saptune{
IsJSONSupported: false,
executor: mockCommand,
}

statusOutput, err := saptuneRetriever.RunCommandJSON("status")

expectedOutput := []byte(nil)

suite.EqualError(err, ErrUnsupportedSaptuneVer.Error())
suite.Equal(expectedOutput, statusOutput)
}
1 change: 1 addition & 0 deletions internal/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type DiscoveriesPeriodConfig struct {
Cloud time.Duration
Host time.Duration
Subscription time.Duration
Saptune time.Duration
}

type DiscoveriesConfig struct {
Expand Down
88 changes: 88 additions & 0 deletions internal/discovery/saptune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package discovery

import (
"encoding/json"
"time"

log "github.com/sirupsen/logrus"
"github.com/trento-project/agent/internal/core/saptune"
"github.com/trento-project/agent/internal/discovery/collector"
"github.com/trento-project/agent/pkg/utils"
)

const SaptuneDiscoveryID string = "saptune_discovery"
const SaptuneDiscoveryMinPeriod time.Duration = 1 * time.Second

type SaptuneDiscovery struct {
id string
collectorClient collector.Client
interval time.Duration
}

type SaptuneDiscoveryPayload struct {
PackageVersion string `json:"package_version"`
SaptuneInstalled bool `json:"saptune_installed"`
Status interface{} `json:"status"`
}

func NewSaptuneDiscovery(collectorClient collector.Client, config DiscoveriesConfig) Discovery {
return SaptuneDiscovery{
id: SaptuneDiscoveryID,
collectorClient: collectorClient,
interval: config.DiscoveriesPeriodsConfig.Saptune,
}
}

func (d SaptuneDiscovery) GetID() string {
return d.id
}

func (d SaptuneDiscovery) GetInterval() time.Duration {
return d.interval
}

func (d SaptuneDiscovery) Discover() (string, error) {
var saptunePayload SaptuneDiscoveryPayload

saptuneRetriever, err := saptune.NewSaptune(utils.Executor{})
switch {
case err != nil:
saptunePayload = SaptuneDiscoveryPayload{
PackageVersion: "",
SaptuneInstalled: false,
Status: nil,
}
case !saptuneRetriever.IsJSONSupported:
saptunePayload = SaptuneDiscoveryPayload{
PackageVersion: saptuneRetriever.Version,
SaptuneInstalled: true,
Status: nil,
}
default:
saptuneData, _ := saptuneRetriever.RunCommandJSON("status")
unmarshalled := make(map[string]interface{})
err = json.Unmarshal(saptuneData, &unmarshalled)
if err != nil {
log.Debugf("Error while unmarshalling saptune status: %s", err)
saptunePayload = SaptuneDiscoveryPayload{
PackageVersion: saptuneRetriever.Version,
SaptuneInstalled: true,
Status: nil,
}
} else {
saptunePayload = SaptuneDiscoveryPayload{
PackageVersion: saptuneRetriever.Version,
SaptuneInstalled: true,
Status: unmarshalled,
}
}
}

err = d.collectorClient.Publish(d.id, saptunePayload)
if err != nil {
log.Debugf("Error while sending saptune discovery to data collector: %s", err)
return "", err
}

return "Saptune data discovery completed", nil
}
1 change: 1 addition & 0 deletions test/fixtures/config/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ cluster-discovery-period: 10s
host-discovery-period: 10s
sapsystem-discovery-period: 10s
subscription-discovery-period: 900s
saptune-discovery-period: 10s
server-url: http://serverurl
api-key: some-api-key
force-agent-id: some-agent-id
Expand Down

0 comments on commit c802521

Please sign in to comment.