Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pull request on adding missing statistic under solaris/illumos #1381

Merged
merged 2 commits into from Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 116 additions & 3 deletions disk/disk_solaris.go
Expand Up @@ -10,6 +10,10 @@ import (
"fmt"
"math"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"

"github.com/shirou/gopsutil/v3/internal/common"
Expand Down Expand Up @@ -73,20 +77,129 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro
})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("unable to scan %q: %v", _MNTTAB, err)
return nil, fmt.Errorf("unable to scan %q: %w", _MNTTAB, err)
}

return ret, err
}

func IOCountersWithContext(ctx context.Context, names ...string) (map[string]IOCountersStat, error) {
return nil, common.ErrNotImplementedError
var issolaris bool
if runtime.GOOS == "illumos" {
issolaris = false
} else {
issolaris = true
}
// check disks instead of zfs pools
filterstr := "/[^zfs]/:::/^nread$|^nwritten$|^reads$|^writes$|^rtime$|^wtime$/"
kstatSysOut, err := invoke.CommandWithContext(ctx, "kstat", "-c", "disk", "-p", filterstr)
if err != nil {
return nil, fmt.Errorf("cannot execute kstat: %w", err)
}
lines := strings.Split(strings.TrimSpace(string(kstatSysOut)), "\n")
if len(lines) == 0 {
return nil, fmt.Errorf("no disk class found")
}
dnamearr := make(map[string]string)
nreadarr := make(map[string]uint64)
nwrittenarr := make(map[string]uint64)
readsarr := make(map[string]uint64)
writesarr := make(map[string]uint64)
rtimearr := make(map[string]uint64)
wtimearr := make(map[string]uint64)
re := regexp.MustCompile(`[:\s]+`)

// in case the name is "/dev/sda1", then convert to "sda1"
for i, name := range names {
names[i] = filepath.Base(name)
}

for _, line := range lines {
fields := re.Split(line, -1)
if len(fields) == 0 {
continue
}
moduleName := fields[0]
instance := fields[1]
dname := fields[2]

if len(names) > 0 && !common.StringsHas(names, dname) {
continue
}
dnamearr[moduleName+instance] = dname
// fields[3] is the statistic label, fields[4] is the value
switch fields[3] {
case "nread":
nreadarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64)
if err != nil {
return nil, err
}
case "nwritten":
nwrittenarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64)
if err != nil {
return nil, err
}
case "reads":
readsarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64)
if err != nil {
return nil, err
}
case "writes":
writesarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64)
if err != nil {
return nil, err
}
case "rtime":
if issolaris {
// from sec to milli secs
var frtime float64
frtime, err = strconv.ParseFloat((fields[4]), 64)
rtimearr[moduleName+instance] = uint64(frtime * 1000)
} else {
// from nano to milli secs
rtimearr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64)
rtimearr[moduleName+instance] = rtimearr[moduleName+instance] / 1000 / 1000
}
if err != nil {
return nil, err
}
case "wtime":
if issolaris {
// from sec to milli secs
var fwtime float64
fwtime, err = strconv.ParseFloat((fields[4]), 64)
wtimearr[moduleName+instance] = uint64(fwtime * 1000)
} else {
// from nano to milli secs
wtimearr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64)
wtimearr[moduleName+instance] = wtimearr[moduleName+instance] / 1000 / 1000
}
if err != nil {
return nil, err
}
}
}

ret := make(map[string]IOCountersStat, 0)
for k := range dnamearr {
d := IOCountersStat{
Name: dnamearr[k],
ReadBytes: nreadarr[k],
WriteBytes: nwrittenarr[k],
ReadCount: readsarr[k],
WriteCount: writesarr[k],
ReadTime: rtimearr[k],
WriteTime: wtimearr[k],
}
ret[d.Name] = d
}
return ret, nil
}

