From 4ba9d864ebd54f367ef638bf823ba419b26b76f5 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Wed, 16 Mar 2022 19:42:12 +0100 Subject: [PATCH 01/11] WIP: restructuring inspector-driver integration --- driver/driver.go | 16 ++- driver/local.go | 21 +++- driver/local_unix_test.go | 8 ++ driver/local_windows_test.go | 8 +- driver/ssh.go | 41 +++--- driver/ssh_test.go | 22 ++++ driver/web.go | 20 ++- driver/web_test.go | 34 +++++ go.mod | 2 +- inspector/custom.go | 44 +++++-- inspector/disk.go | 60 ++++++--- inspector/disk_test.go | 34 ++++- inspector/docker_stats.go | 40 ++++-- inspector/inspector.go | 54 +------- inspector/loadavg.go | 87 +++++++++++-- inspector/loadavg_test.go | 14 --- inspector/loadavg_unix_test.go | 31 +++++ inspector/meminfo.go | 180 ++++++++++++++++++++++----- inspector/meminfo_test.go | 9 +- inspector/process.go | 127 +++++++++++++++++-- inspector/rt.go | 39 ++++-- inspector/tasklist_windows.go | 80 ------------ inspector/uptime.go | 118 +++++++++++++++--- integration/integration_unix_test.go | 162 +++++++++++++----------- 24 files changed, 889 insertions(+), 362 deletions(-) create mode 100644 driver/web_test.go delete mode 100644 inspector/loadavg_test.go create mode 100644 inspector/loadavg_unix_test.go delete mode 100644 inspector/tasklist_windows.go diff --git a/driver/driver.go b/driver/driver.go index e18aae6..1dbc5a3 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -1,5 +1,15 @@ package driver +// SystemInfo gives more insight into system details +type SystemDetails struct { + IsWindows bool + IsLinux bool + IsDarwin bool + IsWeb bool + Name string + Extra string +} + type fields struct { // Supported inspector representations for specific driver Supported []string @@ -7,12 +17,16 @@ type fields struct { Selected []string // Polling interval between retrievals PollInterval int64 + Info *SystemDetails } +// Command represents the two commands ReadFile & RunCommand +type Command func(string) (string, error) + // Driver : specification of functions to be defined by every Driver type Driver interface { ReadFile(path string) (string, error) RunCommand(command string) (string, error) // shows the driver details, not sure if we should be showing OS name - GetDetails() string + GetDetails() SystemDetails } diff --git a/driver/local.go b/driver/local.go index a9fb448..e07ae1c 100644 --- a/driver/local.go +++ b/driver/local.go @@ -40,12 +40,29 @@ func (d *Local) RunCommand(command string) (string, error) { } } out, err := cmd.Output() + fmt.Printf("%s", string(out)) if err != nil { return ``, err } return string(out), nil } -func (d *Local) GetDetails() string { - return fmt.Sprintf(`Local - %s`, runtime.GOOS) +func (d *Local) GetDetails() SystemDetails { + if d.Info == nil { + details := &SystemDetails{} + details.Name = runtime.GOOS + switch details.Name { + case "windows": + details.IsWindows = true + case "linux": + details.IsLinux = true + case "darwin": + details.IsDarwin = true + default: + details.IsLinux = true + } + details.Extra = runtime.GOARCH + d.Info = details + } + return *d.Info } diff --git a/driver/local_unix_test.go b/driver/local_unix_test.go index 9b15fac..5bf3074 100644 --- a/driver/local_unix_test.go +++ b/driver/local_unix_test.go @@ -15,3 +15,11 @@ func TestUnixLocalRunCommand(t *testing.T) { t.Error(err) } } + +func TestUnixLocalSystemDetails(t *testing.T) { + d := Local{} + details := d.GetDetails() + if !(details.IsLinux || details.IsDarwin) { + t.Errorf("Expected Darwin or Linux on unix test, got %s", details.Name) + } +} diff --git a/driver/local_windows_test.go b/driver/local_windows_test.go index 187dc87..7755920 100644 --- a/driver/local_windows_test.go +++ b/driver/local_windows_test.go @@ -13,10 +13,10 @@ func TestWindowsRunCommand(t *testing.T) { } } -func TestWindowsLocalGetDetails(t *testing.T) { +func TestWindowsLocalSystemDetails(t *testing.T) { d := Local{} - output := d.GetDetails() - if output != "Local - windows" { - t.Error(output) + details := d.GetDetails() + if !details.IsWindows { + t.Errorf("Expected windows got %s", details.Name) } } diff --git a/driver/ssh.go b/driver/ssh.go index 3d049f8..7642b40 100644 --- a/driver/ssh.go +++ b/driver/ssh.go @@ -91,20 +91,31 @@ func (d *SSH) RunCommand(command string) (string, error) { return string(out), nil } -func (d *SSH) GetDetails() string { - return fmt.Sprintf(`SSH - %s`, d.String()) -} - -func NewSSHForTest() *SSH { - return &SSH{ - User: "dev", - Host: "127.0.0.1", - Port: 2222, - KeyFile: "/home/deven/.ssh/id_rsa", - KeyPass: "", - CheckKnownHosts: false, - fields: fields{ - PollInterval: 5, - }, +func (d *SSH) GetDetails() SystemDetails { + if d.Info == nil { + uname, err := d.RunCommand(`uname`) + // try windows command + if err != nil { + windowsName, err := d.RunCommand(`systeminfo | findstr /B /C:"OS Name"`) + if err == nil { + if strings.Contains(strings.ToLower(windowsName), "windows") { + uname = "windows" + } + } + } + details := &SystemDetails{} + details.Name = uname + switch details.Name { + case "windows": + details.IsWindows = true + case "linux": + details.IsLinux = true + case "darwin": + details.IsDarwin = true + default: + details.IsLinux = true + } + d.Info = details } + return *d.Info } diff --git a/driver/ssh_test.go b/driver/ssh_test.go index e239f0a..ae19ad3 100644 --- a/driver/ssh_test.go +++ b/driver/ssh_test.go @@ -5,6 +5,20 @@ import ( "testing" ) +func NewSSHForTest() Driver { + return &SSH{ + User: "dev", + Host: "127.0.0.1", + Port: 2222, + KeyFile: "/home/diretnan/.ssh/id_rsa", + KeyPass: "", + CheckKnownHosts: false, + fields: fields{ + PollInterval: 5, + }, + } +} + func TestSSHRunCommand(t *testing.T) { d := NewSSHForTest() output, err := d.RunCommand(`ps -A`) @@ -12,3 +26,11 @@ func TestSSHRunCommand(t *testing.T) { t.Error(err) } } + +func TestSSHSystemDetails(t *testing.T) { + d := NewSSHForTest() + details := d.GetDetails() + if !details.IsLinux { + t.Errorf("Expected linux server for ssh test got %s", details.Name) + } +} diff --git a/driver/web.go b/driver/web.go index 6c42d1a..291d824 100644 --- a/driver/web.go +++ b/driver/web.go @@ -62,16 +62,14 @@ func (d *Web) RunCommand(command string) (string, error) { return ``, errors.New("Cannot read file on web driver") } -func (d *Web) GetDetails() string { - return fmt.Sprintf(`Web - %s`, d.String()) -} - -func NewWebForTest() *Web { - return &Web{ - URL: "https://duckduckgo.com", - Method: GET, - fields: fields{ - PollInterval: 5, - }, +func (d *Web) GetDetails() SystemDetails { + if d.Info == nil { + details := &SystemDetails{ + Name: "web", + Extra: d.URL, + IsWeb: true, + } + d.Info = details } + return *d.Info } diff --git a/driver/web_test.go b/driver/web_test.go new file mode 100644 index 0000000..ec01b5b --- /dev/null +++ b/driver/web_test.go @@ -0,0 +1,34 @@ +package driver + +import ( + "testing" +) + +func NewWebForTest() *Web { + return &Web{ + URL: "https://duckduckgo.com", + Method: GET, + fields: fields{ + PollInterval: 5, + }, + } +} + +func TestWebRunCommand(t *testing.T) { + d := NewWebForTest() + output, err := d.RunCommand(`response`) + if err != nil { + t.Error(err) + } + if output == "" { + t.Error("Could not parse response time") + } +} + +func TestWebSystemDetails(t *testing.T) { + d := NewWebForTest() + details := d.GetDetails() + if !details.IsWeb { + t.Errorf("Expected web driver for web test got %s", details.Name) + } +} diff --git a/go.mod b/go.mod index fa71f44..625d530 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/kr/pretty v0.2.0 // indirect github.com/melbahja/goph v1.2.1 - github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mitchellh/mapstructure v1.4.3 github.com/mum4k/termdash v0.16.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.2.1 diff --git a/inspector/custom.go b/inspector/custom.go index 60365c2..40aa8f4 100644 --- a/inspector/custom.go +++ b/inspector/custom.go @@ -1,6 +1,9 @@ package inspector import ( + "fmt" + + "github.com/bisohns/saido/driver" log "github.com/sirupsen/logrus" ) @@ -11,8 +14,9 @@ type CustomMetrics struct { // Custom : Parsing the custom command output for disk monitoring type Custom struct { - fields - Values CustomMetrics + Driver *driver.Driver + Values CustomMetrics + Command string } // Parse : run custom parsing on output of the command @@ -27,13 +31,35 @@ func (i Custom) createMetric(output string) CustomMetrics { } } -// NewCustom : Initialize a new Custom instance -func NewCustom(custom string) *Custom { - return &Custom{ - fields: fields{ - Type: Command, - Command: custom, - }, +func (i *Custom) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if details.IsWeb { + panic(fmt.Sprintf("Cannot use Custom(%s) on web", i.Command)) } + i.Driver = driver +} +func (i Custom) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *Custom) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) + } +} + +// NewCustom : Initialize a new Custom instance +func NewCustom(custom string, driver *driver.Driver) Inspector { + var customInspector Inspector + details := (*driver).GetDetails() + if details.IsWeb { + panic(fmt.Sprintf("Cannot use Custom(%s) on web", custom)) + } + customInspector = &Custom{ + Command: custom, + } + customInspector.SetDriver(driver) + return customInspector } diff --git a/inspector/disk.go b/inspector/disk.go index 53adde1..9266816 100644 --- a/inspector/disk.go +++ b/inspector/disk.go @@ -1,9 +1,11 @@ package inspector import ( - log "github.com/sirupsen/logrus" "strconv" "strings" + + "github.com/bisohns/saido/driver" + log "github.com/sirupsen/logrus" ) // DFMetrics : Metrics used by DF @@ -16,7 +18,8 @@ type DFMetrics struct { // DF : Parsing the `df` output for disk monitoring type DF struct { - fields + Driver *driver.Driver + Command string // The values read from the command output string are defaultly in KB RawByteSize string // We want do display disk values in GB @@ -40,7 +43,7 @@ func (i *DF) Parse(output string) { continue } columns := strings.Fields(line) - if len(columns) == 6 { + if len(columns) >= 6 { percent := columns[4] if len(percent) > 1 { percent = percent[:len(percent)-1] @@ -51,7 +54,7 @@ func (i *DF) Parse(output string) { if err != nil { log.Fatalf(`Error Parsing Percent Full: %s `, err) } - if columns[5] == i.MountPoint { + if columns[len(columns)-1] == i.MountPoint { values = append(values, i.createMetric(columns, percentInt)) } else if strings.HasPrefix(columns[0], i.DeviceStartsWith) && i.MountPoint == "" { @@ -71,16 +74,45 @@ func (i DF) createMetric(columns []string, percent int) DFMetrics { } } -// NewDF : Initialize a new DF instance -func NewDF() *DF { - return &DF{ - fields: fields{ - Type: Command, - Command: `df -a`, - }, - RawByteSize: `KB`, - DisplayByteSize: `GB`, - MountPoint: `/`, +func (i *DF) SetDriver(driver *driver.Driver) { + i.Driver = driver +} + +func (i DF) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *DF) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) } +} + +// TODO: Implement DF for windows using +// `wmic logicaldisk` to satisfy Inspector interface +type WMIC struct { + Driver *driver.Driver + Command string +} +// NewDF : Initialize a new DF instance +func NewDF(driver *driver.Driver) Inspector { + var df Inspector + details := (*driver).GetDetails() + if !(details.IsLinux || details.IsDarwin || details.IsWindows) { + panic("Cannot use 'df' command on drivers outside (linux, darwin, windows)") + } + if details.IsLinux || details.IsDarwin { + df = &DF{ + // Using -k to ensure size is + // always reported in posix standard of 1K-blocks + Command: `df -a -k`, + RawByteSize: `KB`, + DisplayByteSize: `GB`, + MountPoint: `/`, + } + } + df.SetDriver(driver) + return df } diff --git a/inspector/disk_test.go b/inspector/disk_test.go index a9531e1..13de455 100644 --- a/inspector/disk_test.go +++ b/inspector/disk_test.go @@ -4,11 +4,37 @@ package inspector import ( "testing" + + "github.com/bisohns/saido/driver" ) -func TestDF(t *testing.T) { - d := NewDF() - if d.Type != Command || d.Command != `df -a` { - t.Error("Initialized df wrongly") +func NewSSHForTest() driver.Driver { + return &driver.SSH{ + User: "dev", + Host: "127.0.0.1", + Port: 2222, + KeyFile: "/home/diretnan/.ssh/id_rsa", + KeyPass: "", + CheckKnownHosts: false, + } +} + +func TestDFOnLocal(t *testing.T) { + driver := NewLocalForTest() + d := NewDF(&driver) + d.Execute() + dfConcrete, _ := d.(*DF) + if len(dfConcrete.Values) == 0 { + t.Error("Values are empty!") + } +} + +func TestDFOnSSH(t *testing.T) { + driver := NewSSHForTest() + d := NewDF(&driver) + d.Execute() + dfConcrete, _ := d.(*DF) + if len(dfConcrete.Values) == 0 { + t.Error("Values are empty!") } } diff --git a/inspector/docker_stats.go b/inspector/docker_stats.go index 4f06cf8..da082e2 100644 --- a/inspector/docker_stats.go +++ b/inspector/docker_stats.go @@ -1,9 +1,11 @@ package inspector import ( - log "github.com/sirupsen/logrus" "strconv" "strings" + + "github.com/bisohns/saido/driver" + log "github.com/sirupsen/logrus" ) // DockerStatsMetrics : Metrics used by DockerStats @@ -19,7 +21,8 @@ type DockerStatsMetrics struct { // DockerStats : Parsing the `docker stats` output for container monitoring type DockerStats struct { - fields + Driver *driver.Driver + Command string // We want do display disk values in GB DisplayByteSize string // Values of metrics being read @@ -83,14 +86,31 @@ func (i DockerStats) createMetric( } } -// NewDockerStats : Initialize a new DockerStats instance -func NewDockerStats() *DockerStats { - return &DockerStats{ - fields: fields{ - Type: Command, - Command: `docker stats --no-stream`, - }, - DisplayByteSize: `GB`, +func (i *DockerStats) SetDriver(driver *driver.Driver) { + i.Driver = driver +} + +func (i DockerStats) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *DockerStats) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) } +} +// NewDockerStats : Initialize a new DockerStats instance +func NewDockerStats(driver *driver.Driver) Inspector { + var dockerstats Inspector + details := (*driver).GetDetails() + if !(details.IsLinux || details.IsDarwin || details.IsWindows) { + panic("Cannot use LoadAvgDarwin on drivers outside (linux, darwin, windows)") + } + dockerstats = &DockerStats{ + Command: `docker stats --no-stream`, + } + dockerstats.SetDriver(driver) + return dockerstats } diff --git a/inspector/inspector.go b/inspector/inspector.go index 53020fa..956dd52 100644 --- a/inspector/inspector.go +++ b/inspector/inspector.go @@ -1,59 +1,13 @@ package inspector import ( - log "github.com/sirupsen/logrus" + "github.com/bisohns/saido/driver" ) -// Mode : This specifies whether an Inspector is a command or a file -type Mode int - -const ( - // Command : Inspector is a command to be executes - Command Mode = iota - // File : Inspector is a file to be read - File -) - -// exclude windows specific inspectors -var inspectorMap = map[string]Inspector{ - `disk`: NewDF(), - `meminfo`: NewMemInfo(), - `dockerstats`: NewDockerStats(), - `uptime`: NewUptime(), - `loadavg`: NewLoadAvg(), - `responsetime`: NewResponseTime(), - `custom`: NewCustom(``), -} - -type fields struct { - // Specify a mode for the Inspector - Type Mode - // File path to read - FilePath string - // Command to execute - Command string -} - -func (f *fields) String() string { - value := `None` - if f.Type == Command { - value = f.Command - } else if f.Type == File { - value = f.FilePath - } - return value -} - // Inspector : defines a particular metric supported by a driver type Inspector interface { Parse(output string) -} - -// GetInspector : obtain an initialized inspector using name -func GetInspector(name string) Inspector { - val, ok := inspectorMap[name] - if !ok { - log.Fatalf(`%s inspector not found`, name) - } - return val + SetDriver(driver *driver.Driver) + Execute() + driverExec() driver.Command } diff --git a/inspector/loadavg.go b/inspector/loadavg.go index 39402f9..7634315 100644 --- a/inspector/loadavg.go +++ b/inspector/loadavg.go @@ -4,6 +4,7 @@ import ( "strconv" "strings" + "github.com/bisohns/saido/driver" log "github.com/sirupsen/logrus" ) @@ -16,12 +17,12 @@ type LoadAvgMetrics struct { // LoadAvg : Parsing the /proc/loadavg output for load average monitoring type LoadAvg struct { - fields - Values LoadAvgMetrics + FilePath string + Driver *driver.Driver + Values *LoadAvgMetrics } -// Parse : run custom parsing on output of the command -func (i *LoadAvg) Parse(output string) { +func loadavgParseOutput(output string) *LoadAvgMetrics { var err error log.Debug("Parsing ouput string in LoadAvg inspector") columns := strings.Fields(output) @@ -32,20 +33,86 @@ func (i *LoadAvg) Parse(output string) { log.Fatalf(`Error Parsing LoadAvg: %s `, err) } - i.Values = LoadAvgMetrics{ + return &LoadAvgMetrics{ Load1M, Load5M, Load15M, } } +type LoadAvgDarwin struct { + Command string + Driver *driver.Driver + Values *LoadAvgMetrics +} + +func (i *LoadAvgDarwin) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsDarwin { + panic("Cannot use LoadAvgDarwin on drivers outside (darwin)") + } + i.Driver = driver +} + +func (i LoadAvgDarwin) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *LoadAvgDarwin) Parse(output string) { + i.Values = loadavgParseOutput(output) +} + +func (i *LoadAvgDarwin) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) + } +} + +func (i *LoadAvg) Parse(output string) { + i.Values = loadavgParseOutput(output) +} + +func (i *LoadAvg) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsLinux { + panic("Cannot use LoadAvg on drivers outside (linux)") + } + i.Driver = driver +} + +func (i LoadAvg) driverExec() driver.Command { + return (*i.Driver).ReadFile +} + +func (i *LoadAvg) Execute() { + output, err := i.driverExec()(i.FilePath) + if err == nil { + i.Parse(output) + } +} + +//TODO: Windows Equivalents +// of LoadAvg + // NewLoadAvg : Initialize a new LoadAvg instance -func NewLoadAvg() *LoadAvg { - return &LoadAvg{ - fields: fields{ - Type: File, +func NewLoadAvg(driver *driver.Driver) Inspector { + var loadavg Inspector + details := (*driver).GetDetails() + if !(details.IsLinux || details.IsDarwin) { + panic("Cannot use LoadAvg on drivers outside (linux, darwin)") + } + if details.IsLinux { + loadavg = &LoadAvg{ FilePath: `/proc/loadavg`, - }, + } + } else if details.IsDarwin { + loadavg = &LoadAvgDarwin{ + // Command: `sysctl -n vm.loadavg | awk '/./ { printf "%.2f %.2f %.2f ", $2, $3, $4 }'`, + Command: `top -l 1 | grep "Load Avg\:" | awk '{print $3, $4, $5}'`, + } } + loadavg.SetDriver(driver) + return loadavg } diff --git a/inspector/loadavg_test.go b/inspector/loadavg_test.go deleted file mode 100644 index a54b748..0000000 --- a/inspector/loadavg_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build !windows - -package inspector - -import ( - "testing" -) - -func TestLoadAvg(t *testing.T) { - d := NewLoadAvg() - if d.Type != File || d.FilePath != `/proc/loadavg` { - t.Error("Initialized loadavg wrongly") - } -} diff --git a/inspector/loadavg_unix_test.go b/inspector/loadavg_unix_test.go new file mode 100644 index 0000000..e2395ac --- /dev/null +++ b/inspector/loadavg_unix_test.go @@ -0,0 +1,31 @@ +// +build !windows + +package inspector + +import ( + "testing" + + "github.com/bisohns/saido/driver" +) + +func NewLocalForTest() driver.Driver { + return &driver.Local{} +} + +func TestLoadAvg(t *testing.T) { + testDriver := NewLocalForTest() + loadavg := NewLoadAvg(&testDriver) + loadavg.Execute() + loadavgConcreteLinux, ok := loadavg.(*LoadAvg) + if ok { + if loadavgConcreteLinux.Values == nil { + t.Error("Load metrics for linux did not get set") + } + } + loadavgConcreteDarwin, ok := loadavg.(*LoadAvgDarwin) + if ok { + if loadavgConcreteDarwin.Values == nil { + t.Error("Load metrics for darwin did not get set") + } + } +} diff --git a/inspector/meminfo.go b/inspector/meminfo.go index b70cf5e..999280f 100644 --- a/inspector/meminfo.go +++ b/inspector/meminfo.go @@ -1,8 +1,12 @@ package inspector import ( - log "github.com/sirupsen/logrus" + "fmt" + "strconv" "strings" + + "github.com/bisohns/saido/driver" + log "github.com/sirupsen/logrus" ) // Metrics used by MemInfo @@ -14,29 +18,52 @@ type MemInfoMetrics struct { SwapFree float64 } -// MemInfo : Parsing the `/proc/meminfo` file output for memory monitoring -type MemInfo struct { - fields +// MemInfoLinux : Parsing the `/proc/meminfo` file output for memory monitoring +type MemInfoLinux struct { + Driver *driver.Driver + FilePath string // The values read from the command output string are defaultly in KB RawByteSize string // We want do display disk values in GB DisplayByteSize string // Values of metrics being read - Values MemInfoMetrics + Values *MemInfoMetrics } -// Parse : run custom parsing on output of the command -func (i *MemInfo) Parse(output string) { - log.Debug("Parsing ouput string in MemInfo inspector") - memTotal := i.getMatching("MemTotal", output) - memFree := i.getMatching("MemFree", output) - cached := i.getMatching("Cached", output) - swapTotal := i.getMatching("SwapTotal", output) - swapFree := i.getMatching("SwapFree", output) - i.Values = i.createMetric([]string{memTotal, memFree, cached, swapTotal, swapFree}) +// MemInfoDarwin : Parsing `top -l 1` and `sysctl` to be able to retrieve memory details +type MemInfoDarwin struct { + Driver *driver.Driver + PhysMemCommand string + SwapCommand string + // The values read from the command output string are defaultly in KB + RawByteSize string + // We want do display disk values in GB + DisplayByteSize string + // Values of metrics being read + Values *MemInfoMetrics } -func (i MemInfo) getMatching(metric string, rows string) string { +func memInfoParseOutput(output, rawByteSize, displayByteSize string) *MemInfoMetrics { + log.Debug("Parsing ouput string in meminfo inspector") + memTotal := getMatching("MemTotal", output) + memFree := getMatching("MemFree", output) + cached := getMatching("Cached", output) + swapTotal := getMatching("SwapTotal", output) + swapFree := getMatching("SwapFree", output) + return createMetric( + []string{ + memTotal, + memFree, + cached, + swapTotal, + swapFree, + }, + rawByteSize, + displayByteSize, + ) +} + +func getMatching(metric string, rows string) string { lines := strings.Split(rows, "\n") for _, line := range lines { if strings.HasPrefix(line, metric) { @@ -47,25 +74,118 @@ func (i MemInfo) getMatching(metric string, rows string) string { return `0` } -func (i MemInfo) createMetric(columns []string) MemInfoMetrics { - return MemInfoMetrics{ - MemTotal: NewByteSize(columns[0], i.RawByteSize).format(i.DisplayByteSize), - MemFree: NewByteSize(columns[1], i.RawByteSize).format(i.DisplayByteSize), - Cached: NewByteSize(columns[2], i.RawByteSize).format(i.DisplayByteSize), - SwapTotal: NewByteSize(columns[3], i.RawByteSize).format(i.DisplayByteSize), - SwapFree: NewByteSize(columns[4], i.RawByteSize).format(i.DisplayByteSize), +func createMetric(columns []string, rawByteSize, displayByteSize string) *MemInfoMetrics { + return &MemInfoMetrics{ + MemTotal: NewByteSize(columns[0], rawByteSize).format(displayByteSize), + MemFree: NewByteSize(columns[1], rawByteSize).format(displayByteSize), + Cached: NewByteSize(columns[2], rawByteSize).format(displayByteSize), + SwapTotal: NewByteSize(columns[3], rawByteSize).format(displayByteSize), + SwapFree: NewByteSize(columns[4], rawByteSize).format(displayByteSize), + } +} + +// Parse : run custom parsing on output of the command +func (i *MemInfoLinux) Parse(output string) { + log.Debug("Parsing ouput string in MemInfoLinux inspector") + i.Values = memInfoParseOutput(output, i.RawByteSize, i.DisplayByteSize) +} + +func (i *MemInfoLinux) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsLinux { + panic("Cannot use MeminfoLinux outside (linux)") + } + i.Driver = driver +} + +func (i MemInfoLinux) driverExec() driver.Command { + return (*i.Driver).ReadFile +} + +func (i *MemInfoLinux) Execute() { + output, err := i.driverExec()(i.FilePath) + if err == nil { + i.Parse(output) } } -// NewMemInfo : Initialize a new MemInfo instance -func NewMemInfo() *MemInfo { - return &MemInfo{ - fields: fields{ - Type: File, - FilePath: `/proc/meminfo`, +func (i *MemInfoDarwin) Parse(output string) { + rows := strings.Split(output, "\n") + physMemRaw := rows[0] + swapRaw := rows[1] + physMemCols := strings.Fields(physMemRaw) + swapCols := strings.Fields(swapRaw) + memUsed := strings.TrimSuffix(physMemCols[0], "M") + memUnused := strings.TrimSuffix(physMemCols[1], "M") + memUsedInt, err := strconv.ParseInt(memUsed, 0, 64) + memUnusedInt, err := strconv.ParseInt(memUnused, 0, 64) + if err != nil { + panic("Error parsing memory on MemInfoDarwin") + } + memTotal := fmt.Sprintf("%d", memUsedInt+memUnusedInt) + swapTotal := strings.TrimSuffix(swapCols[0], "M") + swapFree := strings.TrimSuffix(swapCols[1], "M") + //TODO: Figure out where to get cached size + i.Values = createMetric( + []string{ + memTotal, + memUnused, + `0`, + swapTotal, + swapFree, }, - RawByteSize: `KB`, - DisplayByteSize: `MB`, + i.RawByteSize, + i.DisplayByteSize, + ) +} + +func (i *MemInfoDarwin) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsDarwin { + panic("Cannot use MeminfoDarwin outside (darwin)") } + i.Driver = driver +} + +func (i MemInfoDarwin) driverExec() driver.Command { + return (*i.Driver).RunCommand +} +func (i *MemInfoDarwin) Execute() { + physMemOutput, err := i.driverExec()(i.PhysMemCommand) + swapOutput, err := i.driverExec()(i.SwapCommand) + + if err == nil { + physMemOutput = strings.TrimSuffix(physMemOutput, "\n") + swapOutput = strings.TrimSuffix(swapOutput, "\n") + output := fmt.Sprintf("%s\n%s", physMemOutput, swapOutput) + i.Parse(output) + } +} + +// TODO: Windows Equivalents of MemInfo + +// NewMemInfoLinux : Initialize a new MemInfoLinux instance +func NewMemInfo(driver *driver.Driver) Inspector { + var meminfo Inspector + details := (*driver).GetDetails() + if !(details.IsLinux || details.IsDarwin) { + panic("Cannot use MemInfo on drivers outside (linux, darwin)") + } + if details.IsLinux { + meminfo = &MemInfoLinux{ + FilePath: `/proc/meminfo`, + RawByteSize: `KB`, + DisplayByteSize: `MB`, + } + } else if details.IsDarwin { + meminfo = &MemInfoDarwin{ + PhysMemCommand: `top -l 1 | grep PhysMem: | awk '{print $2, $6}'`, + SwapCommand: `sysctl -n vm.swapusage | awk '{print $3, $9}'`, + RawByteSize: `MB`, + DisplayByteSize: `MB`, + } + } + meminfo.SetDriver(driver) + return meminfo } diff --git a/inspector/meminfo_test.go b/inspector/meminfo_test.go index dd26fbb..1a2240a 100644 --- a/inspector/meminfo_test.go +++ b/inspector/meminfo_test.go @@ -6,9 +6,8 @@ import ( "testing" ) -func TestMemInfo(t *testing.T) { - d := NewMemInfo() - if d.Type != File || d.FilePath != `/proc/meminfo` { - t.Error("Initialized meminfo wrongly") - } +func TestMemInfoOnLocal(t *testing.T) { + driver := NewLocalForTest() + d := NewMemInfo(&driver) + d.Execute() } diff --git a/inspector/process.go b/inspector/process.go index 8096b49..12c2744 100644 --- a/inspector/process.go +++ b/inspector/process.go @@ -1,9 +1,11 @@ package inspector import ( - log "github.com/sirupsen/logrus" "strconv" "strings" + + "github.com/bisohns/saido/driver" + log "github.com/sirupsen/logrus" ) // ProcessMetrics : Metrics used by Process @@ -20,15 +22,50 @@ type ProcessMetrics struct { TTY string } +type ProcessMetricsWin struct { + Command string + SessionName string + Pid int + Memory float64 +} + // Process : Parsing the `ps -A u` output for process monitoring type Process struct { - fields + Driver *driver.Driver + Command string // Track this particular PID TrackPID int // Values of metrics being read Values []ProcessMetrics } +// ProcessWin : Parsing the `tasklist` output for process monitoring on windows +type ProcessWin struct { + Driver *driver.Driver + Command string + TrackPID int + Values []ProcessMetricsWin +} + +func (i *Process) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !(details.IsLinux || details.IsDarwin) { + panic("Cannot use Process on drivers outside (linux, darwin)") + } + i.Driver = driver +} + +func (i Process) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *Process) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) + } +} + // Parse : run custom parsing on output of the command func (i *Process) Parse(output string) { var values []ProcessMetrics @@ -81,13 +118,87 @@ func (i Process) createMetric(columns []string, pid int) ProcessMetrics { } } +func (i *ProcessWin) Parse(output string) { + var values []ProcessMetricsWin + lines := strings.Split(output, "\n") + for index, line := range lines { + // skip title line + if index == 0 { + continue + } + columns := strings.Fields(line) + colLength := len(columns) + if colLength >= 6 { + pidRaw := columns[colLength-5] + pid, err := strconv.Atoi(pidRaw) + if err != nil { + panic("Could not parse pid for row") + } + if i.TrackPID != 0 && i.TrackPID == pid { + value := i.createMetric(columns, pid) + values = append(values, value) + break + } else if i.TrackPID == 0 { + value := i.createMetric(columns, pid) + values = append(values, value) + } + } + } + i.Values = values +} + +func (i *ProcessWin) createMetric(columns []string, pid int) ProcessMetricsWin { + colLength := len(columns) + memoryRaw := strings.Replace(columns[colLength-2], ",", "", -1) + memory, err := strconv.ParseFloat(memoryRaw, 64) + if err != nil { + panic("Error parsing memory in ProcessWin") + } + sessionName := columns[colLength-4] + command := strings.Join(columns[:colLength-5], " ") + + return ProcessMetricsWin{ + Command: command, + Pid: pid, + SessionName: sessionName, + Memory: memory, + } +} + +func (i *ProcessWin) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsWindows { + panic("Cannot use ProcessWin on drivers outside (windows)") + } + i.Driver = driver +} + +func (i ProcessWin) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *ProcessWin) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) + } +} + // NewProcess : Initialize a new Process instance -func NewProcess() *Process { - return &Process{ - fields: fields{ - Type: Command, +func NewProcess(driver *driver.Driver) Inspector { + var process Inspector + details := (*driver).GetDetails() + if !(details.IsLinux || details.IsDarwin || details.IsWindows) { + panic("Cannot use Process on drivers outside (linux, darwin, windows)") + } + if details.IsLinux || details.IsDarwin { + process = &Process{ Command: `ps -A u`, - }, + } + } else { + process = &ProcessWin{ + Command: `tasklist`, + } } - + return process } diff --git a/inspector/rt.go b/inspector/rt.go index f8880b4..24c0532 100644 --- a/inspector/rt.go +++ b/inspector/rt.go @@ -1,8 +1,10 @@ package inspector import ( - log "github.com/sirupsen/logrus" "strconv" + + "github.com/bisohns/saido/driver" + log "github.com/sirupsen/logrus" ) // ResponseTimeMetrics : Metrics used by ResponseTime @@ -12,7 +14,8 @@ type ResponseTimeMetrics struct { // ResponseTime : Parsing the `web` output for response time type ResponseTime struct { - fields + Driver *driver.Driver + Command string // Values of metrics being read Values ResponseTimeMetrics } @@ -30,13 +33,31 @@ func (i *ResponseTime) Parse(output string) { i.Values = values } -// NewResponseTime : Initialize a new ResponseTime instance -func NewResponseTime() *ResponseTime { - return &ResponseTime{ - fields: fields{ - Type: Command, - Command: `response`, - }, +func (i *ResponseTime) SetDriver(driver *driver.Driver) { + i.Driver = driver +} + +func (i ResponseTime) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *ResponseTime) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) } +} +// NewResponseTime : Initialize a new ResponseTime instance +func NewResponseTime(driver *driver.Driver) Inspector { + var responsetime Inspector + details := (*driver).GetDetails() + if !(details.IsWeb) { + panic("Cannot use response time outside driver (web)") + } + responsetime = &ResponseTime{ + Command: `response`, + } + responsetime.SetDriver(driver) + return responsetime } diff --git a/inspector/tasklist_windows.go b/inspector/tasklist_windows.go deleted file mode 100644 index 23337dd..0000000 --- a/inspector/tasklist_windows.go +++ /dev/null @@ -1,80 +0,0 @@ -package inspector - -import ( - log "github.com/sirupsen/logrus" - "strconv" - "strings" -) - -// TasklistMetrics : Metrics used by Tasklist -type TasklistMetrics struct { - Command string - Session string - Pid int - // value of memory used - Memory float64 -} - -// Tasklist : Parsing the `tasklist` output for process monitoring on windows -type Tasklist struct { - fields - // Track this particular PID - TrackPID int - // We want do display memory values in KB - DisplayByteSize string - // Values of metrics being read - Values []TasklistMetrics -} - -// Parse : run custom parsing on output of the command -func (i *Tasklist) Parse(output string) { - var values []TasklistMetrics - lines := strings.Split(output, "\r\n") - for index, line := range lines { - // skip title line - if index == 0 || index == 1 { - continue - } - columns := strings.Fields(line) - lenCol := len(columns) - if lenCol >= 6 { - pid, err := strconv.Atoi(columns[lenCol-5]) - if err != nil { - log.Fatal("Could not parse pid in Tasklist") - } - // If we are tracking only a particular ID then break loop - if i.TrackPID != 0 && i.TrackPID == pid { - value := i.createMetric(columns, pid) - values = append(values, value) - break - } else if i.TrackPID == 0 { - value := i.createMetric(columns, pid) - values = append(values, value) - } - } - } - i.Values = values -} - -func (i Tasklist) createMetric(columns []string, pid int) TasklistMetrics { - lenCol := len(columns) - - return TasklistMetrics{ - Command: strings.Join(columns[:lenCol-5], " "), - Memory: NewByteSize(columns[lenCol-2], `KB`).format(i.DisplayByteSize), - Session: columns[lenCol-4], - Pid: pid, - } -} - -// NewTasklist : Initialize a new Tasklist instance -func NewTasklist() *Tasklist { - return &Tasklist{ - fields: fields{ - Type: Command, - Command: `tasklist`, - }, - DisplayByteSize: `KB`, - } - -} diff --git a/inspector/uptime.go b/inspector/uptime.go index 7a11be3..90eadea 100644 --- a/inspector/uptime.go +++ b/inspector/uptime.go @@ -1,26 +1,43 @@ package inspector import ( + "fmt" "strconv" "strings" + "github.com/bisohns/saido/driver" log "github.com/sirupsen/logrus" ) // UptimeMetrics : Metrics used by Uptime type UptimeMetrics struct { - Up float64 + Up float64 + // Idle time will not be less than uptime on + // multiprocessor systems as the metric being + // returned is the idle time from all processors + // e.g 80 on an 8 processor system means each + // processor has been idle for an average of 10 seconds Idle float64 + // % of time CPU has been idle + IdlePercent float64 } -// Uptime : Parsing the /proc/uptime output for uptime monitoring -type Uptime struct { - fields - Values UptimeMetrics +// UptimeLinux : Parsing the /proc/uptime output for uptime monitoring +type UptimeLinux struct { + Driver *driver.Driver + FilePath string + Values *UptimeMetrics +} + +type UptimeDarwin struct { + Driver *driver.Driver + UpCommand string + IdleCommand string + Values *UptimeMetrics } // Parse : run custom parsing on output of the command -func (i *Uptime) Parse(output string) { +func (i *UptimeLinux) Parse(output string) { var err error log.Debug("Parsing ouput string in Uptime inspector") columns := strings.Fields(output) @@ -30,19 +47,90 @@ func (i *Uptime) Parse(output string) { log.Fatalf(`Error Parsing Uptime: %s `, err) } - i.Values = UptimeMetrics{ - Up, - Idle, + i.Values = &UptimeMetrics{ + Up: Up, + Idle: Idle, + } +} + +func (i *UptimeLinux) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsLinux { + panic("Cannot use UptimeLinux on drivers outside (linux)") + } + i.Driver = driver +} + +func (i UptimeLinux) driverExec() driver.Command { + return (*i.Driver).ReadFile +} + +func (i *UptimeLinux) Execute() { + output, err := i.driverExec()(i.FilePath) + if err == nil { + i.Parse(output) } } +func (i *UptimeDarwin) Parse(output string) { + log.Debug("Parsing ouput string in UptimeDarwin inspector") + output = strings.TrimSuffix(output, ",") + lines := strings.Split(output, "\n") + unixTime, err := strconv.Atoi(lines[0]) + switchedOn, err := strconv.Atoi(lines[1]) + idleTime, err := strconv.ParseFloat(lines[2], 64) + if err != nil { + panic("Could not parse times in UptimeDarwin") + } + + i.Values = &UptimeMetrics{ + Up: float64(unixTime - switchedOn), + IdlePercent: idleTime, + } +} + +func (i *UptimeDarwin) SetDriver(driver *driver.Driver) { + details := (*driver).GetDetails() + if !details.IsDarwin { + panic("Cannot use UptimeDarwin on drivers outside (darwin)") + } + i.Driver = driver +} + +func (i UptimeDarwin) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *UptimeDarwin) Execute() { + upOutput, err := i.driverExec()(i.UpCommand) + idleOutput, err := i.driverExec()(i.IdleCommand) + if err == nil { + upOutput = strings.TrimSuffix(upOutput, ",") + idleOutput = strings.TrimSuffix(idleOutput, "%") + output := fmt.Sprintf("%s\n%s", upOutput, idleOutput) + i.Parse(output) + } +} + +//TODO: Windows equivalent of uptime + // NewUptime : Initialize a new Uptime instance -func NewUptime() *Uptime { - return &Uptime{ - fields: fields{ - Type: File, +func NewUptime(driver *driver.Driver) Inspector { + var uptime Inspector + details := (*driver).GetDetails() + if !(details.IsDarwin || details.IsLinux) { + panic("Cannot use Uptime on drivers outside (linux, darwin)") + } + if details.IsLinux { + uptime = &UptimeLinux{ FilePath: `/proc/uptime`, - }, + } + } else if details.IsDarwin { + uptime = &UptimeDarwin{ + UpCommand: `date +%s; sysctl kern.boottime | awk '{print $5}'`, + IdleCommand: `top -l 1 | grep "CPU usage" | awk '{print $7}'`, + } } - + uptime.SetDriver(driver) + return uptime } diff --git a/integration/integration_unix_test.go b/integration/integration_unix_test.go index 1f44d01..da4350e 100644 --- a/integration/integration_unix_test.go +++ b/integration/integration_unix_test.go @@ -12,67 +12,80 @@ import ( "github.com/bisohns/saido/inspector" ) +func NewLocalForTest() driver.Driver { + return &driver.Local{} +} + func TestDFonLocal(t *testing.T) { - d := driver.Local{} + d := NewLocalForTest() // can either use NewDF() or get the interface and perform type assertion - i := (inspector.GetInspector(`disk`)).(*inspector.DF) - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) - } - i.Parse(output) - if i.Values[0].Used == 0 { + i := inspector.NewDF(&d) + i.Execute() + iConcrete, _ := i.(*inspector.DF) + if iConcrete.Values[0].Used == 0 { t.Error("showing percent used as 0") } - fmt.Printf(`%#v`, i.Values) + fmt.Printf(`%#v`, iConcrete.Values) } func TestMemInfoonLocal(t *testing.T) { - d := driver.Local{} - // can either use NewDF() or get the interface and perform type assertion - i := (inspector.GetInspector(`meminfo`)).(*inspector.MemInfo) - output, err := d.ReadFile(i.String()) - if err != nil { - t.Error(err) + d := NewLocalForTest() + i := inspector.NewMemInfo(&d) + i.Execute() + iConcreteLinux, ok := i.(*inspector.MemInfoLinux) + if ok { + if iConcreteLinux.Values.MemTotal == 0 { + t.Error("showing percent used as 0") + } + fmt.Printf(`%#v`, iConcreteLinux.Values) } - i.Parse(output) - if i.Values.MemTotal == 0 { - t.Error("showing percent used as 0") + iConcreteDarwin, ok := i.(*inspector.MemInfoDarwin) + if ok { + if iConcreteDarwin.Values.MemTotal == 0 { + t.Error("showing percent used as 0") + } + fmt.Printf(`%#v`, iConcreteDarwin.Values) } - fmt.Printf(`%#v`, i.Values) } func TestDockerStatsonLocal(t *testing.T) { - d := driver.Local{} - // can either use NewDF() or get the interface and perform type assertion - i := (inspector.GetInspector(`dockerstats`)).(*inspector.DockerStats) - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) - } - i.Parse(output) - if len(i.Values) == 0 { + d := NewLocalForTest() + i := inspector.NewDockerStats(&d) + i.Execute() + iConcrete, _ := i.(*inspector.DockerStats) + if len(iConcrete.Values) == 0 { t.Error("showing no running container") } - fmt.Printf(`%#v`, i.Values) + fmt.Printf(`%#v`, iConcrete.Values) } func TestProcessonLocal(t *testing.T) { - d := driver.Local{} - i := inspector.NewProcess() - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) + d := NewLocalForTest() + i := inspector.NewProcess(&d) + i.Execute() + iConcreteUnix, ok := i.(*inspector.Process) + if ok { + if len(iConcreteUnix.Values) <= 2 { + t.Error("Values are less than or equal 2") + } + // Track just root PID of 1 + iConcreteUnix.TrackPID = 1 + iConcreteUnix.Execute() + if len(iConcreteUnix.Values) != 1 { + t.Error("unexpected size of single PID tracking") + } } - i.Parse(output) - if len(i.Values) <= 2 { - t.Error(err) - } - // Track just root PID of 1 - i.TrackPID = 1 - i.Parse(output) - if len(i.Values) != 1 { - t.Error("unexpected size of single PID tracking") + iConcreteWin, ok := i.(*inspector.ProcessWin) + if ok { + if len(iConcreteWin.Values) <= 2 { + t.Error("Values are less than or equal 2") + } + // Track just system PID of 4 + iConcreteWin.TrackPID = 4 + iConcreteWin.Execute() + if len(iConcreteWin.Values) != 1 { + t.Error("unexpected size of single PID tracking") + } } } @@ -93,42 +106,51 @@ func TestProcessonLocal(t *testing.T) { //} func TestCustomonLocal(t *testing.T) { - d := driver.Local{} - i := inspector.NewCustom(`echo /test/test`) - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) - } - i.Parse(output) - if strings.TrimSpace(i.Values.Output) != "/test/test" { - t.Errorf("%s", i.Values.Output) + d := NewLocalForTest() + i := inspector.NewCustom(`echo /test/test`, &d) + i.Execute() + iConcrete, _ := i.(*inspector.Custom) + if strings.TrimSpace(iConcrete.Values.Output) != "/test/test" { + t.Errorf("%s", iConcrete.Values.Output) } } func TestLoadAvgonLocal(t *testing.T) { - d := driver.Local{} - i := inspector.NewLoadAvg() - output, err := d.ReadFile(i.String()) - if err != nil { - t.Error(err) + d := NewLocalForTest() + i := inspector.NewLoadAvg(&d) + i.Execute() + iConcreteDarwin, ok := i.(*inspector.LoadAvgDarwin) + if ok { + if iConcreteDarwin.Values.Load1M == 0 { + t.Errorf("%f", iConcreteDarwin.Values.Load1M) + } + fmt.Printf("%#v", iConcreteDarwin.Values) } - i.Parse(output) - if i.Values.Load1M == 0 { - t.Errorf("%f", i.Values.Load1M) + iConcrete, ok := i.(*inspector.LoadAvg) + if ok { + if iConcrete.Values.Load1M == 0 { + t.Errorf("%f", iConcrete.Values.Load1M) + } + fmt.Printf("%#v", iConcrete.Values) } - fmt.Printf("%#v", i.Values) } func TestUptimeonLocal(t *testing.T) { - d := driver.Local{} - i := inspector.NewUptime() - output, err := d.ReadFile(i.String()) - if err != nil { - t.Error(err) + d := NewLocalForTest() + i := inspector.NewUptime(&d) + i.Execute() + iConcreteLinux, ok := i.(*inspector.UptimeLinux) + if ok { + if iConcreteLinux.Values.Up == 0 { + t.Errorf("%f", iConcreteLinux.Values.Up) + } + fmt.Printf("%#v", iConcreteLinux.Values) } - i.Parse(output) - if i.Values.Up == 0 { - t.Errorf("%f", i.Values.Up) + iConcreteDarwin, ok := i.(*inspector.UptimeDarwin) + if ok { + if iConcreteDarwin.Values.Up == 0 { + t.Errorf("%f", iConcreteDarwin.Values.Up) + } + fmt.Printf("%#v", iConcreteDarwin.Values) } - fmt.Printf("%#v", i.Values) } From e07550fdf16f690f18916710ca3fa447962e9ebf Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Wed, 16 Mar 2022 22:40:54 +0100 Subject: [PATCH 02/11] Fixing bash execution to make use of bash --- driver/local.go | 14 +-- driver/web.go | 2 +- inspector/loadavg.go | 6 +- inspector/meminfo_test.go | 12 +++ integration/integration_test.go | 158 ++++++++++++++++++++------------ 5 files changed, 122 insertions(+), 70 deletions(-) diff --git a/driver/local.go b/driver/local.go index e07ae1c..7a1e582 100644 --- a/driver/local.go +++ b/driver/local.go @@ -1,12 +1,10 @@ package driver import ( - "fmt" "io/ioutil" "os" "os/exec" "runtime" - "strings" log "github.com/sirupsen/logrus" ) @@ -26,21 +24,25 @@ func (d *Local) ReadFile(path string) (string, error) { return string(content), nil } +// RunCommand : For simple commands without shell variables, pipes, e.t.c +// They can be passed directly. For complex commands e.g +// `echo something | awk $var`, turn into a file to be saved +// under ./shell/ func (d *Local) RunCommand(command string) (string, error) { // FIXME: If command contains a shell variable $ or glob // type pattern, it would not be executed, see // https://pkg.go.dev/os/exec for more information - cmdArgs := strings.Fields(command) + var cmd *exec.Cmd log.Debugf("Running command `%s` ", command) - cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + cmd = exec.Command("bash", "-c", command) cmd.Env = os.Environ() if len(d.Vars) != 0 { for _, v := range d.Vars { cmd.Env = append(cmd.Env, v) } } + _ = cmd.Wait() out, err := cmd.Output() - fmt.Printf("%s", string(out)) if err != nil { return ``, err } @@ -58,8 +60,6 @@ func (d *Local) GetDetails() SystemDetails { details.IsLinux = true case "darwin": details.IsDarwin = true - default: - details.IsLinux = true } details.Extra = runtime.GOARCH d.Info = details diff --git a/driver/web.go b/driver/web.go index 291d824..2f6e339 100644 --- a/driver/web.go +++ b/driver/web.go @@ -21,7 +21,7 @@ const ( GET Request = "GET" ) -// Web : Driver for handling ssh executions +// Web : Driver for handling web executions type Web struct { fields // URL e.g https://google.com diff --git a/inspector/loadavg.go b/inspector/loadavg.go index 7634315..dd31ab4 100644 --- a/inspector/loadavg.go +++ b/inspector/loadavg.go @@ -59,6 +59,8 @@ func (i LoadAvgDarwin) driverExec() driver.Command { } func (i *LoadAvgDarwin) Parse(output string) { + output = strings.TrimSuffix(output, "}") + output = strings.TrimPrefix(output, "{") i.Values = loadavgParseOutput(output) } @@ -108,8 +110,8 @@ func NewLoadAvg(driver *driver.Driver) Inspector { } } else if details.IsDarwin { loadavg = &LoadAvgDarwin{ - // Command: `sysctl -n vm.loadavg | awk '/./ { printf "%.2f %.2f %.2f ", $2, $3, $4 }'`, - Command: `top -l 1 | grep "Load Avg\:" | awk '{print $3, $4, $5}'`, + // Command: `sysctl -n vm.loadavg | awk '{ printf "%.2f %.2f %.2f ", $2, $3, $4 }'`, + Command: `top -l 1 | grep "Load Avg:" | awk '{print $3, $4, $5}'`, } } loadavg.SetDriver(driver) diff --git a/inspector/meminfo_test.go b/inspector/meminfo_test.go index 1a2240a..3407de8 100644 --- a/inspector/meminfo_test.go +++ b/inspector/meminfo_test.go @@ -10,4 +10,16 @@ func TestMemInfoOnLocal(t *testing.T) { driver := NewLocalForTest() d := NewMemInfo(&driver) d.Execute() + iConcreteLinux, ok := d.(*MemInfoLinux) + if ok { + if iConcreteLinux.Values == nil { + t.Error("Values did not get set for MemInfoLinux") + } + } + iConcreteDarwin, ok := d.(*MemInfoDarwin) + if ok { + if iConcreteDarwin.Values == nil { + t.Error("Values did not get set for MemInfoDarwin") + } + } } diff --git a/integration/integration_test.go b/integration/integration_test.go index e2049b3..c0d0dc1 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -9,95 +9,133 @@ import ( "github.com/bisohns/saido/inspector" ) +func NewWebForTest() driver.Driver { + return &driver.Web{ + URL: "https://duckduckgo.com", + Method: driver.GET, + } +} + +func NewSSHForTest() driver.Driver { + return &driver.SSH{ + User: "dev", + Host: "127.0.0.1", + Port: 2222, + KeyFile: "/home/diretnan/.ssh/id_rsa", + KeyPass: "", + CheckKnownHosts: false, + } +} + func TestDFonSSH(t *testing.T) { - d := driver.NewSSHForTest() - i := inspector.NewDF() - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) + d := NewSSHForTest() + i := inspector.NewDF(&d) + i.Execute() + iConcrete, ok := i.(*inspector.DF) + if ok { + fmt.Printf(`%#v`, iConcrete.Values) } - i.Parse(output) - fmt.Printf(`%#v`, i.Values) } func TestMemInfoonSSH(t *testing.T) { - d := driver.NewSSHForTest() - i := inspector.NewMemInfo() - output, err := d.ReadFile(i.String()) - if err != nil { - t.Error(err) + d := NewSSHForTest() + i := inspector.NewMemInfo(&d) + i.Execute() + iConcreteLinux, ok := i.(*inspector.MemInfoLinux) + if ok { + if iConcreteLinux.Values.MemTotal == 0 { + t.Error("showing percent used as 0") + } + fmt.Printf(`%#v`, iConcreteLinux.Values) } - i.Parse(output) - if i.Values.MemTotal == 0 { - t.Error("showing percent used as 0") + iConcreteDarwin, ok := i.(*inspector.MemInfoDarwin) + if ok { + if iConcreteDarwin.Values.MemTotal == 0 { + t.Error("showing percent used as 0") + } + fmt.Printf(`%#v`, iConcreteDarwin.Values) } - fmt.Printf(`%#v`, i.Values) } func TestResponseTimeonWeb(t *testing.T) { - d := driver.NewWebForTest() - i := inspector.NewResponseTime() - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) + d := NewWebForTest() + i := inspector.NewResponseTime(&d) + i.Execute() + iConcrete, ok := i.(*inspector.ResponseTime) + if ok { + if iConcrete.Values.Seconds == 0 { + t.Error("showing response time as 0") + } + fmt.Printf(`%#v`, iConcrete.Values) } - i.Parse(output) - if i.Values.Seconds == 0 { - t.Error("showing response time as 0") - } - fmt.Printf(`%#v`, i.Values) } func TestProcessonSSH(t *testing.T) { - d := driver.NewSSHForTest() - i := inspector.NewProcess() - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) + d := NewSSHForTest() + i := inspector.NewProcess(&d) + i.Execute() + iConcreteUnix, ok := i.(*inspector.Process) + if ok { + if len(iConcreteUnix.Values) <= 2 { + t.Error("Less than two processes running") + } } - i.Parse(output) - if len(i.Values) <= 2 { - t.Error(err) + iConcreteWin, ok := i.(*inspector.ProcessWin) + if ok { + if len(iConcreteWin.Values) <= 2 { + t.Error("Less than two processes running") + } } } func TestCustomonSSH(t *testing.T) { - d := driver.NewSSHForTest() + d := NewSSHForTest() // set vars - d.Vars = []string{"MONKEY=true"} - i := inspector.NewCustom(`echo $MONKEY`) - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) - } - i.Parse(output) - if strings.TrimSpace(i.Values.Output) != "true" { - t.Errorf("%s", i.Values.Output) + dfConcrete, _ := d.(*driver.SSH) + dfConcrete.Vars = []string{"MONKEY=true"} + d = dfConcrete + i := inspector.NewCustom(`echo $MONKEY`, &d) + i.Execute() + iConcrete, ok := i.(*inspector.Custom) + if ok { + if strings.TrimSpace(iConcrete.Values.Output) != "true" { + t.Errorf("%s", iConcrete.Values.Output) + } } } func TestLoadAvgonSSH(t *testing.T) { - d := driver.NewSSHForTest() - i := inspector.NewLoadAvg() - output, err := d.ReadFile(i.String()) - if err != nil { - t.Error(err) + d := NewSSHForTest() + i := inspector.NewLoadAvg(&d) + i.Execute() + iConcreteLinux, ok := i.(*inspector.LoadAvg) + if ok { + if iConcreteLinux.Values.Load1M == 0 { + t.Errorf("%f", iConcreteLinux.Values.Load1M) + } } - i.Parse(output) - if i.Values.Load1M == 0 { - t.Errorf("%f", i.Values.Load1M) + iConcreteDarwin, ok := i.(*inspector.LoadAvgDarwin) + if ok { + if iConcreteDarwin.Values.Load1M == 0 { + t.Errorf("%f", iConcreteDarwin.Values.Load1M) + } } } func TestUptimeonSSH(t *testing.T) { - d := driver.NewSSHForTest() - i := inspector.NewUptime() - output, err := d.ReadFile(i.String()) - if err != nil { - t.Error(err) + d := NewSSHForTest() + i := inspector.NewUptime(&d) + i.Execute() + iConcreteLinux, ok := i.(*inspector.UptimeLinux) + if ok { + if iConcreteLinux.Values.Up == 0 { + t.Errorf("%f", iConcreteLinux.Values.Up) + } } - i.Parse(output) - if i.Values.Up == 0 { - t.Errorf("%f", i.Values.Up) + iConcreteDarwin, ok := i.(*inspector.UptimeDarwin) + if ok { + if iConcreteDarwin.Values.Up == 0 { + t.Errorf("%f", iConcreteDarwin.Values.Up) + } } } From 5c292d04c92ab652fcfe9e56f9764f34a2286d7e Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Wed, 16 Mar 2022 22:59:50 +0100 Subject: [PATCH 03/11] Account for cmd running --- driver/local.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/driver/local.go b/driver/local.go index 7a1e582..8049828 100644 --- a/driver/local.go +++ b/driver/local.go @@ -34,7 +34,14 @@ func (d *Local) RunCommand(command string) (string, error) { // https://pkg.go.dev/os/exec for more information var cmd *exec.Cmd log.Debugf("Running command `%s` ", command) - cmd = exec.Command("bash", "-c", command) + if d.Info == nil { + d.GetDetails() + } + if d.Info.IsLinux || d.Info.IsDarwin { + cmd = exec.Command("bash", "-c", command) + } else { + cmd = exec.Command("cmd", "/C", command) + } cmd.Env = os.Environ() if len(d.Vars) != 0 { for _, v := range d.Vars { From 978af3be3970bce2dadbaf57e37030f9ded6f9e1 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 00:28:28 +0100 Subject: [PATCH 04/11] Fixed ProcessWin --- inspector/process.go | 7 ++++--- integration/integration_windows_test.go | 28 ++++++++++++++----------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/inspector/process.go b/inspector/process.go index 12c2744..ac0296a 100644 --- a/inspector/process.go +++ b/inspector/process.go @@ -120,10 +120,10 @@ func (i Process) createMetric(columns []string, pid int) ProcessMetrics { func (i *ProcessWin) Parse(output string) { var values []ProcessMetricsWin - lines := strings.Split(output, "\n") + lines := strings.Split(output, "\r\n") for index, line := range lines { - // skip title line - if index == 0 { + // skip title lines and ===== line + if index == 0 || index == 1 || index == 2 { continue } columns := strings.Fields(line) @@ -200,5 +200,6 @@ func NewProcess(driver *driver.Driver) Inspector { Command: `tasklist`, } } + process.SetDriver(driver) return process } diff --git a/integration/integration_windows_test.go b/integration/integration_windows_test.go index 6871f10..2a6f777 100644 --- a/integration/integration_windows_test.go +++ b/integration/integration_windows_test.go @@ -1,23 +1,27 @@ package integration import ( - "fmt" "testing" "github.com/bisohns/saido/driver" "github.com/bisohns/saido/inspector" ) -func TestTasklistonLocal(t *testing.T) { - d := driver.Local{} - i := inspector.NewTasklist() - output, err := d.RunCommand(i.String()) - if err != nil { - t.Error(err) - } - i.Parse(output) - if len(i.Values) <= 1 { - t.Error("showing 1 or less tasks/processes") +func NewLocalForTest() driver.Driver { + return &driver.Local{} +} + +func TestProcessonLocal(t *testing.T) { + d := NewLocalForTest() + i := inspector.NewProcess(&d) + i.Execute() + iConcreteWin, ok := i.(*inspector.ProcessWin) + if ok { + if len(iConcreteWin.Values) <= 2 { + t.Error("Less than two processes running") + } + if process := iConcreteWin.Values[0].Command; process != "System Idle Process" { + t.Errorf("Expected System Idle Process as first process, found %s", iConcreteWin.Values[0].Command) + } } - fmt.Printf(`%#v`, i.Values) } From 624e205c7af64009f98a6ea58968a70500bbebe9 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 00:38:58 +0100 Subject: [PATCH 05/11] Adding split chars for windows vs linux --- inspector/docker_stats.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/inspector/docker_stats.go b/inspector/docker_stats.go index da082e2..b0fd7e7 100644 --- a/inspector/docker_stats.go +++ b/inspector/docker_stats.go @@ -32,8 +32,15 @@ type DockerStats struct { // Parse : run custom parsing on output of the command func (i *DockerStats) Parse(output string) { var values []DockerStatsMetrics + var splitChars string + details := (*i.Driver).GetDetails() + if details.IsWindows { + splitChars = "\r\n" + } else { + splitChars = "\n" + } log.Debug("Parsing ouput string in DockerStats inspector") - lines := strings.Split(output, "\n") + lines := strings.Split(output, splitChars) for index, line := range lines { // skip title line if index == 0 { From dfaa340f0e0509614dc7b53a0f927242f6b775c1 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 14:53:33 +0100 Subject: [PATCH 06/11] NewInspector Interface enforces sanity across Interface initializers --- driver/ssh.go | 57 +++++++++++++------------ inspector/custom.go | 9 ++-- inspector/disk.go | 7 +-- inspector/disk_test.go | 4 +- inspector/docker_stats.go | 7 +-- inspector/inspector.go | 28 ++++++++++++ inspector/loadavg.go | 7 +-- inspector/loadavg_unix_test.go | 2 +- inspector/meminfo.go | 7 +-- inspector/meminfo_test.go | 2 +- inspector/process.go | 7 +-- inspector/rt.go | 7 +-- inspector/uptime.go | 7 +-- integration/integration_test.go | 22 +++++++--- integration/integration_unix_test.go | 14 +++--- integration/integration_windows_test.go | 2 +- 16 files changed, 118 insertions(+), 71 deletions(-) diff --git a/driver/ssh.go b/driver/ssh.go index 7642b40..46156d2 100644 --- a/driver/ssh.go +++ b/driver/ssh.go @@ -27,7 +27,8 @@ type SSH struct { // Check known hosts (only disable for tests CheckKnownHosts bool // set environmental vars for server e.g []string{"DEBUG=1", "FAKE=echo"} - Vars []string + Vars []string + SessionClient *goph.Client } func (d *SSH) String() string { @@ -36,33 +37,37 @@ func (d *SSH) String() string { // set the goph Client func (d *SSH) Client() (*goph.Client, error) { - var err error - var client *goph.Client - var callback ssh.HostKeyCallback - if d.Port != 0 { - port = d.Port - } - auth, err := goph.Key(d.KeyFile, d.KeyPass) - if err != nil { - return nil, err - } - if d.CheckKnownHosts { - callback, err = goph.DefaultKnownHosts() + if d.SessionClient == nil { + var err error + var client *goph.Client + var callback ssh.HostKeyCallback + if d.Port != 0 { + port = d.Port + } + auth, err := goph.Key(d.KeyFile, d.KeyPass) if err != nil { return nil, err } - } else { - callback = ssh.InsecureIgnoreHostKey() + if d.CheckKnownHosts { + callback, err = goph.DefaultKnownHosts() + if err != nil { + return nil, err + } + } else { + callback = ssh.InsecureIgnoreHostKey() + } + client, err = goph.NewConn(&goph.Config{ + User: d.User, + Addr: d.Host, + Port: uint(port), + Auth: auth, + Timeout: goph.DefaultTimeout, + Callback: callback, + }) + d.SessionClient = client + return client, err } - client, err = goph.NewConn(&goph.Config{ - User: d.User, - Addr: d.Host, - Port: uint(port), - Auth: auth, - Timeout: goph.DefaultTimeout, - Callback: callback, - }) - return client, err + return d.SessionClient, nil } func (d *SSH) ReadFile(path string) (string, error) { @@ -72,7 +77,7 @@ func (d *SSH) ReadFile(path string) (string, error) { } func (d *SSH) RunCommand(command string) (string, error) { - // FIXME: Do we retain client across all command runs? + // FIXME: provide refresh interface to somehow refresh d.Client if d.SessionClient somehow gets dropped log.Debugf("Running remote command %s", command) client, err := d.Client() if err != nil { @@ -112,8 +117,6 @@ func (d *SSH) GetDetails() SystemDetails { details.IsLinux = true case "darwin": details.IsDarwin = true - default: - details.IsLinux = true } d.Info = details } diff --git a/inspector/custom.go b/inspector/custom.go index 40aa8f4..35966ba 100644 --- a/inspector/custom.go +++ b/inspector/custom.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "fmt" "github.com/bisohns/saido/driver" @@ -51,15 +52,15 @@ func (i *Custom) Execute() { } // NewCustom : Initialize a new Custom instance -func NewCustom(custom string, driver *driver.Driver) Inspector { +func NewCustom(driver *driver.Driver, custom ...string) (Inspector, error) { var customInspector Inspector details := (*driver).GetDetails() if details.IsWeb { - panic(fmt.Sprintf("Cannot use Custom(%s) on web", custom)) + return nil, errors.New(fmt.Sprintf("Cannot use Custom(%s) on web", custom)) } customInspector = &Custom{ - Command: custom, + Command: custom[0], } customInspector.SetDriver(driver) - return customInspector + return customInspector, nil } diff --git a/inspector/disk.go b/inspector/disk.go index 9266816..6acd5c7 100644 --- a/inspector/disk.go +++ b/inspector/disk.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "strconv" "strings" @@ -97,11 +98,11 @@ type WMIC struct { } // NewDF : Initialize a new DF instance -func NewDF(driver *driver.Driver) Inspector { +func NewDF(driver *driver.Driver, _ ...string) (Inspector, error) { var df Inspector details := (*driver).GetDetails() if !(details.IsLinux || details.IsDarwin || details.IsWindows) { - panic("Cannot use 'df' command on drivers outside (linux, darwin, windows)") + return nil, errors.New("Cannot use 'df' command on drivers outside (linux, darwin, windows)") } if details.IsLinux || details.IsDarwin { df = &DF{ @@ -114,5 +115,5 @@ func NewDF(driver *driver.Driver) Inspector { } } df.SetDriver(driver) - return df + return df, nil } diff --git a/inspector/disk_test.go b/inspector/disk_test.go index 13de455..7fb6b12 100644 --- a/inspector/disk_test.go +++ b/inspector/disk_test.go @@ -21,7 +21,7 @@ func NewSSHForTest() driver.Driver { func TestDFOnLocal(t *testing.T) { driver := NewLocalForTest() - d := NewDF(&driver) + d, _ := NewDF(&driver) d.Execute() dfConcrete, _ := d.(*DF) if len(dfConcrete.Values) == 0 { @@ -31,7 +31,7 @@ func TestDFOnLocal(t *testing.T) { func TestDFOnSSH(t *testing.T) { driver := NewSSHForTest() - d := NewDF(&driver) + d, _ := NewDF(&driver) d.Execute() dfConcrete, _ := d.(*DF) if len(dfConcrete.Values) == 0 { diff --git a/inspector/docker_stats.go b/inspector/docker_stats.go index b0fd7e7..8449631 100644 --- a/inspector/docker_stats.go +++ b/inspector/docker_stats.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "strconv" "strings" @@ -109,15 +110,15 @@ func (i *DockerStats) Execute() { } // NewDockerStats : Initialize a new DockerStats instance -func NewDockerStats(driver *driver.Driver) Inspector { +func NewDockerStats(driver *driver.Driver, _ ...string) (Inspector, error) { var dockerstats Inspector details := (*driver).GetDetails() if !(details.IsLinux || details.IsDarwin || details.IsWindows) { - panic("Cannot use LoadAvgDarwin on drivers outside (linux, darwin, windows)") + return nil, errors.New("Cannot use LoadAvgDarwin on drivers outside (linux, darwin, windows)") } dockerstats = &DockerStats{ Command: `docker stats --no-stream`, } dockerstats.SetDriver(driver) - return dockerstats + return dockerstats, nil } diff --git a/inspector/inspector.go b/inspector/inspector.go index 956dd52..3736cda 100644 --- a/inspector/inspector.go +++ b/inspector/inspector.go @@ -1,6 +1,9 @@ package inspector import ( + "errors" + "fmt" + "github.com/bisohns/saido/driver" ) @@ -11,3 +14,28 @@ type Inspector interface { Execute() driverExec() driver.Command } + +type NewInspector func(driver *driver.Driver, custom ...string) (Inspector, error) + +var inspectorMap = map[string]NewInspector{ + `disk`: NewDF, + `docker`: NewDockerStats, + `uptime`: NewUptime, + `responsetime`: NewResponseTime, + `memory`: NewMemInfo, + `process`: NewProcess, + `custom`: NewCustom, +} + +// Init : initializes the specified inspector using name and driver +func Init(name string, driver *driver.Driver, custom ...string) (Inspector, error) { + val, ok := inspectorMap[name] + if ok { + inspector, err := val(driver, custom...) + if err != nil { + return nil, err + } + return inspector, nil + } + return nil, errors.New(fmt.Sprintf("Cannot find inspector with name %s", name)) +} diff --git a/inspector/loadavg.go b/inspector/loadavg.go index dd31ab4..f43e93d 100644 --- a/inspector/loadavg.go +++ b/inspector/loadavg.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "strconv" "strings" @@ -98,11 +99,11 @@ func (i *LoadAvg) Execute() { // of LoadAvg // NewLoadAvg : Initialize a new LoadAvg instance -func NewLoadAvg(driver *driver.Driver) Inspector { +func NewLoadAvg(driver *driver.Driver, _ ...string) (Inspector, error) { var loadavg Inspector details := (*driver).GetDetails() if !(details.IsLinux || details.IsDarwin) { - panic("Cannot use LoadAvg on drivers outside (linux, darwin)") + return nil, errors.New("Cannot use LoadAvg on drivers outside (linux, darwin)") } if details.IsLinux { loadavg = &LoadAvg{ @@ -115,6 +116,6 @@ func NewLoadAvg(driver *driver.Driver) Inspector { } } loadavg.SetDriver(driver) - return loadavg + return loadavg, nil } diff --git a/inspector/loadavg_unix_test.go b/inspector/loadavg_unix_test.go index e2395ac..5fdd8dd 100644 --- a/inspector/loadavg_unix_test.go +++ b/inspector/loadavg_unix_test.go @@ -14,7 +14,7 @@ func NewLocalForTest() driver.Driver { func TestLoadAvg(t *testing.T) { testDriver := NewLocalForTest() - loadavg := NewLoadAvg(&testDriver) + loadavg, _ := NewLoadAvg(&testDriver) loadavg.Execute() loadavgConcreteLinux, ok := loadavg.(*LoadAvg) if ok { diff --git a/inspector/meminfo.go b/inspector/meminfo.go index 999280f..f81ee41 100644 --- a/inspector/meminfo.go +++ b/inspector/meminfo.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "fmt" "strconv" "strings" @@ -166,11 +167,11 @@ func (i *MemInfoDarwin) Execute() { // TODO: Windows Equivalents of MemInfo // NewMemInfoLinux : Initialize a new MemInfoLinux instance -func NewMemInfo(driver *driver.Driver) Inspector { +func NewMemInfo(driver *driver.Driver, _ ...string) (Inspector, error) { var meminfo Inspector details := (*driver).GetDetails() if !(details.IsLinux || details.IsDarwin) { - panic("Cannot use MemInfo on drivers outside (linux, darwin)") + return nil, errors.New("Cannot use MemInfo on drivers outside (linux, darwin)") } if details.IsLinux { meminfo = &MemInfoLinux{ @@ -187,5 +188,5 @@ func NewMemInfo(driver *driver.Driver) Inspector { } } meminfo.SetDriver(driver) - return meminfo + return meminfo, nil } diff --git a/inspector/meminfo_test.go b/inspector/meminfo_test.go index 3407de8..d1355eb 100644 --- a/inspector/meminfo_test.go +++ b/inspector/meminfo_test.go @@ -8,7 +8,7 @@ import ( func TestMemInfoOnLocal(t *testing.T) { driver := NewLocalForTest() - d := NewMemInfo(&driver) + d, _ := NewMemInfo(&driver) d.Execute() iConcreteLinux, ok := d.(*MemInfoLinux) if ok { diff --git a/inspector/process.go b/inspector/process.go index ac0296a..3c1bae6 100644 --- a/inspector/process.go +++ b/inspector/process.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "strconv" "strings" @@ -185,11 +186,11 @@ func (i *ProcessWin) Execute() { } // NewProcess : Initialize a new Process instance -func NewProcess(driver *driver.Driver) Inspector { +func NewProcess(driver *driver.Driver, _ ...string) (Inspector, error) { var process Inspector details := (*driver).GetDetails() if !(details.IsLinux || details.IsDarwin || details.IsWindows) { - panic("Cannot use Process on drivers outside (linux, darwin, windows)") + return nil, errors.New("Cannot use Process on drivers outside (linux, darwin, windows)") } if details.IsLinux || details.IsDarwin { process = &Process{ @@ -201,5 +202,5 @@ func NewProcess(driver *driver.Driver) Inspector { } } process.SetDriver(driver) - return process + return process, nil } diff --git a/inspector/rt.go b/inspector/rt.go index 24c0532..9445bdb 100644 --- a/inspector/rt.go +++ b/inspector/rt.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "strconv" "github.com/bisohns/saido/driver" @@ -49,15 +50,15 @@ func (i *ResponseTime) Execute() { } // NewResponseTime : Initialize a new ResponseTime instance -func NewResponseTime(driver *driver.Driver) Inspector { +func NewResponseTime(driver *driver.Driver, _ ...string) (Inspector, error) { var responsetime Inspector details := (*driver).GetDetails() if !(details.IsWeb) { - panic("Cannot use response time outside driver (web)") + return nil, errors.New("Cannot use response time outside driver (web)") } responsetime = &ResponseTime{ Command: `response`, } responsetime.SetDriver(driver) - return responsetime + return responsetime, nil } diff --git a/inspector/uptime.go b/inspector/uptime.go index 90eadea..07dc8ef 100644 --- a/inspector/uptime.go +++ b/inspector/uptime.go @@ -1,6 +1,7 @@ package inspector import ( + "errors" "fmt" "strconv" "strings" @@ -115,11 +116,11 @@ func (i *UptimeDarwin) Execute() { //TODO: Windows equivalent of uptime // NewUptime : Initialize a new Uptime instance -func NewUptime(driver *driver.Driver) Inspector { +func NewUptime(driver *driver.Driver, _ ...string) (Inspector, error) { var uptime Inspector details := (*driver).GetDetails() if !(details.IsDarwin || details.IsLinux) { - panic("Cannot use Uptime on drivers outside (linux, darwin)") + return nil, errors.New("Cannot use Uptime on drivers outside (linux, darwin)") } if details.IsLinux { uptime = &UptimeLinux{ @@ -132,5 +133,5 @@ func NewUptime(driver *driver.Driver) Inspector { } } uptime.SetDriver(driver) - return uptime + return uptime, nil } diff --git a/integration/integration_test.go b/integration/integration_test.go index c0d0dc1..15b9077 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -29,7 +29,7 @@ func NewSSHForTest() driver.Driver { func TestDFonSSH(t *testing.T) { d := NewSSHForTest() - i := inspector.NewDF(&d) + i, _ := inspector.Init(`disk`, &d) i.Execute() iConcrete, ok := i.(*inspector.DF) if ok { @@ -39,7 +39,7 @@ func TestDFonSSH(t *testing.T) { func TestMemInfoonSSH(t *testing.T) { d := NewSSHForTest() - i := inspector.NewMemInfo(&d) + i, _ := inspector.NewMemInfo(&d) i.Execute() iConcreteLinux, ok := i.(*inspector.MemInfoLinux) if ok { @@ -59,7 +59,7 @@ func TestMemInfoonSSH(t *testing.T) { func TestResponseTimeonWeb(t *testing.T) { d := NewWebForTest() - i := inspector.NewResponseTime(&d) + i, _ := inspector.NewResponseTime(&d) i.Execute() iConcrete, ok := i.(*inspector.ResponseTime) if ok { @@ -72,7 +72,7 @@ func TestResponseTimeonWeb(t *testing.T) { func TestProcessonSSH(t *testing.T) { d := NewSSHForTest() - i := inspector.NewProcess(&d) + i, _ := inspector.NewProcess(&d) i.Execute() iConcreteUnix, ok := i.(*inspector.Process) if ok { @@ -94,7 +94,7 @@ func TestCustomonSSH(t *testing.T) { dfConcrete, _ := d.(*driver.SSH) dfConcrete.Vars = []string{"MONKEY=true"} d = dfConcrete - i := inspector.NewCustom(`echo $MONKEY`, &d) + i, _ := inspector.NewCustom(&d, `echo $MONKEY`) i.Execute() iConcrete, ok := i.(*inspector.Custom) if ok { @@ -106,7 +106,7 @@ func TestCustomonSSH(t *testing.T) { func TestLoadAvgonSSH(t *testing.T) { d := NewSSHForTest() - i := inspector.NewLoadAvg(&d) + i, _ := inspector.NewLoadAvg(&d) i.Execute() iConcreteLinux, ok := i.(*inspector.LoadAvg) if ok { @@ -122,9 +122,17 @@ func TestLoadAvgonSSH(t *testing.T) { } } +func TestCustomonWeb(t *testing.T) { + d := NewWebForTest() + _, err := inspector.Init(`custom`, &d, `custom-command`) + if err == nil { + t.Error("should not instantiate custom on web") + } +} + func TestUptimeonSSH(t *testing.T) { d := NewSSHForTest() - i := inspector.NewUptime(&d) + i, _ := inspector.NewUptime(&d) i.Execute() iConcreteLinux, ok := i.(*inspector.UptimeLinux) if ok { diff --git a/integration/integration_unix_test.go b/integration/integration_unix_test.go index da4350e..291df64 100644 --- a/integration/integration_unix_test.go +++ b/integration/integration_unix_test.go @@ -19,7 +19,7 @@ func NewLocalForTest() driver.Driver { func TestDFonLocal(t *testing.T) { d := NewLocalForTest() // can either use NewDF() or get the interface and perform type assertion - i := inspector.NewDF(&d) + i, _ := inspector.NewDF(&d) i.Execute() iConcrete, _ := i.(*inspector.DF) if iConcrete.Values[0].Used == 0 { @@ -30,7 +30,7 @@ func TestDFonLocal(t *testing.T) { func TestMemInfoonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewMemInfo(&d) + i, _ := inspector.NewMemInfo(&d) i.Execute() iConcreteLinux, ok := i.(*inspector.MemInfoLinux) if ok { @@ -50,7 +50,7 @@ func TestMemInfoonLocal(t *testing.T) { func TestDockerStatsonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewDockerStats(&d) + i, _ := inspector.NewDockerStats(&d) i.Execute() iConcrete, _ := i.(*inspector.DockerStats) if len(iConcrete.Values) == 0 { @@ -61,7 +61,7 @@ func TestDockerStatsonLocal(t *testing.T) { func TestProcessonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewProcess(&d) + i, _ := inspector.NewProcess(&d) i.Execute() iConcreteUnix, ok := i.(*inspector.Process) if ok { @@ -107,7 +107,7 @@ func TestProcessonLocal(t *testing.T) { func TestCustomonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewCustom(`echo /test/test`, &d) + i, _ := inspector.NewCustom(&d, `echo /test/test`) i.Execute() iConcrete, _ := i.(*inspector.Custom) if strings.TrimSpace(iConcrete.Values.Output) != "/test/test" { @@ -117,7 +117,7 @@ func TestCustomonLocal(t *testing.T) { func TestLoadAvgonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewLoadAvg(&d) + i, _ := inspector.NewLoadAvg(&d) i.Execute() iConcreteDarwin, ok := i.(*inspector.LoadAvgDarwin) if ok { @@ -137,7 +137,7 @@ func TestLoadAvgonLocal(t *testing.T) { func TestUptimeonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewUptime(&d) + i, _ := inspector.NewUptime(&d) i.Execute() iConcreteLinux, ok := i.(*inspector.UptimeLinux) if ok { diff --git a/integration/integration_windows_test.go b/integration/integration_windows_test.go index 2a6f777..d13a582 100644 --- a/integration/integration_windows_test.go +++ b/integration/integration_windows_test.go @@ -13,7 +13,7 @@ func NewLocalForTest() driver.Driver { func TestProcessonLocal(t *testing.T) { d := NewLocalForTest() - i := inspector.NewProcess(&d) + i, _ := inspector.NewProcess(&d) i.Execute() iConcreteWin, ok := i.(*inspector.ProcessWin) if ok { From 2bd0f6c852b5b16686b5163b726c3d5dad211449 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 20:02:05 +0100 Subject: [PATCH 07/11] Fixing web driver error --- driver/web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/web.go b/driver/web.go index 2f6e339..1f695fa 100644 --- a/driver/web.go +++ b/driver/web.go @@ -53,7 +53,7 @@ func (d *Web) RunCommand(command string) (string, error) { res, err = http.Get(d.URL) } if err != nil || res.StatusCode < 200 || res.StatusCode > 299 { - message := fmt.Sprintf("Error %s running request: %s", err, string(res.StatusCode)) + message := fmt.Sprintf("Error %s running request: %d", err, res.StatusCode) return ``, errors.New(message) } elapsed := time.Since(start) From 590509180764d4d742c902e913be8d9f03b39dc2 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 20:05:53 +0100 Subject: [PATCH 08/11] adding NewLoadAvg to map --- inspector/inspector.go | 1 + 1 file changed, 1 insertion(+) diff --git a/inspector/inspector.go b/inspector/inspector.go index 3736cda..c393853 100644 --- a/inspector/inspector.go +++ b/inspector/inspector.go @@ -25,6 +25,7 @@ var inspectorMap = map[string]NewInspector{ `memory`: NewMemInfo, `process`: NewProcess, `custom`: NewCustom, + `loadavg`: NewLoadAvg, } // Init : initializes the specified inspector using name and driver From 654167a32db403a09a1379158a0e2bea91e8f400 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 19:28:03 +0100 Subject: [PATCH 09/11] Adding customlocaltest on windows --- integration/integration_windows_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/integration/integration_windows_test.go b/integration/integration_windows_test.go index d13a582..39b784b 100644 --- a/integration/integration_windows_test.go +++ b/integration/integration_windows_test.go @@ -1,6 +1,7 @@ package integration import ( + "strings" "testing" "github.com/bisohns/saido/driver" @@ -25,3 +26,18 @@ func TestProcessonLocal(t *testing.T) { } } } + +func TestCustomonLocal(t *testing.T) { + d := NewLocalForTest() + dfConcrete, _ := d.(*driver.Local) + dfConcrete.Vars = []string{"EXAMPLES=true"} + d = dfConcrete + i, _ := inspector.Init(`custom`, &d, `echo %EXAMPLES%`) + i.Execute() + iConcrete, ok := i.(*inspector.Custom) + if ok { + if strings.TrimSpace(iConcrete.Values.Output) != "true" { + t.Errorf("Expected 'true', found %s", iConcrete.Values.Output) + } + } +} From 935de096cf21cb1fe3afabed879ecd4bb5f9ec5e Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 21:50:50 +0100 Subject: [PATCH 10/11] Modified DF to support Windows --- inspector/disk.go | 95 +++++++++++++++++++++++-- integration/integration_windows_test.go | 14 ++++ 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/inspector/disk.go b/inspector/disk.go index 6acd5c7..85c7a3e 100644 --- a/inspector/disk.go +++ b/inspector/disk.go @@ -2,6 +2,7 @@ package inspector import ( "errors" + "fmt" "strconv" "strings" @@ -11,10 +12,13 @@ import ( // DFMetrics : Metrics used by DF type DFMetrics struct { + FileSystem string Size float64 Used float64 Available float64 PercentFull int + // Optional Volume Name that may be available on Windows + VolumeName string } // DF : Parsing the `df` output for disk monitoring @@ -27,8 +31,6 @@ type DF struct { DisplayByteSize string // Parse only device that start with this e.g /dev/sd DeviceStartsWith string - // Mount point to examine - MountPoint string // Values of metrics being read Values []DFMetrics } @@ -55,10 +57,9 @@ func (i *DF) Parse(output string) { if err != nil { log.Fatalf(`Error Parsing Percent Full: %s `, err) } - if columns[len(columns)-1] == i.MountPoint { + if strings.HasPrefix(columns[0], i.DeviceStartsWith) { values = append(values, i.createMetric(columns, percentInt)) - } else if strings.HasPrefix(columns[0], i.DeviceStartsWith) && - i.MountPoint == "" { + } else { values = append(values, i.createMetric(columns, percentInt)) } } @@ -68,6 +69,7 @@ func (i *DF) Parse(output string) { func (i DF) createMetric(columns []string, percent int) DFMetrics { return DFMetrics{ + FileSystem: columns[0], Size: NewByteSize(columns[1], i.RawByteSize).format(i.DisplayByteSize), Used: NewByteSize(columns[2], i.RawByteSize).format(i.DisplayByteSize), Available: NewByteSize(columns[3], i.RawByteSize).format(i.DisplayByteSize), @@ -92,9 +94,81 @@ func (i *DF) Execute() { // TODO: Implement DF for windows using // `wmic logicaldisk` to satisfy Inspector interface -type WMIC struct { +type DFWin struct { Driver *driver.Driver Command string + // The values read from the command output string are defaultly in KB + RawByteSize string + // We want do display disk values in GB + DisplayByteSize string + // Parse only device that start with this e.g /dev/sd + DeviceStartsWith string + // Values of metrics being read + Values []DFMetrics +} + +func (i *DFWin) Parse(output string) { + var values []DFMetrics + log.Debug("Parsing ouput string in DF inspector") + lineChar := "\r" + output = strings.TrimPrefix(output, lineChar) + output = strings.TrimSuffix(output, lineChar) + lines := strings.Split(output, lineChar) + for index, line := range lines { + // skip title line + if index == 0 || index == 1 { + continue + } + columns := strings.Split(line, ",") + if len(columns) >= 7 { + available, err := strconv.Atoi(columns[3]) + size, err := strconv.Atoi(columns[5]) + if err != nil { + panic("Could not parse sizes for DFWin") + } + used := size - available + percentInt := int((float64(used) / float64(size)) * 100) + cols := []string{ + columns[1], + fmt.Sprintf("%d", size), + fmt.Sprintf("%d", used), + fmt.Sprintf("%d", available), + columns[6], + } + if strings.HasPrefix(columns[1], i.DeviceStartsWith) { + values = append(values, i.createMetric(cols, percentInt)) + } else { + values = append(values, i.createMetric(cols, percentInt)) + } + } + } + i.Values = values +} + +func (i DFWin) createMetric(columns []string, percent int) DFMetrics { + return DFMetrics{ + FileSystem: columns[0], + Size: NewByteSize(columns[1], i.RawByteSize).format(i.DisplayByteSize), + Used: NewByteSize(columns[2], i.RawByteSize).format(i.DisplayByteSize), + Available: NewByteSize(columns[3], i.RawByteSize).format(i.DisplayByteSize), + VolumeName: columns[4], + PercentFull: percent, + } +} + +func (i *DFWin) SetDriver(driver *driver.Driver) { + i.Driver = driver +} + +func (i DFWin) driverExec() driver.Command { + return (*i.Driver).RunCommand +} + +func (i *DFWin) Execute() { + output, err := i.driverExec()(i.Command) + if err == nil { + i.Parse(output) + } } // NewDF : Initialize a new DF instance @@ -111,7 +185,14 @@ func NewDF(driver *driver.Driver, _ ...string) (Inspector, error) { Command: `df -a -k`, RawByteSize: `KB`, DisplayByteSize: `GB`, - MountPoint: `/`, + } + } else { + df = &DFWin{ + // Using format to account for weird spacing + // issues that arise on windows + Command: `wmic logicaldisk list brief /format:csv`, + RawByteSize: `B`, + DisplayByteSize: `GB`, } } df.SetDriver(driver) diff --git a/integration/integration_windows_test.go b/integration/integration_windows_test.go index 39b784b..fedde05 100644 --- a/integration/integration_windows_test.go +++ b/integration/integration_windows_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "strings" "testing" @@ -41,3 +42,16 @@ func TestCustomonLocal(t *testing.T) { } } } + +func TestDFonLocal(t *testing.T) { + d := NewLocalForTest() + i, _ := inspector.Init(`disk`, &d) + i.Execute() + iConcrete, ok := i.(*inspector.DFWin) + if ok { + fmt.Printf("%#v", iConcrete.Values) + if len(iConcrete.Values) < 1 { + t.Error("DFWin not showing at least one drive") + } + } +} From 0a1aaa4c4ac6d364e9d2f360192998cba06d36b7 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Thu, 17 Mar 2022 22:04:20 +0100 Subject: [PATCH 11/11] Fixed most review comments --- driver/driver.go | 6 +----- driver/local.go | 8 ++++---- driver/ssh.go | 16 +++++++++------- driver/ssh_test.go | 2 +- driver/web.go | 2 +- driver/web_test.go | 2 +- integration/integration_windows_test.go | 2 +- 7 files changed, 18 insertions(+), 20 deletions(-) diff --git a/driver/driver.go b/driver/driver.go index 1dbc5a3..bef464b 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -10,11 +10,7 @@ type SystemDetails struct { Extra string } -type fields struct { - // Supported inspector representations for specific driver - Supported []string - // Selected inspector representations - Selected []string +type driverBase struct { // Polling interval between retrievals PollInterval int64 Info *SystemDetails diff --git a/driver/local.go b/driver/local.go index 8049828..d63585e 100644 --- a/driver/local.go +++ b/driver/local.go @@ -11,8 +11,8 @@ import ( // Local : Driver for handling local executions type Local struct { - fields - Vars []string + driverBase + EnvVars []string } func (d *Local) ReadFile(path string) (string, error) { @@ -43,8 +43,8 @@ func (d *Local) RunCommand(command string) (string, error) { cmd = exec.Command("cmd", "/C", command) } cmd.Env = os.Environ() - if len(d.Vars) != 0 { - for _, v := range d.Vars { + if len(d.EnvVars) != 0 { + for _, v := range d.EnvVars { cmd.Env = append(cmd.Env, v) } } diff --git a/driver/ssh.go b/driver/ssh.go index 46156d2..7bba21d 100644 --- a/driver/ssh.go +++ b/driver/ssh.go @@ -13,7 +13,7 @@ var port = 22 // SSH : Driver for handling ssh executions type SSH struct { - fields + driverBase // User e.g root User string // Host name/ip e.g 171.23.122.1 @@ -27,7 +27,7 @@ type SSH struct { // Check known hosts (only disable for tests CheckKnownHosts bool // set environmental vars for server e.g []string{"DEBUG=1", "FAKE=echo"} - Vars []string + EnvVars []string SessionClient *goph.Client } @@ -64,7 +64,9 @@ func (d *SSH) Client() (*goph.Client, error) { Timeout: goph.DefaultTimeout, Callback: callback, }) - d.SessionClient = client + if err != nil { + d.SessionClient = client + } return client, err } return d.SessionClient, nil @@ -77,16 +79,16 @@ func (d *SSH) ReadFile(path string) (string, error) { } func (d *SSH) RunCommand(command string) (string, error) { - // FIXME: provide refresh interface to somehow refresh d.Client if d.SessionClient somehow gets dropped + // TODO: Ensure clients of all SSH drivers are closed on context end + // i.e d.SessionClient.Close() log.Debugf("Running remote command %s", command) client, err := d.Client() if err != nil { return ``, err } - defer client.Close() - if len(d.Vars) != 0 { + if len(d.EnvVars) != 0 { // add env variable to command - envline := strings.Join(d.Vars, ";") + envline := strings.Join(d.EnvVars, ";") command = strings.Join([]string{envline, command}, ";") } out, err := client.Run(command) diff --git a/driver/ssh_test.go b/driver/ssh_test.go index ae19ad3..a8a8378 100644 --- a/driver/ssh_test.go +++ b/driver/ssh_test.go @@ -13,7 +13,7 @@ func NewSSHForTest() Driver { KeyFile: "/home/diretnan/.ssh/id_rsa", KeyPass: "", CheckKnownHosts: false, - fields: fields{ + driverBase: driverBase{ PollInterval: 5, }, } diff --git a/driver/web.go b/driver/web.go index 1f695fa..e701378 100644 --- a/driver/web.go +++ b/driver/web.go @@ -23,7 +23,7 @@ const ( // Web : Driver for handling web executions type Web struct { - fields + driverBase // URL e.g https://google.com URL string // Method POST/GET diff --git a/driver/web_test.go b/driver/web_test.go index ec01b5b..e3cecfb 100644 --- a/driver/web_test.go +++ b/driver/web_test.go @@ -8,7 +8,7 @@ func NewWebForTest() *Web { return &Web{ URL: "https://duckduckgo.com", Method: GET, - fields: fields{ + driverBase: driverBase{ PollInterval: 5, }, } diff --git a/integration/integration_windows_test.go b/integration/integration_windows_test.go index fedde05..38ab5e4 100644 --- a/integration/integration_windows_test.go +++ b/integration/integration_windows_test.go @@ -31,7 +31,7 @@ func TestProcessonLocal(t *testing.T) { func TestCustomonLocal(t *testing.T) { d := NewLocalForTest() dfConcrete, _ := d.(*driver.Local) - dfConcrete.Vars = []string{"EXAMPLES=true"} + dfConcrete.EnvVars = []string{"EXAMPLES=true"} d = dfConcrete i, _ := inspector.Init(`custom`, &d, `echo %EXAMPLES%`) i.Execute()