Repository: prometheus/procfs
Pull request #517 (2023-05-11 12:57:04)

Fix softnet CPU Index
When parsing older kernel versoins, we assume the CPU is indexed based on the parsed line, rather than an explicit index column.


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d3ad0f9..25a6795 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,7 +4,7 @@ version: 2.1
 jobs:
   lint:
     docker:
-    - image: cimg/go:1.19
+    - image: cimg/go:1.20
     steps:
     - checkout
     - run: make check_license
@@ -55,8 +55,8 @@ workflows:
         matrix:
           parameters:
             go_version:
-            - "1.18"
             - "1.19"
+            - "1.20"
     - test:
         name: test-windows
         os: windows
@@ -64,6 +64,6 @@ workflows:
         matrix:
           parameters:
             go_version:
-            - "1.18"
             - "1.19"
+            - "1.20"
     - codespell
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 2ba5e88..100cf93 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -18,13 +18,13 @@ jobs:
       - name: Checkout repository
         uses: actions/checkout@v3
       - name: install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
-          go-version: 1.18.x
+          go-version: 1.20.x
       - name: Install snmp_exporter/generator dependencies
         run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
         if: github.repository == 'prometheus/snmp_exporter'
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.3.1
+        uses: golangci/golangci-lint-action@v3.4.0
         with:
-          version: v1.50.1
+          version: v1.51.2
diff --git a/fs.go b/fs.go
index df9044e..60c551e 100644
--- a/fs.go
+++ b/fs.go
@@ -14,8 +14,6 @@
 package procfs
 
 import (
-	"syscall"
-
 	"github.com/prometheus/procfs/internal/fs"
 )
 
@@ -50,15 +48,3 @@ func NewFS(mountPoint string) (FS, error) {
 
 	return FS{fs, real}, nil
 }
-
-// isRealProc determines whether supplied mountpoint is really a proc filesystem.
-func isRealProc(mountPoint string) (bool, error) {
-	stat := syscall.Statfs_t{}
-	err := syscall.Statfs(mountPoint, &stat)
-	if err != nil {
-		return false, err
-	}
-
-	// 0x9fa0 is PROC_SUPER_MAGIC: https://elixir.bootlin.com/linux/v6.1/source/include/uapi/linux/magic.h#L87
-	return stat.Type == 0x9fa0, nil
-}
diff --git a/fs_statfs_notype.go b/fs_statfs_notype.go
new file mode 100644
index 0000000..446a85d
--- /dev/null
+++ b/fs_statfs_notype.go
@@ -0,0 +1,23 @@
+// Copyright 2018 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build netbsd || openbsd || solaris
+// +build netbsd openbsd solaris
+
+package procfs
+
+// isRealProc returns true on architectures that don't have a Type argument
+// in their Statfs_t struct
+func isRealProc(mountPoint string) (bool, error) {
+	return true, nil
+}
diff --git a/fs_statfs_type.go b/fs_statfs_type.go
new file mode 100644
index 0000000..6f5af28
--- /dev/null
+++ b/fs_statfs_type.go
@@ -0,0 +1,33 @@
+// Copyright 2018 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !netbsd && !openbsd && !solaris
+// +build !netbsd,!openbsd,!solaris
+
+package procfs
+
+import (
+	"syscall"
+)
+
+// isRealProc determines whether supplied mountpoint is really a proc filesystem.
+func isRealProc(mountPoint string) (bool, error) {
+	stat := syscall.Statfs_t{}
+	err := syscall.Statfs(mountPoint, &stat)
+	if err != nil {
+		return false, err
+	}
+
+	// 0x9fa0 is PROC_SUPER_MAGIC: https://elixir.bootlin.com/linux/v6.1/source/include/uapi/linux/magic.h#L87
+	return stat.Type == 0x9fa0, nil
+}
diff --git a/go.mod b/go.mod
index cd59bd6..6013506 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,9 @@
 module github.com/prometheus/procfs
 
-go 1.18
+go 1.19
 
 require (
 	github.com/google/go-cmp v0.5.9
-	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.7.0
+	golang.org/x/sync v0.2.0
+	golang.org/x/sys v0.8.0
 )
