...

Source file src/github.com/containerd/cgroups/v2/utils.go

Documentation: github.com/containerd/cgroups/v2

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package v2
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"io"
    23  	"math"
    24  	"os"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/containerd/cgroups/v2/stats"
    31  
    32  	"github.com/godbus/dbus/v5"
    33  	"github.com/opencontainers/runtime-spec/specs-go"
    34  	"github.com/sirupsen/logrus"
    35  )
    36  
    37  const (
    38  	cgroupProcs    = "cgroup.procs"
    39  	cgroupThreads  = "cgroup.threads"
    40  	defaultDirPerm = 0755
    41  )
    42  
    43  // defaultFilePerm is a var so that the test framework can change the filemode
    44  // of all files created when the tests are running.  The difference between the
    45  // tests and real world use is that files like "cgroup.procs" will exist when writing
    46  // to a read cgroup filesystem and do not exist prior when running in the tests.
    47  // this is set to a non 0 value in the test code
    48  var defaultFilePerm = os.FileMode(0)
    49  
    50  // remove will remove a cgroup path handling EAGAIN and EBUSY errors and
    51  // retrying the remove after a exp timeout
    52  func remove(path string) error {
    53  	var err error
    54  	delay := 10 * time.Millisecond
    55  	for i := 0; i < 5; i++ {
    56  		if i != 0 {
    57  			time.Sleep(delay)
    58  			delay *= 2
    59  		}
    60  		if err = os.RemoveAll(path); err == nil {
    61  			return nil
    62  		}
    63  	}
    64  	return fmt.Errorf("cgroups: unable to remove path %q: %w", path, err)
    65  }
    66  
    67  // parseCgroupProcsFile parses /sys/fs/cgroup/$GROUPPATH/cgroup.procs
    68  func parseCgroupProcsFile(path string) ([]uint64, error) {
    69  	f, err := os.Open(path)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	defer f.Close()
    74  	var (
    75  		out []uint64
    76  		s   = bufio.NewScanner(f)
    77  	)
    78  	for s.Scan() {
    79  		if t := s.Text(); t != "" {
    80  			pid, err := strconv.ParseUint(t, 10, 0)
    81  			if err != nil {
    82  				return nil, err
    83  			}
    84  			out = append(out, pid)
    85  		}
    86  	}
    87  	if err := s.Err(); err != nil {
    88  		return nil, err
    89  	}
    90  	return out, nil
    91  }
    92  
    93  func parseKV(raw string) (string, interface{}, error) {
    94  	parts := strings.Fields(raw)
    95  	switch len(parts) {
    96  	case 2:
    97  		v, err := parseUint(parts[1], 10, 64)
    98  		if err != nil {
    99  			// if we cannot parse as a uint, parse as a string
   100  			return parts[0], parts[1], nil
   101  		}
   102  		return parts[0], v, nil
   103  	default:
   104  		return "", 0, ErrInvalidFormat
   105  	}
   106  }
   107  
   108  func parseUint(s string, base, bitSize int) (uint64, error) {
   109  	v, err := strconv.ParseUint(s, base, bitSize)
   110  	if err != nil {
   111  		intValue, intErr := strconv.ParseInt(s, base, bitSize)
   112  		// 1. Handle negative values greater than MinInt64 (and)
   113  		// 2. Handle negative values lesser than MinInt64
   114  		if intErr == nil && intValue < 0 {
   115  			return 0, nil
   116  		} else if intErr != nil &&
   117  			intErr.(*strconv.NumError).Err == strconv.ErrRange &&
   118  			intValue < 0 {
   119  			return 0, nil
   120  		}
   121  		return 0, err
   122  	}
   123  	return v, nil
   124  }
   125  
   126  // parseCgroupFile parses /proc/PID/cgroup file and return string
   127  func parseCgroupFile(path string) (string, error) {
   128  	f, err := os.Open(path)
   129  	if err != nil {
   130  		return "", err
   131  	}
   132  	defer f.Close()
   133  	return parseCgroupFromReader(f)
   134  }
   135  
   136  func parseCgroupFromReader(r io.Reader) (string, error) {
   137  	var (
   138  		s = bufio.NewScanner(r)
   139  	)
   140  	for s.Scan() {
   141  		var (
   142  			text  = s.Text()
   143  			parts = strings.SplitN(text, ":", 3)
   144  		)
   145  		if len(parts) < 3 {
   146  			return "", fmt.Errorf("invalid cgroup entry: %q", text)
   147  		}
   148  		// text is like "0::/user.slice/user-1001.slice/session-1.scope"
   149  		if parts[0] == "0" && parts[1] == "" {
   150  			return parts[2], nil
   151  		}
   152  	}
   153  	if err := s.Err(); err != nil {
   154  		return "", err
   155  	}
   156  	return "", fmt.Errorf("cgroup path not found")
   157  }
   158  
   159  // ToResources converts the oci LinuxResources struct into a
   160  // v2 Resources type for use with this package.
   161  //
   162  // converting cgroups configuration from v1 to v2
   163  // ref: https://github.com/containers/crun/blob/master/crun.1.md#cgroup-v2
   164  func ToResources(spec *specs.LinuxResources) *Resources {
   165  	var resources Resources
   166  	if cpu := spec.CPU; cpu != nil {
   167  		resources.CPU = &CPU{
   168  			Cpus: cpu.Cpus,
   169  			Mems: cpu.Mems,
   170  		}
   171  		if shares := cpu.Shares; shares != nil {
   172  			convertedWeight := 1 + ((*shares-2)*9999)/262142
   173  			resources.CPU.Weight = &convertedWeight
   174  		}
   175  		if period := cpu.Period; period != nil {
   176  			resources.CPU.Max = NewCPUMax(cpu.Quota, period)
   177  		}
   178  	}
   179  	if mem := spec.Memory; mem != nil {
   180  		resources.Memory = &Memory{}
   181  		if swap := mem.Swap; swap != nil {
   182  			resources.Memory.Swap = swap
   183  		}
   184  		if l := mem.Limit; l != nil {
   185  			resources.Memory.Max = l
   186  		}
   187  		if l := mem.Reservation; l != nil {
   188  			resources.Memory.Low = l
   189  		}
   190  	}
   191  	if hugetlbs := spec.HugepageLimits; hugetlbs != nil {
   192  		hugeTlbUsage := HugeTlb{}
   193  		for _, hugetlb := range hugetlbs {
   194  			hugeTlbUsage = append(hugeTlbUsage, HugeTlbEntry{
   195  				HugePageSize: hugetlb.Pagesize,
   196  				Limit:        hugetlb.Limit,
   197  			})
   198  		}
   199  		resources.HugeTlb = &hugeTlbUsage
   200  	}
   201  	if pids := spec.Pids; pids != nil {
   202  		resources.Pids = &Pids{
   203  			Max: pids.Limit,
   204  		}
   205  	}
   206  	if i := spec.BlockIO; i != nil {
   207  		resources.IO = &IO{}
   208  		if i.Weight != nil {
   209  			resources.IO.BFQ.Weight = 1 + (*i.Weight-10)*9999/990
   210  		}
   211  		for t, devices := range map[IOType][]specs.LinuxThrottleDevice{
   212  			ReadBPS:   i.ThrottleReadBpsDevice,
   213  			WriteBPS:  i.ThrottleWriteBpsDevice,
   214  			ReadIOPS:  i.ThrottleReadIOPSDevice,
   215  			WriteIOPS: i.ThrottleWriteIOPSDevice,
   216  		} {
   217  			for _, d := range devices {
   218  				resources.IO.Max = append(resources.IO.Max, Entry{
   219  					Type:  t,
   220  					Major: d.Major,
   221  					Minor: d.Minor,
   222  					Rate:  d.Rate,
   223  				})
   224  			}
   225  		}
   226  	}
   227  	if i := spec.Rdma; i != nil {
   228  		resources.RDMA = &RDMA{}
   229  		for device, value := range spec.Rdma {
   230  			if device != "" && (value.HcaHandles != nil && value.HcaObjects != nil) {
   231  				resources.RDMA.Limit = append(resources.RDMA.Limit, RDMAEntry{
   232  					Device:     device,
   233  					HcaHandles: *value.HcaHandles,
   234  					HcaObjects: *value.HcaObjects,
   235  				})
   236  			}
   237  		}
   238  	}
   239  
   240  	return &resources
   241  }
   242  
   243  // Gets uint64 parsed content of single value cgroup stat file
   244  func getStatFileContentUint64(filePath string) uint64 {
   245  	contents, err := os.ReadFile(filePath)
   246  	if err != nil {
   247  		return 0
   248  	}
   249  	trimmed := strings.TrimSpace(string(contents))
   250  	if trimmed == "max" {
   251  		return math.MaxUint64
   252  	}
   253  
   254  	res, err := parseUint(trimmed, 10, 64)
   255  	if err != nil {
   256  		logrus.Errorf("unable to parse %q as a uint from Cgroup file %q", string(contents), filePath)
   257  		return res
   258  	}
   259  
   260  	return res
   261  }
   262  
   263  func readIoStats(path string) []*stats.IOEntry {
   264  	// more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt
   265  	var usage []*stats.IOEntry
   266  	fpath := filepath.Join(path, "io.stat")
   267  	currentData, err := os.ReadFile(fpath)
   268  	if err != nil {
   269  		return usage
   270  	}
   271  	entries := strings.Split(string(currentData), "\n")
   272  
   273  	for _, entry := range entries {
   274  		parts := strings.Split(entry, " ")
   275  		if len(parts) < 2 {
   276  			continue
   277  		}
   278  		majmin := strings.Split(parts[0], ":")
   279  		if len(majmin) != 2 {
   280  			continue
   281  		}
   282  		major, err := strconv.ParseUint(majmin[0], 10, 0)
   283  		if err != nil {
   284  			return usage
   285  		}
   286  		minor, err := strconv.ParseUint(majmin[1], 10, 0)
   287  		if err != nil {
   288  			return usage
   289  		}
   290  		parts = parts[1:]
   291  		ioEntry := stats.IOEntry{
   292  			Major: major,
   293  			Minor: minor,
   294  		}
   295  		for _, s := range parts {
   296  			keyPairValue := strings.Split(s, "=")
   297  			if len(keyPairValue) != 2 {
   298  				continue
   299  			}
   300  			v, err := strconv.ParseUint(keyPairValue[1], 10, 0)
   301  			if err != nil {
   302  				continue
   303  			}
   304  			switch keyPairValue[0] {
   305  			case "rbytes":
   306  				ioEntry.Rbytes = v
   307  			case "wbytes":
   308  				ioEntry.Wbytes = v
   309  			case "rios":
   310  				ioEntry.Rios = v
   311  			case "wios":
   312  				ioEntry.Wios = v
   313  			}
   314  		}
   315  		usage = append(usage, &ioEntry)
   316  	}
   317  	return usage
   318  }
   319  
   320  func rdmaStats(filepath string) []*stats.RdmaEntry {
   321  	currentData, err := os.ReadFile(filepath)
   322  	if err != nil {
   323  		return []*stats.RdmaEntry{}
   324  	}
   325  	return toRdmaEntry(strings.Split(string(currentData), "\n"))
   326  }
   327  
   328  func parseRdmaKV(raw string, entry *stats.RdmaEntry) {
   329  	var value uint64
   330  	var err error
   331  
   332  	parts := strings.Split(raw, "=")
   333  	switch len(parts) {
   334  	case 2:
   335  		if parts[1] == "max" {
   336  			value = math.MaxUint32
   337  		} else {
   338  			value, err = parseUint(parts[1], 10, 32)
   339  			if err != nil {
   340  				return
   341  			}
   342  		}
   343  		if parts[0] == "hca_handle" {
   344  			entry.HcaHandles = uint32(value)
   345  		} else if parts[0] == "hca_object" {
   346  			entry.HcaObjects = uint32(value)
   347  		}
   348  	}
   349  }
   350  
   351  func toRdmaEntry(strEntries []string) []*stats.RdmaEntry {
   352  	var rdmaEntries []*stats.RdmaEntry
   353  	for i := range strEntries {
   354  		parts := strings.Fields(strEntries[i])
   355  		switch len(parts) {
   356  		case 3:
   357  			entry := new(stats.RdmaEntry)
   358  			entry.Device = parts[0]
   359  			parseRdmaKV(parts[1], entry)
   360  			parseRdmaKV(parts[2], entry)
   361  
   362  			rdmaEntries = append(rdmaEntries, entry)
   363  		default:
   364  			continue
   365  		}
   366  	}
   367  	return rdmaEntries
   368  }
   369  
   370  // isUnitExists returns true if the error is that a systemd unit already exists.
   371  func isUnitExists(err error) bool {
   372  	if err != nil {
   373  		if dbusError, ok := err.(dbus.Error); ok {
   374  			return strings.Contains(dbusError.Name, "org.freedesktop.systemd1.UnitExists")
   375  		}
   376  	}
   377  	return false
   378  }
   379  
   380  func systemdUnitFromPath(path string) string {
   381  	_, unit := filepath.Split(path)
   382  	return unit
   383  }
   384  
   385  func readHugeTlbStats(path string) []*stats.HugeTlbStat {
   386  	var usage = []*stats.HugeTlbStat{}
   387  	var keyUsage = make(map[string]*stats.HugeTlbStat)
   388  	f, err := os.Open(path)
   389  	if err != nil {
   390  		return usage
   391  	}
   392  	files, err := f.Readdir(-1)
   393  	f.Close()
   394  	if err != nil {
   395  		return usage
   396  	}
   397  
   398  	for _, file := range files {
   399  		if strings.Contains(file.Name(), "hugetlb") &&
   400  			(strings.HasSuffix(file.Name(), "max") || strings.HasSuffix(file.Name(), "current")) {
   401  			var hugeTlb *stats.HugeTlbStat
   402  			var ok bool
   403  			fileName := strings.Split(file.Name(), ".")
   404  			pageSize := fileName[1]
   405  			if hugeTlb, ok = keyUsage[pageSize]; !ok {
   406  				hugeTlb = &stats.HugeTlbStat{}
   407  			}
   408  			hugeTlb.Pagesize = pageSize
   409  			out, err := os.ReadFile(filepath.Join(path, file.Name()))
   410  			if err != nil {
   411  				continue
   412  			}
   413  			var value uint64
   414  			stringVal := strings.TrimSpace(string(out))
   415  			if stringVal == "max" {
   416  				value = math.MaxUint64
   417  			} else {
   418  				value, err = strconv.ParseUint(stringVal, 10, 64)
   419  			}
   420  			if err != nil {
   421  				continue
   422  			}
   423  			switch fileName[2] {
   424  			case "max":
   425  				hugeTlb.Max = value
   426  			case "current":
   427  				hugeTlb.Current = value
   428  			}
   429  			keyUsage[pageSize] = hugeTlb
   430  		}
   431  	}
   432  	for _, entry := range keyUsage {
   433  		usage = append(usage, entry)
   434  	}
   435  	return usage
   436  }
   437  

View as plain text