init
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var goos string
|
||||
var goarch string
|
||||
|
||||
func main() {
|
||||
|
||||
cli.CommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||
OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
`
|
||||
|
||||
app := &cli.App{
|
||||
Name: "scrutiny-collector-metrics",
|
||||
Usage: "smartctl data collector for scrutiny",
|
||||
Version: version.VERSION,
|
||||
Compiled: time.Now(),
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Jason Kulatunga",
|
||||
Email: "jason@thesparktree.com",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
|
||||
collectorMetrics := "AnalogJ/scrutiny/metrics"
|
||||
|
||||
var versionInfo string
|
||||
if len(goos) > 0 && len(goarch) > 0 {
|
||||
versionInfo = fmt.Sprintf("%s.%s-%s", goos, goarch, version.VERSION)
|
||||
} else {
|
||||
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
|
||||
}
|
||||
|
||||
subtitle := collectorMetrics + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorMetrics))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "Run the scrutiny smartctl metrics collector",
|
||||
Action: func(c *cli.Context) error {
|
||||
|
||||
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "metrics",
|
||||
})
|
||||
|
||||
if c.Bool("debug") {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
metricCollector, err := collector.CreateMetricsCollector(
|
||||
collectorLogger,
|
||||
c.String("api-endpoint"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return metricCollector.Run()
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "api-endpoint",
|
||||
Usage: "The api server endpoint",
|
||||
Value: "http://localhost:8080",
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug logging",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var goos string
|
||||
var goarch string
|
||||
|
||||
func main() {
|
||||
|
||||
cli.CommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||
OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
`
|
||||
|
||||
app := &cli.App{
|
||||
Name: "scrutiny-collector-selftest",
|
||||
Usage: "smartctl self-test data collector for scrutiny",
|
||||
Version: version.VERSION,
|
||||
Compiled: time.Now(),
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Jason Kulatunga",
|
||||
Email: "jason@thesparktree.com",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
|
||||
collectorSelfTest := "AnalogJ/scrutiny/selftest"
|
||||
|
||||
var versionInfo string
|
||||
if len(goos) > 0 && len(goarch) > 0 {
|
||||
versionInfo = fmt.Sprintf("%s.%s-%s", goos, goarch, version.VERSION)
|
||||
} else {
|
||||
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
|
||||
}
|
||||
|
||||
subtitle := collectorSelfTest + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorSelfTest))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "Run the scrutiny self-test data collector",
|
||||
Action: func(c *cli.Context) error {
|
||||
|
||||
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "selftest",
|
||||
})
|
||||
|
||||
if c.Bool("debug") {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
stCollector, err := collector.CreateSelfTestCollector(
|
||||
collectorLogger,
|
||||
c.String("api-endpoint"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stCollector.Run()
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "api-endpoint",
|
||||
Usage: "The api server endpoint",
|
||||
Value: "http://localhost:8080",
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug logging",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
)
|
||||
|
||||
type BaseCollector struct{}
|
||||
|
||||
func (c *BaseCollector) getJson(url string, target interface{}) error {
|
||||
|
||||
r, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (c *BaseCollector) execCmd(cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) {
|
||||
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
var stdBuffer bytes.Buffer
|
||||
mw := io.MultiWriter(os.Stdout, &stdBuffer)
|
||||
|
||||
cmd.Stdout = mw
|
||||
cmd.Stderr = mw
|
||||
|
||||
if environ != nil {
|
||||
cmd.Env = environ
|
||||
}
|
||||
if workingDir != "" && path.IsAbs(workingDir) {
|
||||
cmd.Dir = workingDir
|
||||
} else if workingDir != "" {
|
||||
return "", errors.New("Working Directory must be an absolute path")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return stdBuffer.String(), err
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
type MetricsCollector struct {
|
||||
BaseCollector
|
||||
|
||||
apiEndpoint *url.URL
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func CreateMetricsCollector(logger *logrus.Entry, apiEndpoint string) (MetricsCollector, error) {
|
||||
apiEndpointUrl, err := url.Parse(apiEndpoint)
|
||||
if err != nil {
|
||||
return MetricsCollector{}, err
|
||||
}
|
||||
|
||||
sc := MetricsCollector{
|
||||
apiEndpoint: apiEndpointUrl,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Run() error {
|
||||
err := mc.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
|
||||
apiEndpoint.Path = "/api/devices"
|
||||
|
||||
deviceRespWrapper := new(models.DeviceRespWrapper)
|
||||
|
||||
fmt.Println("Getting devices")
|
||||
err = mc.getJson(apiEndpoint.String(), &deviceRespWrapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !deviceRespWrapper.Success {
|
||||
//TODO print error payload
|
||||
fmt.Println("An error occurred while retrieving devices")
|
||||
} else {
|
||||
fmt.Println(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)
|
||||
}
|
||||
|
||||
fmt.Println("Main: Waiting for workers to finish")
|
||||
wg.Wait()
|
||||
fmt.Println("Main: Completed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Validate() error {
|
||||
fmt.Println("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) {
|
||||
defer wg.Done()
|
||||
fmt.Printf("Collecting smartctl results for %s\n", deviceName)
|
||||
|
||||
result, err := mc.execCmd("smartctl", []string{"-a", "-j", fmt.Sprintf("/dev/%s", deviceName)}, "", nil)
|
||||
resultBytes := []byte(result)
|
||||
if err != nil {
|
||||
fmt.Printf("error while retrieving data from smartctl %s\n", deviceName)
|
||||
fmt.Printf("ERROR MESSAGE: %v", err)
|
||||
fmt.Printf("RESULT: %v", result)
|
||||
// TODO: error while retrieving data from smartctl.
|
||||
// TODO: we should pass this data on to scrutiny API for recording.
|
||||
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 {
|
||||
fmt.Printf("Publishing smartctl results for %s\n", deviceWWN)
|
||||
|
||||
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
|
||||
apiEndpoint.Path = fmt.Sprintf("/api/device/%s/smart", strings.ToLower(deviceWWN))
|
||||
|
||||
resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type SelfTestCollector struct {
|
||||
BaseCollector
|
||||
|
||||
apiEndpoint *url.URL
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func CreateSelfTestCollector(logger *logrus.Entry, apiEndpoint string) (SelfTestCollector, error) {
|
||||
apiEndpointUrl, err := url.Parse(apiEndpoint)
|
||||
if err != nil {
|
||||
return SelfTestCollector{}, err
|
||||
}
|
||||
|
||||
stc := SelfTestCollector{
|
||||
apiEndpoint: apiEndpointUrl,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
return stc, nil
|
||||
}
|
||||
|
||||
func (sc *SelfTestCollector) Run() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Raised when config file is missing
|
||||
type ConfigFileMissingError string
|
||||
|
||||
func (str ConfigFileMissingError) Error() string {
|
||||
return fmt.Sprintf("ConfigFileMissingError: %q", string(str))
|
||||
}
|
||||
|
||||
// Raised when the config file doesnt match schema
|
||||
type ConfigValidationError string
|
||||
|
||||
func (str ConfigValidationError) Error() string {
|
||||
return fmt.Sprintf("ConfigValidationError: %q", string(str))
|
||||
}
|
||||
|
||||
// Raised when a dependency (like smartd or ssh-agent) is missing
|
||||
type DependencyMissingError string
|
||||
|
||||
func (str DependencyMissingError) Error() string {
|
||||
return fmt.Sprintf("DependencyMissingError: %q", string(str))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//func TestCheckErr_WithoutError(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// //assert
|
||||
// require.NotPanics(t, func() {
|
||||
// errors.CheckErr(nil)
|
||||
// })
|
||||
//}
|
||||
|
||||
//func TestCheckErr_Error(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// //assert
|
||||
// require.Panics(t, func() {
|
||||
// errors.CheckErr(stderrors.New("This is an error"))
|
||||
// })
|
||||
//}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//assert
|
||||
require.Implements(t, (*error)(nil), errors.ConfigFileMissingError("test"), "should implement the error interface")
|
||||
require.Implements(t, (*error)(nil), errors.ConfigValidationError("test"), "should implement the error interface")
|
||||
require.Implements(t, (*error)(nil), errors.DependencyMissingError("test"), "should implement the error interface")
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
type Device struct {
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
InterfaceSpeed string `json:"interface_speed"`
|
||||
SerialNumber string `json:"serial_name"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
}
|
||||
|
||||
type DeviceRespWrapper struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []error `json:"errors"`
|
||||
Data []Device `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
//type SelfTest struct {
|
||||
// DeviceWWN string
|
||||
// Device Device `gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
//
|
||||
// TestDate time.Time
|
||||
//}
|
||||
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
//type Smart struct {
|
||||
// DeviceWWN string
|
||||
// Device Device `gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
//
|
||||
// TestDate time.Time
|
||||
//
|
||||
// Temp float32
|
||||
// PowerOnCount int64
|
||||
// PowerOnHours int64
|
||||
// SmartStatus string
|
||||
// SmartAttributes string
|
||||
//}
|
||||
@@ -0,0 +1,5 @@
|
||||
package version
|
||||
|
||||
// VERSION is the app-global version string, which will be replaced with a
|
||||
// new value during packaging
|
||||
const VERSION = "1.0.0"
|
||||
Reference in New Issue
Block a user