-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from vimeo/import_cgroup2_and_type_changes_2024…
…-12-06 Import cgroup2 support & use generics to improve pparser's interface
- Loading branch information
Showing
19 changed files
with
2,640 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// package cgresolver contains helpers and types for resolving the CGroup associated with specific subsystems | ||
// If you don't know what cgroup subsystems are, you probably want one of the higher-level interfaces in the parent package. | ||
package cgresolver | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"slices" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
// CGMode is an enum indicating which cgroup type is active for the returned controller | ||
type CGMode uint8 | ||
|
||
const ( | ||
CGModeUnknown CGMode = iota | ||
// CGroup V1 | ||
CGModeV1 | ||
// CGroup V2 | ||
CGModeV2 | ||
) | ||
|
||
func cgroup2Mode(iscg2 bool) CGMode { | ||
if iscg2 { | ||
return CGModeV2 | ||
} | ||
return CGModeV1 | ||
} | ||
|
||
// CGroupPath includes information about a cgroup. | ||
type CGroupPath struct { | ||
AbsPath string | ||
MountPath string | ||
Mode CGMode | ||
} | ||
|
||
// Parent returns a CGroupPath for the parent directory as long as it wouldn't pass the root of the mountpoint. | ||
// second return indicates whether a new path was returned. | ||
func (c *CGroupPath) Parent() (CGroupPath, bool) { | ||
// Remove any trailing slash | ||
path := strings.TrimSuffix(c.AbsPath, string(os.PathSeparator)) | ||
mnt := strings.TrimSuffix(c.MountPath, string(os.PathSeparator)) | ||
if mnt == path { | ||
return CGroupPath{ | ||
AbsPath: path, | ||
MountPath: mnt, | ||
Mode: c.Mode, | ||
}, false | ||
} | ||
lastSlashIdx := strings.LastIndexByte(path, byte(os.PathSeparator)) | ||
if lastSlashIdx == -1 { | ||
// This shouldn't happen | ||
panic("invalid state: path \"" + path + "\" has no slashes and doesn't match the mountpoint") | ||
} | ||
return CGroupPath{ | ||
AbsPath: path[:lastSlashIdx], | ||
MountPath: mnt, // Strip any trailing slash in case one snuck in | ||
Mode: c.Mode, | ||
}, true | ||
} | ||
|
||
// SelfSubsystemPath returns a CGroupPath for the cgroup associated with a specific subsystem for the current process. | ||
func SelfSubsystemPath(subsystem string) (CGroupPath, error) { | ||
return subsystemPath("self", subsystem) | ||
} | ||
|
||
// PIDSubsystemPath returns a CGroupPath for the cgroup associated with a specific subsystem for the specified PID | ||
func PIDSubsystemPath(pid int, subsystem string) (CGroupPath, error) { | ||
return subsystemPath(strconv.Itoa(pid), subsystem) | ||
} | ||
|
||
func subsystemPath(procSubDir string, subsystem string) (CGroupPath, error) { | ||
cgSubSyses, cgSubSysReadErr := ParseReadCGSubsystems() | ||
if cgSubSysReadErr != nil { | ||
return CGroupPath{}, fmt.Errorf("failed to resolve subsystems to hierarchies: %w", cgSubSysReadErr) | ||
} | ||
cgIdx := slices.IndexFunc(cgSubSyses, func(c CGroupSubsystem) bool { | ||
return c.Subsys == subsystem | ||
}) | ||
if cgIdx == -1 { | ||
return CGroupPath{}, fmt.Errorf("no cgroup hierarchy associated with subsystem %q", subsystem) | ||
} | ||
cgHierID := cgSubSyses[cgIdx].Hierarchy | ||
|
||
procCGs, procCGsErr := resolveProcCGControllers(procSubDir) | ||
if procCGsErr != nil { | ||
return CGroupPath{}, fmt.Errorf("failed to resolve cgroup controllers: %w", procCGsErr) | ||
} | ||
|
||
procCGIdx := slices.IndexFunc(procCGs, func(cg CGProcHierarchy) bool { return cg.HierarchyID == cgHierID }) | ||
if procCGIdx == -1 { | ||
return CGroupPath{}, fmt.Errorf("failed to resolve process cgroup controllers: %w", procCGsErr) | ||
} | ||
|
||
cgMountInfo, mountInfoParseErr := CGroupMountInfo() | ||
if mountInfoParseErr != nil { | ||
return CGroupPath{}, fmt.Errorf("failed to parse mountinfo: %w", mountInfoParseErr) | ||
} | ||
|
||
cgPath, cgPathErr := procCGs[procCGIdx].cgPath(cgMountInfo) | ||
if cgPathErr != nil { | ||
return CGroupPath{}, fmt.Errorf("failed to resolve filesystem path for cgroup %+v: %w", procCGs[procCGIdx], cgPathErr) | ||
} | ||
return cgPath, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package cgresolver | ||
|
||
import "testing" | ||
|
||
func TestCGroupPathParent(t *testing.T) { | ||
for _, tbl := range []struct { | ||
name string | ||
in CGroupPath | ||
expParent CGroupPath | ||
expNewParent bool | ||
}{ | ||
{ | ||
name: "cgroup_mount_root", | ||
in: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expParent: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expNewParent: false, | ||
}, | ||
{ | ||
name: "cgroup_mount_root_strip_trailing_slashes", | ||
in: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/", | ||
MountPath: "/sys/fs/cgroup/", | ||
Mode: CGModeV2, | ||
}, | ||
expParent: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expNewParent: false, | ||
}, | ||
{ | ||
name: "cgroup_mount_sub_cgroup_cgv1", | ||
in: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/a/b/c", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV1, | ||
}, | ||
expParent: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/a/b", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV1, | ||
}, | ||
expNewParent: true, | ||
}, | ||
{ | ||
name: "cgroup_mount_sub_cgroup_cgv2", | ||
in: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/a/b/c", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expParent: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/a/b", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expNewParent: true, | ||
}, | ||
{ | ||
name: "cgroup_mount_sub_cgroup_strip_trailing_slash", | ||
in: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/a/b/c/", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expParent: CGroupPath{ | ||
AbsPath: "/sys/fs/cgroup/a/b", | ||
MountPath: "/sys/fs/cgroup", | ||
Mode: CGModeV2, | ||
}, | ||
expNewParent: true, | ||
}, | ||
} { | ||
t.Run(tbl.name, func(t *testing.T) { | ||
par, np := tbl.in.Parent() | ||
if np != tbl.expNewParent { | ||
t.Errorf("unexpected OK value: %t; expected %t", np, tbl.expNewParent) | ||
} | ||
if par != tbl.expParent { | ||
t.Errorf("unexpected parent CGroupPath:\n got %+v\n want %+v", par, tbl.expParent) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package cgresolver | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
// Mount represents a cgroup or cgroup2 mount. | ||
// Subsystems will be nil if the mount is for a unified hierarchy/cgroup v2 | ||
// in that case, CGroupV2 will be true. | ||
type Mount struct { | ||
Mountpoint string | ||
Root string | ||
Subsystems []string | ||
CGroupV2 bool // true if this is a cgroup2 mount | ||
} | ||
|
||
const ( | ||
mountinfoPath = "/proc/self/mountinfo" | ||
) | ||
|
||
// CGroupMountInfo parses /proc/self/mountinfo and returns info about all cgroup and cgroup2 mounts | ||
func CGroupMountInfo() ([]Mount, error) { | ||
mountinfoContents, mntInfoReadErr := os.ReadFile(mountinfoPath) | ||
if mntInfoReadErr != nil { | ||
return nil, fmt.Errorf("failed to read contents of %s: %w", | ||
mountinfoPath, mntInfoReadErr) | ||
} | ||
|
||
mounts, mntsErr := getCGroupMountsFromMountinfo(string(mountinfoContents)) | ||
if mntsErr != nil { | ||
return nil, fmt.Errorf("failed to list cgroupfs mounts: %w", mntsErr) | ||
} | ||
|
||
return mounts, nil | ||
} | ||
|
||
func getCGroupMountsFromMountinfo(mountinfo string) ([]Mount, error) { | ||
// mountinfo is line-delimited, then space-delimited | ||
mountinfoLines := strings.Split(mountinfo, "\n") | ||
if len(mountinfoLines) == 0 { | ||
return nil, fmt.Errorf("unexpectedly empty mountinfo (one line): %q", mountinfo) | ||
} | ||
out := make([]Mount, 0, len(mountinfoLines)) | ||
for _, line := range mountinfoLines { | ||
if len(line) == 0 { | ||
continue | ||
} | ||
sections := strings.SplitN(line, " - ", 2) | ||
if len(sections) < 2 { | ||
return nil, fmt.Errorf("missing section separator in line %q", line) | ||
} | ||
s2Fields := strings.SplitN(sections[1], " ", 3) | ||
if len(s2Fields) < 3 { | ||
return nil, fmt.Errorf("line %q contains %d fields in second section, expected 3", | ||
line, len(s2Fields)) | ||
|
||
} | ||
isCG2 := false | ||
switch s2Fields[0] { | ||
case "cgroup": | ||
isCG2 = false | ||
case "cgroup2": | ||
isCG2 = true | ||
default: | ||
// skip anything that's not a cgroup | ||
continue | ||
} | ||
s1Fields := strings.Split(sections[0], " ") | ||
if len(s1Fields) < 5 { | ||
return nil, fmt.Errorf("too few fields in line %q before optional separator: %d; expected 5", | ||
line, len(s1Fields)) | ||
} | ||
mntpnt, mntPntUnescapeErr := unOctalEscape(s1Fields[4]) | ||
if mntPntUnescapeErr != nil { | ||
return nil, fmt.Errorf("failed to unescape mountpoint %q: %w", s1Fields[4], mntPntUnescapeErr) | ||
} | ||
rootPath, rootUnescErr := unOctalEscape(s1Fields[3]) | ||
if rootUnescErr != nil { | ||
return nil, fmt.Errorf("failed to unescape mount root %q: %w", s1Fields[3], rootUnescErr) | ||
} | ||
mnt := Mount{ | ||
CGroupV2: isCG2, | ||
Mountpoint: mntpnt, | ||
Root: rootPath, | ||
Subsystems: nil, | ||
} | ||
// only bother with the mount options to find subsystems if cgroup v1 | ||
if !isCG2 { | ||
for _, mntOpt := range strings.Split(s2Fields[2], ",") { | ||
switch mntOpt { | ||
case "ro", "rw": | ||
// These mount options are lies, (or at least | ||
// only reflect the original mount, without | ||
// considering the layering of later bind-mounts) | ||
continue | ||
case "": | ||
continue | ||
default: | ||
mnt.Subsystems = append(mnt.Subsystems, mntOpt) | ||
} | ||
} | ||
} | ||
|
||
out = append(out, mnt) | ||
|
||
} | ||
return out, nil | ||
} | ||
|
||
func unOctalEscape(str string) (string, error) { | ||
b := strings.Builder{} | ||
b.Grow(len(str)) | ||
for { | ||
backslashIdx := strings.IndexByte(str, byte('\\')) | ||
if backslashIdx == -1 { | ||
b.WriteString(str) | ||
return b.String(), nil | ||
} | ||
b.WriteString(str[:backslashIdx]) | ||
// if the end of the escape is beyond the end of the string, abort! | ||
if backslashIdx+3 >= len(str) { | ||
return "", fmt.Errorf("invalid offset: %d+3 >= len %d", backslashIdx, len(str)) | ||
} | ||
// slice out the octal 3-digit component | ||
esc := str[backslashIdx+1 : backslashIdx+4] | ||
asciiVal, parseUintErr := strconv.ParseUint(esc, 8, 8) | ||
if parseUintErr != nil { | ||
return "", fmt.Errorf("failed to parse escape value %q: %w", esc, parseUintErr) | ||
} | ||
b.WriteByte(byte(asciiVal)) | ||
str = str[backslashIdx+4:] | ||
} | ||
|
||
} |
Oops, something went wrong.