Merge branch '20756-singularity-cgroups'
[arvados.git] / lib / crunchrun / cgroup.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package crunchrun
6
7 import (
8         "bytes"
9         "errors"
10         "fmt"
11         "io/fs"
12         "os"
13         "os/exec"
14         "regexp"
15         "strconv"
16         "sync"
17 )
18
19 // Return the current process's cgroup for the given subsystem.
20 //
21 // If the host has cgroups v2 and not v1 (i.e., unified mode), return
22 // the current process's cgroup.
23 func findCgroup(fsys fs.FS, subsystem string) (string, error) {
24         subsys := []byte(subsystem)
25         cgroups, err := fs.ReadFile(fsys, "proc/self/cgroup")
26         if err != nil {
27                 return "", err
28         }
29         for _, line := range bytes.Split(cgroups, []byte("\n")) {
30                 toks := bytes.SplitN(line, []byte(":"), 4)
31                 if len(toks) < 3 {
32                         continue
33                 }
34                 if len(toks[1]) == 0 && string(toks[0]) == "0" {
35                         // cgroups v2: "0::$PATH"
36                         //
37                         // In "hybrid" mode, this entry is last, so we
38                         // use it when the specified subsystem doesn't
39                         // match a cgroups v1 entry.
40                         //
41                         // In "unified" mode, this is the only entry,
42                         // so we use it regardless of which subsystem
43                         // was specified.
44                         return string(toks[2]), nil
45                 }
46                 for _, s := range bytes.Split(toks[1], []byte(",")) {
47                         // cgroups v1: "7:cpu,cpuacct:/user.slice"
48                         if bytes.Compare(s, subsys) == 0 {
49                                 return string(toks[2]), nil
50                         }
51                 }
52         }
53         return "", fmt.Errorf("subsystem %q not found in /proc/self/cgroup", subsystem)
54 }
55
56 var (
57         // After calling checkCgroupSupport, cgroupSupport indicates
58         // support for singularity resource limits.
59         //
60         // E.g., cgroupSupport["memory"]==true if systemd is installed
61         // and configured such that singularity can use the "memory"
62         // cgroup controller to set resource limits.
63         cgroupSupport     map[string]bool
64         cgroupSupportLock sync.Mutex
65 )
66
67 // checkCgroupSupport should be called before looking up strings like
68 // "memory" and "cpu" in cgroupSupport.
69 func checkCgroupSupport(logf func(string, ...interface{})) {
70         cgroupSupportLock.Lock()
71         defer cgroupSupportLock.Unlock()
72         if cgroupSupport != nil {
73                 return
74         }
75         cgroupSupport = make(map[string]bool)
76         if os.Getuid() != 0 {
77                 xrd := os.Getenv("XDG_RUNTIME_DIR")
78                 if xrd == "" || os.Getenv("DBUS_SESSION_BUS_ADDRESS") == "" {
79                         logf("not running as root, and empty XDG_RUNTIME_DIR or DBUS_SESSION_BUS_ADDRESS -- singularity resource limits are not supported")
80                         return
81                 }
82                 if fi, err := os.Stat(xrd + "/systemd"); err != nil || !fi.IsDir() {
83                         logf("not running as root, and %s/systemd is not a directory -- singularity resource limits are not supported", xrd)
84                         return
85                 }
86                 version, err := exec.Command("systemd-run", "--version").CombinedOutput()
87                 if match := regexp.MustCompile(`^systemd (\d+)`).FindSubmatch(version); err != nil || match == nil {
88                         logf("not running as root, and could not get systemd version -- singularity resource limits are not supported")
89                         return
90                 } else if v, _ := strconv.ParseInt(string(match[1]), 10, 64); v < 224 {
91                         logf("not running as root, and systemd version %s < minimum 224 -- singularity resource limits are not supported", match[1])
92                         return
93                 }
94         }
95         mount, err := cgroupMount()
96         if err != nil {
97                 logf("no cgroup support: %s", err)
98                 return
99         }
100         cgroup, err := findCgroup(os.DirFS("/"), "")
101         if err != nil {
102                 logf("cannot find cgroup: %s", err)
103                 return
104         }
105         controllers, err := os.ReadFile(mount + cgroup + "/cgroup.controllers")
106         if err != nil {
107                 logf("cannot read cgroup.controllers file: %s", err)
108                 return
109         }
110         for _, controller := range bytes.Split(bytes.TrimRight(controllers, "\n"), []byte{' '}) {
111                 cgroupSupport[string(controller)] = true
112         }
113 }
114
115 // Return the cgroup2 mount point, typically "/sys/fs/cgroup".
116 func cgroupMount() (string, error) {
117         mounts, err := os.ReadFile("/proc/mounts")
118         if err != nil {
119                 return "", err
120         }
121         for _, mount := range bytes.Split(mounts, []byte{'\n'}) {
122                 toks := bytes.Split(mount, []byte{' '})
123                 if len(toks) > 2 && bytes.Equal(toks[0], []byte("cgroup2")) {
124                         return string(toks[1]), nil
125                 }
126         }
127         return "", errors.New("cgroup2 mount not found")
128 }