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 1 commit
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
28 changes: 28 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()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about to pass ctx instead of creating a new context in this function?

Suggested change
freemem, err := globalZoneFreeMemory()
freemem, err := globalZoneFreeMemory(ctx)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Shirou,
Thank for your review and comment; I have made the suggested change and submitted the commit.
For the tested OS environments, below is what I did for the Solaris and illumos environments:

  1. For the Solaris OS test, I have created the Solaris instance from the Oracle Solaris 11.4 image provided by the Oracle Cloud marketplace at https://cloudmarketplace.oracle.com/marketplace/en_US/listing/61750333 (OCI allows to create free Solaris instances with the public image);
  2. For the illumos OS test, I used the omniOS-r151044 (recommended by the illumos.org) from https://omnios.org/download.html to create the virtual machine and do the test.
    For both environments, I tested with the go1.15 illumos/amd64 (and go1.13 illumos/amd64) and tests result look good.

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,26 @@ func globalZoneMemoryCapacity() (uint64, error) {
return totalMB * 1024 * 1024, nil
}

func globalZoneFreeMemory() (uint64, error) {
ctx := context.Background()
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