diff --git a/cmd/config.go b/cmd/config.go index c444d8b9..ce03839b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -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 { @@ -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{ diff --git a/cmd/config_test.go b/cmd/config_test.go index 9a864208..f983bf38 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -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", @@ -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", @@ -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") diff --git a/cmd/start.go b/cmd/start.go index 46c39dc4..a6be0c38 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -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", @@ -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(). diff --git a/internal/agent/agent.go b/internal/agent/agent.go index d43bd847..e2157879 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -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{ diff --git a/internal/core/saptune/saptune.go b/internal/core/saptune/saptune.go new file mode 100644 index 00000000..9e552ea8 --- /dev/null +++ b/internal/core/saptune/saptune.go @@ -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...) +} diff --git a/internal/core/saptune/saptune_test.go b/internal/core/saptune/saptune_test.go new file mode 100644 index 00000000..08c31fe2 --- /dev/null +++ b/internal/core/saptune/saptune_test.go @@ -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) +} diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go index 1a1f7a02..cdfeed9f 100644 --- a/internal/discovery/discovery.go +++ b/internal/discovery/discovery.go @@ -12,6 +12,7 @@ type DiscoveriesPeriodConfig struct { Cloud time.Duration Host time.Duration Subscription time.Duration + Saptune time.Duration } type DiscoveriesConfig struct { diff --git a/internal/discovery/saptune.go b/internal/discovery/saptune.go new file mode 100644 index 00000000..bae5ab6d --- /dev/null +++ b/internal/discovery/saptune.go @@ -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 +} diff --git a/test/fixtures/config/agent.yaml b/test/fixtures/config/agent.yaml index 5fccfad8..5e35e0ae 100644 --- a/test/fixtures/config/agent.yaml +++ b/test/fixtures/config/agent.yaml @@ -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