init
This commit is contained in:
88
src/internal/sysfs/sysfs.go
Normal file
88
src/internal/sysfs/sysfs.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package sysfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SysReader abstracts /sys access for testability.
|
||||
type SysReader interface {
|
||||
ReadInterfaceStats(ifname string) (map[string]int64, error)
|
||||
GetBlockDeviceSize(devPath string) (int64, error)
|
||||
}
|
||||
|
||||
// RealSysReader reads from the actual /sys filesystem.
|
||||
type RealSysReader struct {
|
||||
SysPath string // default "/sys"
|
||||
}
|
||||
|
||||
func NewRealSysReader() *RealSysReader {
|
||||
return &RealSysReader{SysPath: "/sys"}
|
||||
}
|
||||
|
||||
// ReadInterfaceStats reads all statistics files from /sys/class/net/{ifname}/statistics/.
|
||||
func (r *RealSysReader) ReadInterfaceStats(ifname string) (map[string]int64, error) {
|
||||
dir := filepath.Join(r.SysPath, "class", "net", ifname, "statistics")
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := make(map[string]int64)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
stats[e.Name()] = val
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetBlockDeviceSize returns the size in bytes of a block device.
|
||||
// For symlinks (e.g., /dev/zvol/...), resolves to the real device first.
|
||||
// Reads size from /sys/block/{dev}/size (in 512-byte sectors).
|
||||
func (r *RealSysReader) GetBlockDeviceSize(devPath string) (int64, error) {
|
||||
// Resolve symlinks
|
||||
resolved, err := filepath.EvalSymlinks(devPath)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("resolve symlink %s: %w", devPath, err)
|
||||
}
|
||||
|
||||
// Extract device name from /dev/XXX
|
||||
devName := filepath.Base(resolved)
|
||||
|
||||
// Try /sys/block/{devName}/size
|
||||
sizeFile := filepath.Join(r.SysPath, "block", devName, "size")
|
||||
data, err := os.ReadFile(sizeFile)
|
||||
if err != nil {
|
||||
// For partition devices like dm-0, try without partition suffix
|
||||
return 0, fmt.Errorf("read size %s: %w", sizeFile, err)
|
||||
}
|
||||
|
||||
sectors, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse size: %w", err)
|
||||
}
|
||||
|
||||
return sectors * 512, nil
|
||||
}
|
||||
|
||||
// GetDeviceSymlinkTarget resolves a device symlink and returns the target path.
|
||||
func GetDeviceSymlinkTarget(devPath string) (string, error) {
|
||||
resolved, err := filepath.EvalSymlinks(devPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
69
src/internal/sysfs/sysfs_test.go
Normal file
69
src/internal/sysfs/sysfs_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package sysfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadInterfaceStats(t *testing.T) {
|
||||
// Create temp sysfs-like structure
|
||||
tmpDir := t.TempDir()
|
||||
statsDir := filepath.Join(tmpDir, "class", "net", "tap100i0", "statistics")
|
||||
os.MkdirAll(statsDir, 0755)
|
||||
|
||||
os.WriteFile(filepath.Join(statsDir, "rx_bytes"), []byte("123456\n"), 0644)
|
||||
os.WriteFile(filepath.Join(statsDir, "tx_bytes"), []byte("789012\n"), 0644)
|
||||
os.WriteFile(filepath.Join(statsDir, "rx_packets"), []byte("100\n"), 0644)
|
||||
|
||||
reader := &RealSysReader{SysPath: tmpDir}
|
||||
stats, err := reader.ReadInterfaceStats("tap100i0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if stats["rx_bytes"] != 123456 {
|
||||
t.Errorf("rx_bytes = %d", stats["rx_bytes"])
|
||||
}
|
||||
if stats["tx_bytes"] != 789012 {
|
||||
t.Errorf("tx_bytes = %d", stats["tx_bytes"])
|
||||
}
|
||||
if stats["rx_packets"] != 100 {
|
||||
t.Errorf("rx_packets = %d", stats["rx_packets"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadInterfaceStats_NotFound(t *testing.T) {
|
||||
reader := &RealSysReader{SysPath: t.TempDir()}
|
||||
_, err := reader.ReadInterfaceStats("nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent interface")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBlockDeviceSize(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create /sys/block/dm-0/size
|
||||
blockDir := filepath.Join(tmpDir, "block", "dm-0")
|
||||
os.MkdirAll(blockDir, 0755)
|
||||
// 1GB = 2097152 sectors of 512 bytes
|
||||
os.WriteFile(filepath.Join(blockDir, "size"), []byte("2097152\n"), 0644)
|
||||
|
||||
// Create a "device" symlink that points to dm-0
|
||||
devDir := filepath.Join(tmpDir, "dev")
|
||||
os.MkdirAll(devDir, 0755)
|
||||
os.Symlink(filepath.Join(devDir, "dm-0"), filepath.Join(devDir, "mydev"))
|
||||
// Create the actual "device" file so symlink resolves
|
||||
os.WriteFile(filepath.Join(devDir, "dm-0"), []byte{}, 0644)
|
||||
|
||||
reader := &RealSysReader{SysPath: tmpDir}
|
||||
size, err := reader.GetBlockDeviceSize(filepath.Join(devDir, "dm-0"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := int64(2097152 * 512)
|
||||
if size != expected {
|
||||
t.Errorf("size = %d, want %d", size, expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user