Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add saptune discovery #253

Merged
merged 9 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
66 changes: 66 additions & 0 deletions internal/core/saptune/saptune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 {
arbulu89 marked this conversation as resolved.
Show resolved Hide resolved
Version string
IsJSONSupported bool
executor utils.CommandExecutor
}

func getSaptuneVersion(commandExecutor utils.CommandExecutor) (string, error) {
log.Info("Requesting Saptune version...")
rtorrero marked this conversation as resolved.
Show resolved Hide resolved
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{Version: ""}, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could simply return Saptune{}

}

return Saptune{Version: saptuneVersion, executor: commandExecutor, IsJSONSupported: isSaptuneVersionSupported(saptuneVersion)}, nil

Check failure on line 49 in internal/core/saptune/saptune.go

View workflow job for this annotation

GitHub Actions / static-analysis

line is 132 characters (lll)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better to store the result in a variable, just to improve readability

}

func (s *Saptune) RunCommand(args ...string) ([]byte, error) {
if !s.IsJSONSupported {
arbulu89 marked this conversation as resolved.
Show resolved Hide resolved
return nil, ErrUnsupportedSaptuneVer
}

log.Infof("Running saptune command: saptune %v", args)
Copy link
Contributor

@arbulu89 arbulu89 Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could reuse the main RunCommand once we have the new args.
Not a big deal

return s.RunCommand(prependedArgs)

output, err := s.executor.Exec("saptune", args...)
if err != nil {
arbulu89 marked this conversation as resolved.
Show resolved Hide resolved
arbulu89 marked this conversation as resolved.
Show resolved Hide resolved
log.Debugf(err.Error())
}
log.Debugf("saptune output: %s", string(output))
log.Infof("Saptune command executed")

return output, nil
}
112 changes: 112 additions & 0 deletions internal/core/saptune/saptune_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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() {
arbulu89 marked this conversation as resolved.
Show resolved Hide resolved
mockCommand := new(mocks.CommandExecutor)

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

saptuneRetriever, err := NewSaptune(mockCommand)

expectedDetails := Saptune{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could name this expectedSaptune. Wdyt?

Version: "3.1.0",
IsJSONSupported: true,
executor: mockCommand,
}

suite.NoError(err)
suite.Equal(expectedDetails, 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)

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

suite.NoError(err)
suite.Equal(expectedDetails, 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)

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

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

func (suite *SaptuneTestSuite) TestRunCommand() {
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.RunCommand("--format", "json", "status")

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

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

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

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

statusOutput, err := saptuneRetriever.RunCommand("--format", "json", "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
80 changes: 80 additions & 0 deletions internal/discovery/saptune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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.RunCommand("--format", "json", "status")
unmarshalled := make(map[string]interface{})
json.Unmarshal(saptuneData, &unmarshalled)

Check failure on line 64 in internal/discovery/saptune.go

View workflow job for this annotation

GitHub Actions / static-analysis

Error return value of `json.Unmarshal` is not checked (errcheck)

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
Loading