Files
scrutiny/collector/pkg/collector/metrics.go
T
Jason Kulatunga a53397210c adding mechanism to override the smartctl commands used by scrutiny for device scanning, device identification and smart data retrieval.
adding tests for command overrides.

rename GetScanOverrides() to GetDeviceOverrides()

fixes #255
2022-05-28 15:32:44 -07:00

162 lines
5.0 KiB
Go

package collector
import (
"bytes"
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/sirupsen/logrus"
"net/url"
"os"
"os/exec"
"strings"
)
type MetricsCollector struct {
config config.Interface
BaseCollector
apiEndpoint *url.URL
shell shell.Interface
}
func CreateMetricsCollector(appConfig config.Interface, logger *logrus.Entry, apiEndpoint string) (MetricsCollector, error) {
apiEndpointUrl, err := url.Parse(apiEndpoint)
if err != nil {
return MetricsCollector{}, err
}
sc := MetricsCollector{
config: appConfig,
apiEndpoint: apiEndpointUrl,
BaseCollector: BaseCollector{
logger: logger,
},
shell: shell.Create(),
}
return sc, nil
}
func (mc *MetricsCollector) Run() error {
err := mc.Validate()
if err != nil {
return err
}
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint, _ = apiEndpoint.Parse("api/devices/register") //this acts like filepath.Join()
deviceRespWrapper := new(models.DeviceWrapper)
deviceDetector := detect.Detect{
Logger: mc.logger,
Config: mc.config,
}
detectedStorageDevices, err := deviceDetector.Start()
if err != nil {
return err
}
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
jsonObj, _ := json.Marshal(detectedStorageDevices)
mc.logger.Debugf("Detected devices: %v", string(jsonObj))
err = mc.postJson(apiEndpoint.String(), models.DeviceWrapper{
Data: detectedStorageDevices,
}, &deviceRespWrapper)
if err != nil {
return err
}
if !deviceRespWrapper.Success {
mc.logger.Errorln("An error occurred while retrieving filtered devices")
mc.logger.Debugln(deviceRespWrapper)
return errors.ApiServerCommunicationError("An error occurred while retrieving filtered devices")
} else {
mc.logger.Debugln(deviceRespWrapper)
//var wg sync.WaitGroup
for _, device := range deviceRespWrapper.Data {
// execute collection in parallel go-routines
//wg.Add(1)
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
// TODO: we may need to sleep for between each call to smartctl -a
//time.Sleep(30 * time.Millisecond)
}
//mc.logger.Infoln("Main: Waiting for workers to finish")
//wg.Wait()
mc.logger.Infoln("Main: Completed")
}
return nil
}
func (mc *MetricsCollector) Validate() error {
mc.logger.Infoln("Verifying required tools")
_, lookErr := exec.LookPath("smartctl")
if lookErr != nil {
return errors.DependencyMissingError("smartctl is missing")
}
return nil
}
//func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
//defer wg.Done()
if len(deviceWWN) == 0 {
mc.logger.Errorf("no device WWN detected for %s. Skipping collection for this device (no data association possible).\n", deviceName)
return
}
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
fullDeviceName := fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)
args := strings.Split(mc.config.GetCommandMetricsSmartArgs(fullDeviceName), " ")
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
args = append(args, "--device", deviceType)
}
args = append(args, fullDeviceName)
result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ())
resultBytes := []byte(result)
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
// smartctl command exited with an error, we should still push the data to the API server
mc.logger.Errorf("smartctl returned an error code (%d) while processing %s\n", exitError.ExitCode(), deviceName)
mc.LogSmartctlExitCode(exitError.ExitCode())
mc.Publish(deviceWWN, resultBytes)
} else {
mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName)
mc.logger.Errorf("ERROR MESSAGE: %v", err)
mc.logger.Errorf("IGNORING RESULT: %v", result)
}
return
} else {
//successful run, pass the results directly to webapp backend for parsing and processing.
mc.Publish(deviceWWN, resultBytes)
}
}
func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error {
mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN)
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN)))
resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
if err != nil {
mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", deviceWWN, err)
return err
}
defer resp.Body.Close()
return nil
}