func UsageWithContext(ctx context.Context, path string) (*UsageStat, error) {
statvfs := unix.Statvfs_t{}
if err := unix.Statvfs(path, &statvfs); err != nil {
return nil, fmt.Errorf("unable to call statvfs(2) on %q: %v", path, err)
return nil, fmt.Errorf("unable to call statvfs(2) on %q: %w", path, err)
}

usageStat := &UsageStat{
Expand Down
27 changes: 27 additions & 0 deletions mem/mem_solaris.go
Expand Up @@ -12,6 +12,7 @@ import (
"strings"

"github.com/shirou/gopsutil/v3/internal/common"
"github.com/tklauser/go-sysconf"
)

// VirtualMemory for Solaris is a minimal implementation which only returns
Expand All @@ -34,6 +35,13 @@ func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) {
return nil, err
}
result.Total = cap
freemem, err := globalZoneFreeMemory(ctx)
if err != nil {
return nil, err
}
result.Available = freemem
result.Free = freemem
result.Used = result.Total - result.Free
} else {
cap, err := nonGlobalZoneMemoryCapacity()
if err != nil {
Expand Down Expand Up @@ -85,6 +93,25 @@ func globalZoneMemoryCapacity() (uint64, error) {
return totalMB * 1024 * 1024, nil
}

func globalZoneFreeMemory(ctx context.Context) (uint64, error) {
output, err := invoke.CommandWithContext(ctx, "pagesize")
if err != nil {
return 0, err
}

pagesize, err := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 64)
if err != nil {
return 0, err
}

free, err := sysconf.Sysconf(sysconf.SC_AVPHYS_PAGES)
if err != nil {
return 0, err
}

return uint64(free) * pagesize, nil
}

var kstatMatch = regexp.MustCompile(`(\S+)\s+(\S*)`)

func nonGlobalZoneMemoryCapacity() (uint64, error) {
Expand Down
4 changes: 2 additions & 2 deletions mem/mem_test.go
Expand Up @@ -17,8 +17,8 @@ func skipIfNotImplementedErr(t *testing.T, err error) {
}

func TestVirtual_memory(t *testing.T) {
if runtime.GOOS == "solaris" {
t.Skip("Only .Total is supported on Solaris")
if runtime.GOOS == "solaris" || runtime.GOOS == "illumos" {
t.Skip("Only .Total .Available are supported on Solaris/illumos")
}

v, err := VirtualMemory()
Expand Down
4 changes: 2 additions & 2 deletions net/net_fallback.go
@@ -1,5 +1,5 @@
//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows
// +build !aix,!darwin,!linux,!freebsd,!openbsd,!windows
//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows && !solaris
// +build !aix,!darwin,!linux,!freebsd,!openbsd,!windows,!solaris

package net

Expand Down
143 changes: 143 additions & 0 deletions net/net_solaris.go
@@ -0,0 +1,143 @@
//go:build solaris
// +build solaris

package net

import (
"context"
"fmt"
"regexp"
"runtime"
"strconv"
"strings"

"github.com/shirou/gopsutil/v3/internal/common"
)

// NetIOCounters returnes network I/O statistics for every network
// interface installed on the system. If pernic argument is false,
// return only sum of all information (which name is 'all'). If true,
// every network interface installed on the system is returned
// separately.
func IOCounters(pernic bool) ([]IOCountersStat, error) {
return IOCountersWithContext(context.Background(), pernic)
}

func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) {
// collect all the net class's links with below statistics
filterstr := "/^(?!vnic)/::phys:/^rbytes64$|^ipackets64$|^idrops64$|^ierrors$|^obytes64$|^opackets64$|^odrops64$|^oerrors$/"
if runtime.GOOS == "illumos" {
filterstr = "/[^vnic]/::mac:/^rbytes64$|^ipackets64$|^idrops64$|^ierrors$|^obytes64$|^opackets64$|^odrops64$|^oerrors$/"
}
kstatSysOut, err := invoke.CommandWithContext(ctx, "kstat", "-c", "net", "-p", filterstr)
if err != nil {
return nil, fmt.Errorf("cannot execute kstat: %w", err)
}

lines := strings.Split(strings.TrimSpace(string(kstatSysOut)), "\n")
if len(lines) == 0 {
return nil, fmt.Errorf("no interface found")
}
rbytes64arr := make(map[string]uint64)
ipackets64arr := make(map[string]uint64)
idrops64arr := make(map[string]uint64)
ierrorsarr := make(map[string]uint64)
obytes64arr := make(map[string]uint64)
opackets64arr := make(map[string]uint64)
odrops64arr := make(map[string]uint64)
oerrorsarr := make(map[string]uint64)

re := regexp.MustCompile(`[:\s]+`)
for _, line := range lines {
fields := re.Split(line, -1)
interfaceName := fields[0]
instance := fields[1]
switch fields[3] {
case "rbytes64":
rbytes64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse rbytes64: %w", err)
}
case "ipackets64":
ipackets64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse ipackets64: %w", err)
}
case "idrops64":
idrops64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse idrops64: %w", err)
}
case "ierrors":
ierrorsarr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse ierrors: %w", err)
}
case "obytes64":
obytes64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse obytes64: %w", err)
}
case "opackets64":
opackets64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse opackets64: %w", err)
}
case "odrops64":
odrops64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse odrops64: %w", err)
}
case "oerrors":
oerrorsarr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse oerrors: %w", err)
}
}
}
ret := make([]IOCountersStat, 0)
for k := range rbytes64arr {
nic := IOCountersStat{
Name: k,
BytesRecv: rbytes64arr[k],
PacketsRecv: ipackets64arr[k],
Errin: ierrorsarr[k],
Dropin: idrops64arr[k],
BytesSent: obytes64arr[k],
PacketsSent: opackets64arr[k],
Errout: oerrorsarr[k],
Dropout: odrops64arr[k],
}
ret = append(ret, nic)
}

if !pernic {
return getIOCountersAll(ret)
}

return ret, nil
}

func Connections(kind string) ([]ConnectionStat, error) {
return ConnectionsWithContext(context.Background(), kind)
}

func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) {
return []ConnectionStat{}, common.ErrNotImplementedError
}

func FilterCounters() ([]FilterStat, error) {
return FilterCountersWithContext(context.Background())
}

func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) {
return []FilterStat{}, common.ErrNotImplementedError
}

func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) {
return ProtoCountersWithContext(context.Background(), protocols)
}

func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) {
return []ProtoCountersStat{}, common.ErrNotImplementedError
}
11 changes: 8 additions & 3 deletions net/net_test.go
Expand Up @@ -3,7 +3,6 @@ package net
import (
"errors"
"fmt"
"math"
"os"
"runtime"
"testing"
Expand Down Expand Up @@ -86,8 +85,14 @@ func TestNetIOCountersAll(t *testing.T) {
for _, p := range per {
pr += p.PacketsRecv
}
// small diff is ok
if math.Abs(float64(v[0].PacketsRecv-pr)) > 5 {
// small diff is ok, compare instead of math.Abs(subtraction) with uint64
var diff uint64
if v[0].PacketsRecv > pr {
diff = v[0].PacketsRecv - pr
} else {
diff = pr - v[0].PacketsRecv
}
if diff > 5 {
if ci := os.Getenv("CI"); ci != "" {
// This test often fails in CI. so just print even if failed.
fmt.Printf("invalid sum value: %v, %v", v[0].PacketsRecv, pr)
Expand Down