internal/onetime/configure/configure.go (490 lines of code) (raw):
/*
Copyright 2023 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package configure provides the leverage of enabling or disabling features by modifying configuration.json.
package configure
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"runtime"
"strings"
"flag"
wpb "google.golang.org/protobuf/types/known/wrapperspb"
"github.com/google/safetext/shsprintf"
"google.golang.org/protobuf/encoding/protojson"
"github.com/google/subcommands"
"github.com/GoogleCloudPlatform/sapagent/internal/configuration"
"github.com/GoogleCloudPlatform/sapagent/internal/onetime"
cpb "github.com/GoogleCloudPlatform/sapagent/protos/configuration"
"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/commandlineexecutor"
"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log"
)
// Configure has args for backint subcommands.
type Configure struct {
Feature string `json:"feature"`
LogLevel string `json:"loglevel"`
Setting string `json:"setting"`
Path string `json:"path"`
SkipMetrics string `json:"process_metrics_to_skip"`
ValidationMetricsFrequency int64 `json:"workload_evaluation_metrics_frequency,string"`
DbFrequency int64 `json:"workload_evaluation_db_metrics_frequency,string"`
FastMetricsFrequency int64 `json:"process_metrics_frequency,string"`
SlowMetricsFrequency int64 `json:"slow_process_metrics_frequency,string"`
AgentMetricsFrequency int64 `json:"agent_metrics_frequency,string"`
AgentHealthFrequency int64 `json:"agent_health_frequency,string"`
HeartbeatFrequency int64 `json:"heartbeat_frequency,string"`
SampleIntervalSec int64 `json:"sample_interval_sec,string"`
QueryTimeoutSec int64 `json:"query_timeout_sec,string"`
Help bool `json:"help,string"`
Enable bool `json:"enable,string"`
Disable bool `json:"disable,string"`
Showall bool `json:"showall,string"`
Add bool `json:"add,string"`
Remove bool `json:"remove,string"`
LogPath string `json:"log-path"`
usageFunc func()
oteLogger *onetime.OTELogger
}
const (
hostMetrics = "host_metrics"
processMetrics = "process_metrics"
hanaMonitoring = "hana_monitoring"
sapDiscovery = "sap_discovery"
agentMetrics = "agent_metrics"
workloadValidation = "workload_evaluation"
workloadDiscovery = "workload_discovery"
)
var (
loglevels = map[string]cpb.Configuration_LogLevel{
"debug": cpb.Configuration_DEBUG,
"info": cpb.Configuration_INFO,
"warn": cpb.Configuration_WARNING,
"error": cpb.Configuration_ERROR,
}
spaces = regexp.MustCompile(`\s+`)
)
// Name implements the subcommand interface for features.
func (*Configure) Name() string {
return "configure"
}
// Synopsis implements the subcommand interface for features.
func (*Configure) Synopsis() string {
return `enable/disable collection of following: host metrics, process metrics, hana monitoring, sap discovery, agent metrics, workload validation metrics`
}
// Usage implements the subcommand interface for features.
func (*Configure) Usage() string {
return `Usage:
configure [-feature=<host_metrics|process_metrics|hana_monitoring|sap_discovery|agent_metrics|workload_evaluation|workload_discovery> | -setting=<bare_metal|log_to_cloud>]
[-enable|-disable] [-showall] [-h]
[process_metrics_frequency=<int>] [slow_process_metrics_frequency=<int>]
[process_metrics_to_skip=<"comma-separated-metrics">] [-add|-remove]
[workload_evaluation_metrics_frequency=<int>] [workload_evaluation_db_metrics_frequency=<int>]
[-agent_metrics_frequency=<int>] [agent_health_frequency=<int>]
[heartbeat_frequency=<int>] [sample_interval_sec=<int>] [query_timeout_sec=<int>] [-log-path=<log-path>]
`
}
// SetFlags implements the subcommand interface for features.
func (c *Configure) SetFlags(fs *flag.FlagSet) {
fs.StringVar(&c.Feature, "feature", "", "The requested feature. Valid values are: host_metrics, process_metrics, hana_monitoring, sap_discovery, agent_metrics, workload_evaluation, workload_discovery")
fs.StringVar(&c.Feature, "f", "", "The requested feature. Valid values are: host_metrics, process_metrics, hana_monitoring, sap_discovery, agent_metrics, workload_evaluation, workload_discovery")
fs.StringVar(&c.LogLevel, "loglevel", "", "Sets the logging level for the agent configuration file")
fs.StringVar(&c.Setting, "setting", "", "The requested setting. Valid values are: bare_metal, log_to_cloud")
fs.StringVar(&c.SkipMetrics, "process_metrics_to_skip", "", "Add or remove the list of metrics to skip during process metrics collection")
fs.Int64Var(&c.ValidationMetricsFrequency, "workload_evaluation_metrics_frequency", 0, "Sets the frequency of workload validation metrics collection. Default value is 300(s)")
fs.Int64Var(&c.DbFrequency, "workload_evaluation_db_metrics_frequency", 0, "Sets the database frequency of workload validation metrics collection. Default value is 3600(s)")
fs.Int64Var(&c.FastMetricsFrequency, "process_metrics_frequency", 0, "Sets the frequency of fast moving process metrics collection. Default value is 5(s)")
fs.Int64Var(&c.SlowMetricsFrequency, "slow_process_metrics_frequency", 0, "Sets the frequency of slow moving process metrics collection. Default value is 30(s)")
fs.Int64Var(&c.AgentMetricsFrequency, "agent_metrics_frequency", 0, "Sets the agent metrics frequency. Default value is 60(s)")
fs.Int64Var(&c.AgentHealthFrequency, "agent_health_frequency", 0, "Sets the agent health frequency. Default value is 60(s)")
fs.Int64Var(&c.HeartbeatFrequency, "heartbeat_frequency", 0, "Sets the heartbeat frequency. Default value is 60(s)")
fs.Int64Var(&c.SampleIntervalSec, "sample_interval_sec", 0, "Sets the sample interval sec for HANA Monitoring. Default value is 300(s)")
fs.Int64Var(&c.QueryTimeoutSec, "query_timeout_sec", 0, "Sets the query timeout for HANA Monitoring. Default value is 300(s)")
fs.BoolVar(&c.Help, "help", false, "Display help")
fs.BoolVar(&c.Help, "h", false, "Display help")
fs.StringVar(&c.LogPath, "log-path", "", "The log path to write the log file (optional), default value is /var/log/google-cloud-sap-agent/configure.log")
fs.BoolVar(&c.Showall, "showall", false, "Display the status of all features")
fs.BoolVar(&c.Enable, "enable", false, "Enable the requested feature/setting")
fs.BoolVar(&c.Disable, "disable", false, "Disable the requested feature/setting")
fs.BoolVar(&c.Add, "add", false, "Add the requested list of process metrics to skip. process-metrics-to-skip should not be empty")
fs.BoolVar(&c.Remove, "remove", false, "Remove the requested list of process metrics to skip. process-metrics-to-skip should not be empty")
}
// Execute implements the subcommand interface for feature.
func (c *Configure) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
_, cp, exitStatus, completed := onetime.Init(ctx, onetime.InitOptions{
Name: c.Name(),
Help: c.Help,
Fs: fs,
LogLevel: c.LogLevel,
LogPath: c.LogPath,
}, args...)
if !completed {
return exitStatus
}
c.usageFunc = fs.Usage
_, res := c.Run(ctx, onetime.CreateRunOptions(cp, false), args...)
return res
}
// Run executes the command and returns a message string in addition to the status.
func (c *Configure) Run(ctx context.Context, runOpts *onetime.RunOptions, args ...any) (string, subcommands.ExitStatus) {
c.oteLogger = onetime.CreateOTELogger(runOpts.DaemonMode)
if c.Path == "" {
c.Path = configuration.LinuxConfigPath
if runtime.GOOS == "windows" {
c.Path = configuration.WindowsConfigPath
}
}
if c.Showall {
return c.showFeatures(ctx)
}
newCfg, res := c.modifyConfig(ctx, os.ReadFile)
if res == subcommands.ExitSuccess {
c.oteLogger.LogMessageToConsole("Successfully modified configuration.json.")
}
return newCfg, res
}
// setStatus returns a map of feature name and its status.
func setStatus(ctx context.Context, config *cpb.Configuration) map[string]bool {
featureStatus := map[string]bool{
hostMetrics: true,
hanaMonitoring: false,
agentMetrics: false,
workloadValidation: true,
processMetrics: false,
sapDiscovery: false,
workloadDiscovery: false,
}
if hm := config.GetProvideSapHostAgentMetrics(); hm != nil {
featureStatus[hostMetrics] = hm.GetValue()
}
if hmc := config.GetHanaMonitoringConfiguration(); hmc != nil {
featureStatus[hanaMonitoring] = hmc.GetEnabled()
}
if cc := config.GetCollectionConfiguration(); cc != nil {
featureStatus[agentMetrics] = cc.GetCollectAgentMetrics()
if wlm := cc.GetCollectWorkloadValidationMetrics(); wlm != nil {
featureStatus[workloadValidation] = wlm.GetValue()
}
featureStatus[processMetrics] = cc.GetCollectProcessMetrics()
}
if config.GetDiscoveryConfiguration().GetEnableDiscovery().GetValue() {
featureStatus[sapDiscovery] = true
}
if config.GetDiscoveryConfiguration().GetEnableWorkloadDiscovery().GetValue() {
featureStatus[workloadDiscovery] = true
}
log.CtxLogger(ctx).Info("Feature status: ", featureStatus)
return featureStatus
}
// showFeatures displays the status of all features.
func (c *Configure) showFeatures(ctx context.Context) (string, subcommands.ExitStatus) {
config, _ := configuration.Read(c.Path, os.ReadFile)
if config == nil {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Unable to read configuration.json")
return "Unable to read configuration.json", subcommands.ExitFailure
}
featureStatus := setStatus(ctx, config)
var enabled, disabled []string
var output string
for feature, status := range featureStatus {
if status {
enabled = append(enabled, feature)
} else {
disabled = append(disabled, feature)
}
}
isWindows := runtime.GOOS == "windows"
for _, feature := range enabled {
out, err := showStatus(ctx, feature, featureStatus[feature], isWindows)
if err != nil {
return err.Error(), subcommands.ExitFailure
}
output += out
}
for _, feature := range disabled {
out, err := showStatus(ctx, feature, featureStatus[feature], isWindows)
if err != nil {
return err.Error(), subcommands.ExitFailure
}
output += out
}
c.oteLogger.LogMessageToConsole(output)
return output, subcommands.ExitSuccess
}
// modifyConfig takes user input and enables/disables features in configuration.json.
func (c *Configure) modifyConfig(ctx context.Context, read configuration.ReadConfigFile) (string, subcommands.ExitStatus) {
log.Logger.Infow("Beginning execution of features command")
config, _ := configuration.Read(c.Path, read)
if config == nil {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Unable to read configuration.json")
return "Unable to read configuration.json", subcommands.ExitFailure
}
log.CtxLogger(ctx).Infow("Config before any changes", "config", config)
isCmdValid := false
if len(c.LogLevel) > 0 {
if _, ok := loglevels[c.LogLevel]; !ok {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Invalid log level. Please use [debug, info, warn, error]")
return "Invalid log level. Please use [debug, info, warn, error]", subcommands.ExitUsageError
}
isCmdValid = true
config.LogLevel = loglevels[c.LogLevel]
}
if len(c.Feature) > 0 {
if res := c.modifyFeature(ctx, config); res != subcommands.ExitSuccess {
return "Failed to modify feature", res
}
isCmdValid = true
} else if len(c.Setting) > 0 {
if !c.Enable && !c.Disable {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Please choose to enable or disable the given feature/setting\n")
return "Please choose to enable or disable the given feature/setting", subcommands.ExitUsageError
}
isEnabled := c.Enable
switch c.Setting {
case "bare_metal":
config.BareMetal = isEnabled
case "log_to_cloud":
config.LogToCloud = &wpb.BoolValue{Value: isEnabled}
default:
c.oteLogger.LogMessageToFileAndConsole(ctx, "Unsupported setting")
return "Unsupported setting", subcommands.ExitUsageError
}
isCmdValid = true
}
if !isCmdValid {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Insufficient flags. Please check usage:\n")
// Checking for nil for testing purposes
if c.usageFunc != nil {
c.usageFunc()
}
return "Insufficient flags. Please check usage", subcommands.ExitUsageError
}
newCfg, err := c.writeFile(ctx, config, c.Path)
if err != nil {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Unable to write configuration.json", err)
return newCfg, subcommands.ExitUsageError
}
return newCfg, subcommands.ExitSuccess
}
// modifyFeature takes user input and modifies fields related to a particular feature, which could simply
// be enabling or disabling the feature or updating frequency values, for instance, of the feature.
func (c *Configure) modifyFeature(ctx context.Context, config *cpb.Configuration) subcommands.ExitStatus {
// var isEnabled any
var isEnabled *bool
if c.Enable || c.Disable {
isEnabled = &c.Enable
}
isCmdValid := false
switch c.Feature {
case hostMetrics:
if isEnabled != nil {
isCmdValid = true
config.ProvideSapHostAgentMetrics = &wpb.BoolValue{Value: *isEnabled}
}
case processMetrics:
if isEnabled != nil {
isCmdValid = true
checkCollectionConfig(config).CollectProcessMetrics = *isEnabled
}
if c.FastMetricsFrequency != 0 {
if c.FastMetricsFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).ProcessMetricsFrequency = c.FastMetricsFrequency
}
if c.SlowMetricsFrequency != 0 {
if c.SlowMetricsFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("slow-metrics-frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).SlowProcessMetricsFrequency = c.SlowMetricsFrequency
}
if len(c.SkipMetrics) > 0 {
log.CtxLogger(ctx).Info("Skip Metrics: ", c.SkipMetrics)
if !c.Add && !c.Remove {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Please choose to add or remove given list of process metrics.")
return subcommands.ExitUsageError
}
isCmdValid = true
if res := c.modifyProcessMetricsToSkip(ctx, config); res != subcommands.ExitSuccess {
return res
}
}
case hanaMonitoring:
if isEnabled != nil {
isCmdValid = true
if hmc := config.GetHanaMonitoringConfiguration(); hmc != nil {
hmc.Enabled = *isEnabled
} else {
config.HanaMonitoringConfiguration = &cpb.HANAMonitoringConfiguration{Enabled: *isEnabled}
}
}
if c.SampleIntervalSec != 0 {
if c.SampleIntervalSec < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("sample-interval-sec must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
config.HanaMonitoringConfiguration.SampleIntervalSec = c.SampleIntervalSec
}
if c.QueryTimeoutSec != 0 {
if c.QueryTimeoutSec < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("query-timeout-sec must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
config.HanaMonitoringConfiguration.QueryTimeoutSec = c.QueryTimeoutSec
}
case sapDiscovery:
if isEnabled != nil {
isCmdValid = true
checkDiscoveryConfig(config).EnableDiscovery = &wpb.BoolValue{Value: *isEnabled}
}
case agentMetrics:
if isEnabled != nil {
isCmdValid = true
checkCollectionConfig(config).CollectAgentMetrics = *isEnabled
}
if c.AgentMetricsFrequency != 0 {
if c.AgentMetricsFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).AgentMetricsFrequency = c.AgentMetricsFrequency
}
if c.AgentHealthFrequency != 0 {
if c.AgentHealthFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("agent-health-frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).AgentHealthFrequency = c.AgentHealthFrequency
}
if c.HeartbeatFrequency != 0 {
if c.HeartbeatFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("heartbeat-frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).HeartbeatFrequency = c.HeartbeatFrequency
}
case workloadValidation:
if isEnabled != nil {
isCmdValid = true
checkCollectionConfig(config).CollectWorkloadValidationMetrics = &wpb.BoolValue{Value: *isEnabled}
}
if c.ValidationMetricsFrequency != 0 {
if c.ValidationMetricsFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).WorkloadValidationMetricsFrequency = c.ValidationMetricsFrequency
}
if c.DbFrequency != 0 {
if c.DbFrequency < 0 {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Inappropriate flag values:", fmt.Errorf("db-frequency must be non-negative"))
return subcommands.ExitUsageError
}
isCmdValid = true
checkCollectionConfig(config).WorkloadValidationDbMetricsFrequency = c.DbFrequency
}
case workloadDiscovery:
if isEnabled != nil {
isCmdValid = true
checkDiscoveryConfig(config).EnableWorkloadDiscovery = &wpb.BoolValue{Value: *isEnabled}
}
default:
c.oteLogger.LogMessageToFileAndConsole(ctx, "Unsupported Metric")
return subcommands.ExitUsageError
}
if !isCmdValid {
c.oteLogger.LogMessageToFileAndConsole(ctx, "Insufficient flags. Please check usage:\n")
// Checking for nil for testing purposes
if c.usageFunc != nil {
c.usageFunc()
}
return subcommands.ExitUsageError
}
return subcommands.ExitSuccess
}
// modifyProcessMetricsToSkip modifies 'process_metrics_to_skip' field of the configuration file.
func (c *Configure) modifyProcessMetricsToSkip(ctx context.Context, config *cpb.Configuration) subcommands.ExitStatus {
str := spaces.ReplaceAllString(c.SkipMetrics, "")
metricsSkipList := strings.Split(str, ",")
currList := checkCollectionConfig(config).GetProcessMetricsToSkip()
if c.Add {
metricsPresent := map[string]bool{}
for _, metric := range currList {
metricsPresent[metric] = true
}
for _, metric := range metricsSkipList {
metricsPresent[metric] = true
}
newList := []string{}
for metric := range metricsPresent {
newList = append(newList, metric)
}
checkCollectionConfig(config).ProcessMetricsToSkip = newList
} else if c.Remove {
metricsPresent := map[string]bool{}
for _, metric := range currList {
metricsPresent[metric] = true
}
for _, metric := range metricsSkipList {
metricsPresent[metric] = false
}
newList := []string{}
for metric, keep := range metricsPresent {
if keep {
newList = append(newList, metric)
}
}
checkCollectionConfig(config).ProcessMetricsToSkip = newList
} else {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Error: ", fmt.Errorf("no -add or -remove flag with -process-metrics-to-skip"))
return subcommands.ExitUsageError
}
return subcommands.ExitSuccess
}
// writeFile writes the configuration to the given path.
func (c *Configure) writeFile(ctx context.Context, config *cpb.Configuration, path string) (string, error) {
file, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(config)
if err != nil {
log.CtxLogger(ctx).Errorw("Unable to marshal configuration.json")
return "Unable to marshal configuration.json", err
}
var fileBuf bytes.Buffer
json.Indent(&fileBuf, file, "", " ")
log.CtxLogger(ctx).Info("Config file data we're about to write: ", fileBuf.String())
err = os.WriteFile(path, fileBuf.Bytes(), 0644)
if err != nil {
c.oteLogger.LogErrorToFileAndConsole(ctx, "Unable to write configuration.json", err)
return "Unable to write configuration.json", err
}
return fileBuf.String(), nil
}
// checkCollectionConfig returns the collection configuration from the configuration file.
func checkCollectionConfig(config *cpb.Configuration) *cpb.CollectionConfiguration {
if cc := config.GetCollectionConfiguration(); cc != nil {
return cc
}
return &cpb.CollectionConfiguration{}
}
// checkDiscoveryConfig returns the discovery configuration from the configuration file.
func checkDiscoveryConfig(config *cpb.Configuration) *cpb.DiscoveryConfiguration {
if dc := config.GetDiscoveryConfiguration(); dc != nil {
return dc
}
return &cpb.DiscoveryConfiguration{}
}
func showStatus(ctx context.Context, feature string, enabled bool, isWindows bool) (string, error) {
var color, status string
if enabled {
color = "\\033[0;32m"
status = "ENABLED"
} else {
color = "\\033[0;31m"
status = "DISABLED"
}
NC := "\\033[0m" // No color
executable := "echo"
args := fmt.Sprintf("-e %s %s[%s]%s", feature, color, status, NC)
if isWindows {
executable = "cmd"
args, _ = shsprintf.Sprintf("/C echo %s [%s]", feature, status)
}
result := commandlineexecutor.ExecuteCommand(ctx, commandlineexecutor.Params{
Executable: executable,
ArgsToSplit: args,
})
if result.ExitCode != 0 {
log.CtxLogger(ctx).Errorw("failed displaying feature status", "feature:", feature, "errorCode:", result.ExitCode, "error:", result.StdErr)
return "", result.Error
}
return result.StdOut, nil
}