diff --git a/go.sum b/go.sum
index a2fcec7..4530cf5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/net_softnet.go b/net_softnet.go
index 06b7b8f..540cea5 100644
--- a/net_softnet.go
+++ b/net_softnet.go
@@ -76,6 +76,7 @@ func parseSoftnet(r io.Reader) ([]SoftnetStat, error) {
 	s := bufio.NewScanner(r)
 
 	var stats []SoftnetStat
+	cpuIndex := 0
 	for s.Scan() {
 		columns := strings.Fields(s.Text())
 		width := len(columns)
@@ -127,9 +128,13 @@ func parseSoftnet(r io.Reader) ([]SoftnetStat, error) {
 
 			softnetStat.SoftnetBacklogLen = us[0]
 			softnetStat.Index = us[1]
+		} else {
+			// For older kernels, create the Index based on the scan line number.
+			softnetStat.Index = uint32(cpuIndex)
 		}
 		softnetStat.Width = width
 		stats = append(stats, softnetStat)
+		cpuIndex++
 	}
 
 	return stats, nil
diff --git a/proc_status.go b/proc_status.go
index 3d8c064..c055d07 100644
--- a/proc_status.go
+++ b/proc_status.go
@@ -15,6 +15,7 @@ package procfs
 
 import (
 	"bytes"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -76,6 +77,9 @@ type ProcStatus struct {
 	UIDs [4]string
 	// GIDs of the process (Real, effective, saved set, and filesystem GIDs)
 	GIDs [4]string
+
+	// CpusAllowedList: List of cpu cores processes are allowed to run on.
+	CpusAllowedList []uint64
 }
 
 // NewStatus returns the current status information of the process.
@@ -161,10 +165,38 @@ func (s *ProcStatus) fillStatus(k string, vString string, vUint uint64, vUintByt
 		s.VoluntaryCtxtSwitches = vUint
 	case "nonvoluntary_ctxt_switches":
 		s.NonVoluntaryCtxtSwitches = vUint
+	case "Cpus_allowed_list":
+		s.CpusAllowedList = calcCpusAllowedList(vString)
 	}
+
 }
 
 // TotalCtxtSwitches returns the total context switch.
 func (s ProcStatus) TotalCtxtSwitches() uint64 {
 	return s.VoluntaryCtxtSwitches + s.NonVoluntaryCtxtSwitches
 }
+
+func calcCpusAllowedList(cpuString string) []uint64 {
+	s := strings.Split(cpuString, ",")
+
+	var g []uint64
+
+	for _, cpu := range s {
+		// parse cpu ranges, example: 1-3=[1,2,3]
+		if l := strings.Split(strings.TrimSpace(cpu), "-"); len(l) > 1 {
+			startCPU, _ := strconv.ParseUint(l[0], 10, 64)
+			endCPU, _ := strconv.ParseUint(l[1], 10, 64)
+
+			for i := startCPU; i <= endCPU; i++ {
+				g = append(g, i)
+			}
+		} else if len(l) == 1 {
+			cpu, _ := strconv.ParseUint(l[0], 10, 64)
+			g = append(g, cpu)
+		}
+
+	}
+
+	sort.Slice(g, func(i, j int) bool { return g[i] < g[j] })
+	return g
+}
diff --git a/sysfs/class_sas_device.go b/sysfs/class_sas_device.go
index 61361be..14014a7 100644
--- a/sysfs/class_sas_device.go
+++ b/sysfs/class_sas_device.go
@@ -17,7 +17,6 @@
 package sysfs
 
 import (
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -53,7 +52,7 @@ var (
 func (fs FS) parseSASDeviceClass(dir string) (SASDeviceClass, error) {
 	path := fs.sys.Path(dir)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -97,7 +96,7 @@ func (fs FS) parseSASDevice(name string) (*SASDevice, error) {
 
 	devicepath := fs.sys.Path(filepath.Join(sasDeviceClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(devicepath)
+	dirs, err := os.ReadDir(devicepath)
 	if err != nil {
 		return nil, err
 	}
@@ -139,7 +138,7 @@ func (fs FS) blockSASDeviceBlockDevices(name string) ([]string, error) {
 
 	devicepath := fs.sys.Path(filepath.Join(sasDeviceClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(devicepath)
+	dirs, err := os.ReadDir(devicepath)
 	if err != nil {
 		return nil, err
 	}
@@ -148,7 +147,7 @@ func (fs FS) blockSASDeviceBlockDevices(name string) ([]string, error) {
 		if sasTargetDeviceRegexp.MatchString(d.Name()) {
 			targetdir := d.Name()
 
-			subtargets, err := ioutil.ReadDir(filepath.Join(devicepath, targetdir))
+			subtargets, err := os.ReadDir(filepath.Join(devicepath, targetdir))
 			if err != nil {
 				return nil, err
 			}
@@ -160,7 +159,7 @@ func (fs FS) blockSASDeviceBlockDevices(name string) ([]string, error) {
 					continue
 				}
 
-				blocks, err := ioutil.ReadDir(filepath.Join(devicepath, targetdir, targetsubdir.Name(), "block"))
+				blocks, err := os.ReadDir(filepath.Join(devicepath, targetdir, targetsubdir.Name(), "block"))
 				if err != nil {
 					if os.IsNotExist(err) {
 						continue
diff --git a/sysfs/class_sas_host.go b/sysfs/class_sas_host.go
index 783902b..fade1ca 100644
--- a/sysfs/class_sas_host.go
+++ b/sysfs/class_sas_host.go
@@ -17,7 +17,7 @@
 package sysfs
 
 import (
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"regexp"
 )
@@ -49,7 +49,7 @@ var (
 func (fs FS) SASHostClass() (SASHostClass, error) {
 	path := fs.sys.Path(sasHostClassPath)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -75,7 +75,7 @@ func (fs FS) parseSASHost(name string) (*SASHost, error) {
 
 	devicepath := fs.sys.Path(filepath.Join(sasHostClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(devicepath)
+	dirs, err := os.ReadDir(devicepath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sysfs/class_sas_phy.go b/sysfs/class_sas_phy.go
index 9e028db..e0e3a31 100644
--- a/sysfs/class_sas_phy.go
+++ b/sysfs/class_sas_phy.go
@@ -18,7 +18,6 @@ package sysfs
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -54,7 +53,7 @@ type SASPhyClass map[string]*SASPhy
 func (fs FS) SASPhyClass() (SASPhyClass, error) {
 	path := fs.sys.Path(sasPhyClassPath)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -88,7 +87,7 @@ func (fs FS) parseSASPhy(name string) (*SASPhy, error) {
 		}
 	}
 
-	files, err := ioutil.ReadDir(phypath)
+	files, err := os.ReadDir(phypath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sysfs/class_sas_port.go b/sysfs/class_sas_port.go
index 6d625bc..7afe251 100644
--- a/sysfs/class_sas_port.go
+++ b/sysfs/class_sas_port.go
@@ -17,7 +17,7 @@
 package sysfs
 
 import (
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"regexp"
 )
@@ -52,7 +52,7 @@ var (
 func (fs FS) SASPortClass() (SASPortClass, error) {
 	path := fs.sys.Path(sasPortClassPath)
 
-	dirs, err := ioutil.ReadDir(path)
+	dirs, err := os.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
@@ -77,7 +77,7 @@ func (fs FS) parseSASPort(name string) (*SASPort, error) {
 
 	portpath := fs.sys.Path(filepath.Join(sasPortClassPath, name, "device"))
 
-	dirs, err := ioutil.ReadDir(portpath)
+	dirs, err := os.ReadDir(portpath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sysfs/net_class.go b/sysfs/net_class.go
index e1b1811..20a0612 100644
--- a/sysfs/net_class.go
+++ b/sysfs/net_class.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"syscall"
 
 	"github.com/prometheus/procfs/internal/util"
 )
@@ -117,13 +118,38 @@ func (fs FS) NetClass() (NetClass, error) {
 	return netClass, nil
 }
 
+// canIgnoreError returns true if the error is non-fatal and can be ignored.
+// Some kernels and some devices don't expose specific attributes or return
+// errors when reading those attributes; we can ignore these errors and the
+// attribute that caused them.
+func canIgnoreError(err error) bool {
+	var errno syscall.Errno
+
+	if os.IsNotExist(err) {
+		return true
+	} else if os.IsPermission(err) {
+		return true
+	} else if err.Error() == "operation not supported" {
+		return true
+	} else if errors.Is(err, os.ErrInvalid) {
+		return true
+	} else if errors.As(err, &errno) && (errno == syscall.EINVAL) {
+		return true
+	}
+	// all other errors are fatal
+	return false
+}
+
 // ParseNetClassAttribute parses a given file in /sys/class/net/<iface>
 // and sets the value in a given NetClassIface object if the value was readable.
-// It returns an error if the file cannot be read.
+// It returns an error if the file cannot be read and the error is fatal.
 func ParseNetClassAttribute(devicePath, attrName string, interfaceClass *NetClassIface) error {
 	attrPath := filepath.Join(devicePath, attrName)
 	value, err := util.SysReadFile(attrPath)
 	if err != nil {
+		if canIgnoreError(err) {
+			return nil
+		}
 		return fmt.Errorf("failed to read file %q: %w", attrPath, err)
 	}
 
@@ -202,12 +228,8 @@ func parseNetClassIface(devicePath string) (*NetClassIface, error) {
 		if !f.Type().IsRegular() {
 			continue
 		}
-		err := ParseNetClassAttribute(devicePath, f.Name(), &interfaceClass)
-		if err != nil {
-			// Return fatal errors to caller
-			if !os.IsNotExist(err) && !os.IsPermission(err) && err.Error() != "operation not supported" && !errors.Is(err, os.ErrInvalid) {
-				return nil, err
-			}
+		if err := ParseNetClassAttribute(devicePath, f.Name(), &interfaceClass); err != nil {
+			return nil, err
 		}
 	}
 


Test patch:
diff --git a/net_softnet_test.go b/net_softnet_test.go
index 1620c86..ac67f41 100644
--- a/net_softnet_test.go
+++ b/net_softnet_test.go
@@ -55,6 +55,7 @@ func TestNetSoftnet(t *testing.T) {
 			CPUCollision:   0x00000004,
 			ReceivedRps:    0x00000003,
 			FlowLimitCount: 0x00000002,
+			Index:          0x00000002,
 			Width:          11,
 		},
 		{
@@ -62,6 +63,7 @@ func TestNetSoftnet(t *testing.T) {
 			Dropped:      0x00000000,
 			TimeSqueezed: 0x0109a4,
 			CPUCollision: 0x00020e76,
+			Index:        0x00000003,
 			Width:        9,
 		},
 		{
@@ -70,6 +72,7 @@ func TestNetSoftnet(t *testing.T) {
 			TimeSqueezed: 0x00000011,
 			CPUCollision: 0x00000020,
 			ReceivedRps:  0x00000010,
+			Index:        0x00000004,
 			Width:        10,
 		},
 	}
diff --git a/proc_status_test.go b/proc_status_test.go
index 3c9ab59..2f0884f 100644
--- a/proc_status_test.go
+++ b/proc_status_test.go
@@ -14,6 +14,7 @@
 package procfs
 
 import (
+	"reflect"
 	"testing"
 )
 
@@ -121,3 +122,19 @@ func TestProcStatusGIDs(t *testing.T) {
 		t.Errorf("want uids %s, have %s", want, have)
 	}
 }
+
+func TestCpusAllowedList(t *testing.T) {
+	p, err := getProcFixtures(t).Proc(26231)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s, err := p.NewStatus()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if want, have := []uint64{0, 1, 2, 3, 4, 5, 6, 7}, s.CpusAllowedList; !reflect.DeepEqual(want, have) {
+		t.Errorf("want CpusAllowedList %v, have %v", want, have)
+	}
+}