internal/system/resources.go (254 lines of code) (raw):
package system
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"go.uber.org/zap"
)
var (
cpuDirRegExp = regexp.MustCompile(`/cpu(\d+)`)
memoryCapacityRegexp = regexp.MustCompile(`MemTotal:\s*([0-9]+) kB`)
nodeDir = "/sys/devices/system/node"
cpusPath = "/sys/devices/system/cpu"
)
const (
sysFsCPUTopology = "/topology"
cpuDirPattern = "cpu*[0-9]"
nodeDirPattern = "node*[0-9]"
coreIDFilePath = sysFsCPUTopology + "/core_id"
packageIDFilePath = sysFsCPUTopology + "/physical_package_id"
)
type core struct {
Id int `json:"core_id"`
Threads []int `json:"thread_ids"`
SocketID int `json:"socket_id"`
}
func init() {
// cannot copy file to /sys for doc build, use this as a hack for testing
cpuDirEnv := os.Getenv("CPU_DIR")
if cpuDirEnv != "" {
cpusPath = cpuDirEnv
}
nodeDirEnv := os.Getenv("NODE_DIR")
if nodeDirEnv != "" {
nodeDir = nodeDirEnv
}
}
// GetMilliNumCores this is a striped version of GetNodesInfo that only get information for NumCores
// https://github.com/google/cadvisor/blob/master/utils/sysinfo/sysinfo.go#L203
func GetMilliNumCores() (int, error) {
allLogicalCoresCount := 0
nodesDirs, err := getNodesPaths()
if err != nil {
return 0, err
}
if len(nodesDirs) == 0 {
zap.L().Error("Nodes topology is not available, providing CPU topology")
cpuCount, err := getCPUCount()
if err != nil {
return 0, err
}
return cpuCount * 1000, nil
}
for _, dir := range nodesDirs {
cpuDirs, err := getCPUsPaths(dir)
if len(cpuDirs) == 0 {
zap.L().Error("Found node without any CPU", zap.String("dir", dir), zap.Error(err))
} else {
cores, err := getCoresInfo(cpuDirs)
if err != nil {
return 0, err
}
for _, core := range cores {
allLogicalCoresCount += len(core.Threads)
}
}
}
return allLogicalCoresCount * 1000, err
}
func getCPUCount() (int, error) {
cpusPaths, err := getCPUsPaths(cpusPath)
if err != nil {
return 0, err
}
cpusCount := len(cpusPaths)
if cpusCount == 0 {
return 0, fmt.Errorf("Any CPU is not available, cpusPath: %s", cpusPath)
}
return cpusCount, nil
}
func getNodesPaths() ([]string, error) {
pathPattern := fmt.Sprintf("%s/%s", nodeDir, nodeDirPattern)
return filepath.Glob(pathPattern)
}
func getCPUsPaths(cpusPath string) ([]string, error) {
pathPattern := fmt.Sprintf("%s/%s", cpusPath, cpuDirPattern)
return filepath.Glob(pathPattern)
}
func getCPUPhysicalPackageID(cpuPath string) (string, error) {
packageIDFilePath := fmt.Sprintf("%s%s", cpuPath, packageIDFilePath)
// #nosec G304 // This cpuPath essentially come from getCPUsPaths and it should be a system path
packageID, err := os.ReadFile(packageIDFilePath)
if err != nil {
return "", err
}
return strings.TrimSpace(string(packageID)), err
}
func getCoresInfo(cpuDirs []string) ([]core, error) {
cores := make([]core, 0, len(cpuDirs))
for _, cpuDir := range cpuDirs {
cpuID, err := getCPUID(cpuDir)
if err != nil {
return nil, fmt.Errorf("unexpected format of CPU directory, cpuDirRegExp %s, cpuDir: %s", cpuDirRegExp, cpuDir)
}
if !IsCPUOnline(cpuID) {
continue
}
rawPhysicalID, err := getCoreID(cpuDir)
if os.IsNotExist(err) {
zap.L().Warn("Cannot read core id for input cpuDir, core_id file does not exist",
zap.String("cpuDir", cpuDir), zap.Error(err))
continue
} else if err != nil {
return nil, err
}
physicalID, err := strconv.Atoi(rawPhysicalID)
if err != nil {
return nil, err
}
rawPhysicalPackageID, err := getCPUPhysicalPackageID(cpuDir)
if os.IsNotExist(err) {
zap.L().Warn("Cannot read physical package id for input cpuDir, physical_package_id file does not exist",
zap.String("cpuDir", cpuDir), zap.Error(err))
continue
} else if err != nil {
return nil, err
}
physicalPackageID, err := strconv.Atoi(rawPhysicalPackageID)
if err != nil {
return nil, err
}
coreIDx := -1
for id, core := range cores {
if core.Id == physicalID && core.SocketID == physicalPackageID {
coreIDx = id
}
}
if coreIDx == -1 {
cores = append(cores, core{})
coreIDx = len(cores) - 1
}
desiredCore := &cores[coreIDx]
desiredCore.Id = physicalID
desiredCore.SocketID = physicalPackageID
if len(desiredCore.Threads) == 0 {
desiredCore.Threads = []int{cpuID}
} else {
desiredCore.Threads = append(desiredCore.Threads, cpuID)
}
}
return cores, nil
}
func getCPUID(str string) (int, error) {
matches := cpuDirRegExp.FindStringSubmatch(str)
if len(matches) != 2 {
return 0, fmt.Errorf("failed to match regexp, str: %s", str)
}
valInt, err := strconv.Atoi(matches[1])
if err != nil {
return 0, err
}
return valInt, nil
}
func getCoreID(cpuPath string) (string, error) {
coreIDFilePath := fmt.Sprintf("%s%s", cpuPath, coreIDFilePath)
// #nosec G304 // This cpuPath essentially come from getCPUsPaths and it should be a system path
coreID, err := os.ReadFile(coreIDFilePath)
if err != nil {
return "", err
}
return strings.TrimSpace(string(coreID)), err
}
func IsCPUOnline(cpuID int) bool {
cpuOnlinePath, err := filepath.Abs(cpusPath + "/online")
if err != nil {
zap.L().Info("Unable to get absolute path", zap.String("absolutPath", cpusPath+"/online"))
return false
}
// Quick check to determine if file exists: if it does not then kernel CPU hotplug is disabled and all CPUs are online.
_, err = os.Stat(cpuOnlinePath)
if err != nil && os.IsNotExist(err) {
return true
}
if err != nil {
zap.L().Warn("Unable to stat cpuOnlinePath",
zap.String("cpuOnlinePath", cpuOnlinePath),
zap.Error(err))
}
isOnline, err := isCpuOnline(cpuOnlinePath, uint16(cpuID))
if err != nil {
zap.L().Error("Unable to get online CPUs list", zap.Error(err))
return false
}
return isOnline
}
func isCpuOnline(path string, cpuID uint16) (bool, error) {
// #nosec G304 // This path is cpuOnlinePath from isCPUOnline
fileContent, err := os.ReadFile(path)
if err != nil {
return false, err
}
if len(fileContent) == 0 {
return false, fmt.Errorf("%s found to be empty", path)
}
cpuList := strings.TrimSpace(string(fileContent))
for _, s := range strings.Split(cpuList, ",") {
splitted := strings.SplitN(s, "-", 3)
switch len(splitted) {
case 3:
return false, fmt.Errorf("invalid values in %s", path)
case 2:
min, err := strconv.ParseUint(splitted[0], 10, 16)
if err != nil {
return false, err
}
max, err := strconv.ParseUint(splitted[1], 10, 16)
if err != nil {
return false, err
}
if min > max {
return false, fmt.Errorf("invalid values in %s", path)
}
// Return true, if the CPU under consideration is in the range of online CPUs.
if cpuID >= uint16(min) && cpuID <= uint16(max) {
return true, nil
}
case 1:
value, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return false, err
}
if uint16(value) == cpuID {
return true, nil
}
}
}
return false, nil
}
// GetMachineMemoryCapacity returns the machine's total memory from /proc/meminfo.
// Returns the total memory capacity as number of bytes.
func GetMachineMemoryCapacity() (uint64, error) {
out, err := os.ReadFile("/proc/meminfo")
if err != nil {
return 0, err
}
memoryCapacity, err := parseCapacity(out, memoryCapacityRegexp)
if err != nil {
return 0, err
}
return memoryCapacity, err
}
// parseCapacity matches a Regexp in a []byte, returning the resulting value in bytes.
// Assumes that the value matched by the Regexp is in KB.
func parseCapacity(b []byte, r *regexp.Regexp) (uint64, error) {
matches := r.FindSubmatch(b)
if len(matches) != 2 {
return 0, fmt.Errorf("failed to match regexp in output: %q", string(b))
}
m, err := strconv.ParseUint(string(matches[1]), 10, 64)
if err != nil {
return 0, err
}
// Convert to bytes.
return m * 1024, err
}