github.com/google/cadvisor@v0.49.1/container/common/helpers.go (about)

     1  // Copyright 2016 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package common
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"os"
    21  	"path"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/karrick/godirwalk"
    27  	"github.com/opencontainers/runc/libcontainer/cgroups"
    28  	"github.com/pkg/errors"
    29  	"golang.org/x/sys/unix"
    30  
    31  	"github.com/google/cadvisor/container"
    32  	info "github.com/google/cadvisor/info/v1"
    33  	"github.com/google/cadvisor/utils"
    34  
    35  	"k8s.io/klog/v2"
    36  )
    37  
    38  func DebugInfo(watches map[string][]string) map[string][]string {
    39  	out := make(map[string][]string)
    40  
    41  	lines := make([]string, 0, len(watches))
    42  	for containerName, cgroupWatches := range watches {
    43  		lines = append(lines, fmt.Sprintf("%s:", containerName))
    44  		for _, cg := range cgroupWatches {
    45  			lines = append(lines, fmt.Sprintf("\t%s", cg))
    46  		}
    47  	}
    48  	out["Inotify watches"] = lines
    49  
    50  	return out
    51  }
    52  
    53  var bootTime = func() time.Time {
    54  	now := time.Now()
    55  	var sysinfo unix.Sysinfo_t
    56  	if err := unix.Sysinfo(&sysinfo); err != nil {
    57  		return now
    58  	}
    59  	sinceBoot := time.Duration(sysinfo.Uptime) * time.Second
    60  	return now.Add(-1 * sinceBoot).Truncate(time.Minute)
    61  }()
    62  
    63  func GetSpec(cgroupPaths map[string]string, machineInfoFactory info.MachineInfoFactory, hasNetwork, hasFilesystem bool) (info.ContainerSpec, error) {
    64  	return getSpecInternal(cgroupPaths, machineInfoFactory, hasNetwork, hasFilesystem, cgroups.IsCgroup2UnifiedMode())
    65  }
    66  
    67  func getSpecInternal(cgroupPaths map[string]string, machineInfoFactory info.MachineInfoFactory, hasNetwork, hasFilesystem, cgroup2UnifiedMode bool) (info.ContainerSpec, error) {
    68  	var spec info.ContainerSpec
    69  
    70  	// Assume unified hierarchy containers.
    71  	// Get the lowest creation time from all hierarchies as the container creation time.
    72  	now := time.Now()
    73  	lowestTime := now
    74  	for _, cgroupPathDir := range cgroupPaths {
    75  		dir, err := os.Stat(cgroupPathDir)
    76  		if err == nil && dir.ModTime().Before(lowestTime) {
    77  			lowestTime = dir.ModTime()
    78  		} else if os.IsNotExist(err) {
    79  			// Directory does not exist, skip checking for files within.
    80  			continue
    81  		}
    82  
    83  		// The modified time of the cgroup directory sometimes changes whenever a subcontainer is created.
    84  		// eg. /docker will have creation time matching the creation of latest docker container.
    85  		// Use clone_children/events as a workaround as it isn't usually modified. It is only likely changed
    86  		// immediately after creating a container. If the directory modified time is lower, we use that.
    87  		cgroupPathFile := path.Join(cgroupPathDir, "cgroup.clone_children")
    88  		if cgroup2UnifiedMode {
    89  			cgroupPathFile = path.Join(cgroupPathDir, "cgroup.events")
    90  		}
    91  		fi, err := os.Stat(cgroupPathFile)
    92  		if err == nil && fi.ModTime().Before(lowestTime) {
    93  			lowestTime = fi.ModTime()
    94  		}
    95  	}
    96  	if lowestTime.Before(bootTime) {
    97  		lowestTime = bootTime
    98  	}
    99  
   100  	if lowestTime != now {
   101  		spec.CreationTime = lowestTime
   102  	}
   103  
   104  	// Get machine info.
   105  	mi, err := machineInfoFactory.GetMachineInfo()
   106  	if err != nil {
   107  		return spec, err
   108  	}
   109  
   110  	// CPU.
   111  	cpuRoot, ok := GetControllerPath(cgroupPaths, "cpu", cgroup2UnifiedMode)
   112  	if ok {
   113  		if utils.FileExists(cpuRoot) {
   114  			if cgroup2UnifiedMode {
   115  				spec.HasCpu = true
   116  
   117  				weight := readUInt64(cpuRoot, "cpu.weight")
   118  				if weight > 0 {
   119  					limit, err := convertCPUWeightToCPULimit(weight)
   120  					if err != nil {
   121  						klog.Errorf("GetSpec: Failed to read CPULimit from %q: %s", path.Join(cpuRoot, "cpu.weight"), err)
   122  					} else {
   123  						spec.Cpu.Limit = limit
   124  					}
   125  				}
   126  				max := readString(cpuRoot, "cpu.max")
   127  				if max != "" {
   128  					splits := strings.SplitN(max, " ", 2)
   129  					if len(splits) != 2 {
   130  						klog.Errorf("GetSpec: Failed to parse CPUmax from %q", path.Join(cpuRoot, "cpu.max"))
   131  					} else {
   132  						if splits[0] != "max" {
   133  							spec.Cpu.Quota = parseUint64String(splits[0])
   134  						}
   135  						spec.Cpu.Period = parseUint64String(splits[1])
   136  					}
   137  				}
   138  			} else {
   139  				spec.HasCpu = true
   140  				spec.Cpu.Limit = readUInt64(cpuRoot, "cpu.shares")
   141  				spec.Cpu.Period = readUInt64(cpuRoot, "cpu.cfs_period_us")
   142  				quota := readString(cpuRoot, "cpu.cfs_quota_us")
   143  
   144  				if quota != "" && quota != "-1" {
   145  					val, err := strconv.ParseUint(quota, 10, 64)
   146  					if err != nil {
   147  						klog.Errorf("GetSpec: Failed to parse CPUQuota from %q: %s", path.Join(cpuRoot, "cpu.cfs_quota_us"), err)
   148  					} else {
   149  						spec.Cpu.Quota = val
   150  					}
   151  				}
   152  			}
   153  		}
   154  	}
   155  
   156  	// Cpu Mask.
   157  	// This will fail for non-unified hierarchies. We'll return the whole machine mask in that case.
   158  	cpusetRoot, ok := GetControllerPath(cgroupPaths, "cpuset", cgroup2UnifiedMode)
   159  	if ok {
   160  		if utils.FileExists(cpusetRoot) {
   161  			spec.HasCpu = true
   162  			mask := ""
   163  			if cgroup2UnifiedMode {
   164  				mask = readString(cpusetRoot, "cpuset.cpus.effective")
   165  			} else {
   166  				mask = readString(cpusetRoot, "cpuset.cpus")
   167  			}
   168  			spec.Cpu.Mask = utils.FixCpuMask(mask, mi.NumCores)
   169  		}
   170  	}
   171  
   172  	// Memory
   173  	memoryRoot, ok := GetControllerPath(cgroupPaths, "memory", cgroup2UnifiedMode)
   174  	if ok {
   175  		if cgroup2UnifiedMode {
   176  			if utils.FileExists(path.Join(memoryRoot, "memory.max")) {
   177  				spec.HasMemory = true
   178  				spec.Memory.Reservation = readUInt64(memoryRoot, "memory.min")
   179  				spec.Memory.Limit = readUInt64(memoryRoot, "memory.max")
   180  				spec.Memory.SwapLimit = readUInt64(memoryRoot, "memory.swap.max")
   181  			}
   182  		} else {
   183  			if utils.FileExists(memoryRoot) {
   184  				spec.HasMemory = true
   185  				spec.Memory.Limit = readUInt64(memoryRoot, "memory.limit_in_bytes")
   186  				spec.Memory.SwapLimit = readUInt64(memoryRoot, "memory.memsw.limit_in_bytes")
   187  				spec.Memory.Reservation = readUInt64(memoryRoot, "memory.soft_limit_in_bytes")
   188  			}
   189  		}
   190  	}
   191  
   192  	// Hugepage
   193  	hugepageRoot, ok := cgroupPaths["hugetlb"]
   194  	if ok {
   195  		if utils.FileExists(hugepageRoot) {
   196  			spec.HasHugetlb = true
   197  		}
   198  	}
   199  
   200  	// Processes, read it's value from pids path directly
   201  	pidsRoot, ok := GetControllerPath(cgroupPaths, "pids", cgroup2UnifiedMode)
   202  	if ok {
   203  		if utils.FileExists(pidsRoot) {
   204  			spec.HasProcesses = true
   205  			spec.Processes.Limit = readUInt64(pidsRoot, "pids.max")
   206  		}
   207  	}
   208  
   209  	spec.HasNetwork = hasNetwork
   210  	spec.HasFilesystem = hasFilesystem
   211  
   212  	ioControllerName := "blkio"
   213  	if cgroup2UnifiedMode {
   214  		ioControllerName = "io"
   215  	}
   216  
   217  	if blkioRoot, ok := GetControllerPath(cgroupPaths, ioControllerName, cgroup2UnifiedMode); ok && utils.FileExists(blkioRoot) {
   218  		spec.HasDiskIo = true
   219  	}
   220  
   221  	return spec, nil
   222  }
   223  
   224  func GetControllerPath(cgroupPaths map[string]string, controllerName string, cgroup2UnifiedMode bool) (string, bool) {
   225  
   226  	ok := false
   227  	path := ""
   228  
   229  	if cgroup2UnifiedMode {
   230  		path, ok = cgroupPaths[""]
   231  	} else {
   232  		path, ok = cgroupPaths[controllerName]
   233  	}
   234  	return path, ok
   235  }
   236  
   237  func readString(dirpath string, file string) string {
   238  	cgroupFile := path.Join(dirpath, file)
   239  
   240  	// Read
   241  	out, err := os.ReadFile(cgroupFile)
   242  	if err != nil {
   243  		// Ignore non-existent files
   244  		if !os.IsNotExist(err) {
   245  			klog.Warningf("readString: Failed to read %q: %s", cgroupFile, err)
   246  		}
   247  		return ""
   248  	}
   249  	return strings.TrimSpace(string(out))
   250  }
   251  
   252  // Convert from [1-10000] to [2-262144]
   253  func convertCPUWeightToCPULimit(weight uint64) (uint64, error) {
   254  	const (
   255  		// minWeight is the lowest value possible for cpu.weight
   256  		minWeight = 1
   257  		// maxWeight is the highest value possible for cpu.weight
   258  		maxWeight = 10000
   259  	)
   260  	if weight < minWeight || weight > maxWeight {
   261  		return 0, fmt.Errorf("convertCPUWeightToCPULimit: invalid cpu weight: %v", weight)
   262  	}
   263  	return 2 + ((weight-1)*262142)/9999, nil
   264  }
   265  
   266  func parseUint64String(strValue string) uint64 {
   267  	if strValue == "max" {
   268  		return math.MaxUint64
   269  	}
   270  	if strValue == "" {
   271  		return 0
   272  	}
   273  
   274  	val, err := strconv.ParseUint(strValue, 10, 64)
   275  	if err != nil {
   276  		klog.Errorf("parseUint64String: Failed to parse int %q: %s", strValue, err)
   277  		return 0
   278  	}
   279  
   280  	return val
   281  }
   282  
   283  func readUInt64(dirpath string, file string) uint64 {
   284  	out := readString(dirpath, file)
   285  	if out == "max" {
   286  		return math.MaxUint64
   287  	}
   288  	if out == "" {
   289  		return 0
   290  	}
   291  
   292  	val, err := strconv.ParseUint(out, 10, 64)
   293  	if err != nil {
   294  		klog.Errorf("readUInt64: Failed to parse int %q from file %q: %s", out, path.Join(dirpath, file), err)
   295  		return 0
   296  	}
   297  
   298  	return val
   299  }
   300  
   301  // Lists all directories under "path" and outputs the results as children of "parent".
   302  func ListDirectories(dirpath string, parent string, recursive bool, output map[string]struct{}) error {
   303  	buf := make([]byte, godirwalk.MinimumScratchBufferSize)
   304  	return listDirectories(dirpath, parent, recursive, output, buf)
   305  }
   306  
   307  func listDirectories(dirpath string, parent string, recursive bool, output map[string]struct{}, buf []byte) error {
   308  	dirents, err := godirwalk.ReadDirents(dirpath, buf)
   309  	if err != nil {
   310  		// Ignore if this hierarchy does not exist.
   311  		if os.IsNotExist(errors.Cause(err)) {
   312  			err = nil
   313  		}
   314  		return err
   315  	}
   316  	for _, dirent := range dirents {
   317  		// We only grab directories.
   318  		if !dirent.IsDir() {
   319  			continue
   320  		}
   321  		dirname := dirent.Name()
   322  
   323  		name := path.Join(parent, dirname)
   324  		output[name] = struct{}{}
   325  
   326  		// List subcontainers if asked to.
   327  		if recursive {
   328  			err := listDirectories(path.Join(dirpath, dirname), name, true, output, buf)
   329  			if err != nil {
   330  				return err
   331  			}
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  func MakeCgroupPaths(mountPoints map[string]string, name string) map[string]string {
   338  	cgroupPaths := make(map[string]string, len(mountPoints))
   339  	for key, val := range mountPoints {
   340  		cgroupPaths[key] = path.Join(val, name)
   341  	}
   342  
   343  	return cgroupPaths
   344  }
   345  
   346  func CgroupExists(cgroupPaths map[string]string) bool {
   347  	// If any cgroup exists, the container is still alive.
   348  	for _, cgroupPath := range cgroupPaths {
   349  		if utils.FileExists(cgroupPath) {
   350  			return true
   351  		}
   352  	}
   353  	return false
   354  }
   355  
   356  func ListContainers(name string, cgroupPaths map[string]string, listType container.ListType) ([]info.ContainerReference, error) {
   357  	containers := make(map[string]struct{})
   358  	for _, cgroupPath := range cgroupPaths {
   359  		err := ListDirectories(cgroupPath, name, listType == container.ListRecursive, containers)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  	}
   364  
   365  	// Make into container references.
   366  	ret := make([]info.ContainerReference, 0, len(containers))
   367  	for cont := range containers {
   368  		ret = append(ret, info.ContainerReference{
   369  			Name: cont,
   370  		})
   371  	}
   372  
   373  	return ret, nil
   374  }
   375  
   376  // AssignDeviceNamesToDiskStats assigns the Device field on the provided DiskIoStats by looking up
   377  // the device major and minor identifiers in the provided device namer.
   378  func AssignDeviceNamesToDiskStats(namer DeviceNamer, stats *info.DiskIoStats) {
   379  	assignDeviceNamesToPerDiskStats(
   380  		namer,
   381  		stats.IoMerged,
   382  		stats.IoQueued,
   383  		stats.IoServiceBytes,
   384  		stats.IoServiceTime,
   385  		stats.IoServiced,
   386  		stats.IoTime,
   387  		stats.IoWaitTime,
   388  		stats.Sectors,
   389  	)
   390  }
   391  
   392  // assignDeviceNamesToPerDiskStats looks up device names for the provided stats, caching names
   393  // if necessary.
   394  func assignDeviceNamesToPerDiskStats(namer DeviceNamer, diskStats ...[]info.PerDiskStats) {
   395  	devices := make(deviceIdentifierMap)
   396  	for _, stats := range diskStats {
   397  		for i, stat := range stats {
   398  			stats[i].Device = devices.Find(stat.Major, stat.Minor, namer)
   399  		}
   400  	}
   401  }
   402  
   403  // DeviceNamer returns string names for devices by their major and minor id.
   404  type DeviceNamer interface {
   405  	// DeviceName returns the name of the device by its major and minor ids, or false if no
   406  	// such device is recognized.
   407  	DeviceName(major, minor uint64) (string, bool)
   408  }
   409  
   410  type MachineInfoNamer info.MachineInfo
   411  
   412  func (n *MachineInfoNamer) DeviceName(major, minor uint64) (string, bool) {
   413  	for _, info := range n.DiskMap {
   414  		if info.Major == major && info.Minor == minor {
   415  			return "/dev/" + info.Name, true
   416  		}
   417  	}
   418  	for _, info := range n.Filesystems {
   419  		if info.DeviceMajor == major && info.DeviceMinor == minor {
   420  			return info.Device, true
   421  		}
   422  	}
   423  	return "", false
   424  }
   425  
   426  type deviceIdentifier struct {
   427  	major uint64
   428  	minor uint64
   429  }
   430  
   431  type deviceIdentifierMap map[deviceIdentifier]string
   432  
   433  // Find locates the device name by device identifier out of from, caching the result as necessary.
   434  func (m deviceIdentifierMap) Find(major, minor uint64, namer DeviceNamer) string {
   435  	d := deviceIdentifier{major, minor}
   436  	if s, ok := m[d]; ok {
   437  		return s
   438  	}
   439  	s, _ := namer.DeviceName(major, minor)
   440  	m[d] = s
   441  	return s
   442  }
   443  
   444  // RemoveNetMetrics is used to remove any network metrics from the given MetricSet.
   445  // It returns the original set as is if remove is false, or if there are no metrics
   446  // to remove.
   447  func RemoveNetMetrics(metrics container.MetricSet, remove bool) container.MetricSet {
   448  	if !remove {
   449  		return metrics
   450  	}
   451  
   452  	// Check if there is anything we can remove, to avoid useless copying.
   453  	if !metrics.HasAny(container.AllNetworkMetrics) {
   454  		return metrics
   455  	}
   456  
   457  	// A copy of all metrics except for network ones.
   458  	return metrics.Difference(container.AllNetworkMetrics)
   459  }