utils.go 10 KB

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