Skip to content

Commit be7ba72

Browse files
committed
remove unnecessary os-specific implementations now that we have abstracted out the filesystem
1 parent 6a878b9 commit be7ba72

File tree

5 files changed

+206
-250
lines changed

5 files changed

+206
-250
lines changed

cli/clistat/cgroup.go

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,165 @@
1-
//go:build !linux
2-
31
package clistat
42

3+
import (
4+
"bufio"
5+
"bytes"
6+
"strconv"
7+
"time"
8+
9+
"github.com/spf13/afero"
10+
"golang.org/x/xerrors"
11+
"tailscale.com/types/ptr"
12+
)
13+
14+
const (
15+
cgroupV2CPUMax = "/sys/fs/cgroup/cpu.max"
16+
cgroupV2CPUStat = "/sys/fs/cgroup/cpu.stat"
17+
)
18+
519
// ContainerCPU returns the CPU usage of the container cgroup.
6-
// On non-Linux platforms, this always returns nil.
20+
// If the system is not containerized, this always returns nil.
721
func (s *Statter) ContainerCPU() (*Result, error) {
8-
return nil, nil
22+
// Firstly, check if we are containerized.
23+
if ok, err := IsContainerized(s.fs); err != nil || !ok {
24+
return nil, nil //nolint: nilnil
25+
}
26+
27+
used1, total, err := s.cgroupCPU()
28+
if err != nil {
29+
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
30+
}
31+
<-time.After(s.sampleInterval)
32+
33+
// total is unlikely to change. Use the first value.
34+
used2, _, err := s.cgroupCPU()
35+
if err != nil {
36+
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
37+
}
38+
39+
r := &Result{
40+
Unit: "cores",
41+
Used: (used2 - used1).Seconds(),
42+
Total: ptr.To(total.Seconds() / s.sampleInterval.Seconds()), // close enough to the truth
43+
}
44+
return r, nil
45+
}
46+
47+
func (s *Statter) cgroupCPU() (used, total time.Duration, err error) {
48+
if s.isCGroupV2() {
49+
return s.cGroupV2CPU()
50+
}
51+
52+
// Fall back to CGroupv1
53+
return s.cGroupV1CPU()
54+
}
55+
56+
func (s *Statter) isCGroupV2() bool {
57+
// Check for the presence of /sys/fs/cgroup/cpu.max
58+
_, err := s.fs.Stat("/sys/fs/cgroup/cpu.max")
59+
return err == nil
60+
}
61+
62+
func (s *Statter) cGroupV2CPU() (used, total time.Duration, err error) {
63+
total, err = s.cGroupv2CPUTotal()
64+
if err != nil {
65+
return 0, 0, xerrors.Errorf("get cgroup v2 CPU cores: %w", err)
66+
}
67+
68+
used, err = s.cGroupv2CPUUsed()
69+
if err != nil {
70+
return 0, 0, xerrors.Errorf("get cgroup v2 CPU used: %w", err)
71+
}
72+
73+
return used, total, nil
74+
}
75+
76+
func (s *Statter) cGroupv2CPUUsed() (used time.Duration, err error) {
77+
var data []byte
78+
data, err = afero.ReadFile(s.fs, cgroupV2CPUStat)
79+
if err != nil {
80+
return 0, xerrors.Errorf("read /sys/fs/cgroup/cpu.stat: %w", err)
81+
}
82+
83+
bs := bufio.NewScanner(bytes.NewReader(data))
84+
for bs.Scan() {
85+
line := bs.Bytes()
86+
if !bytes.HasPrefix(line, []byte("usage_usec ")) {
87+
continue
88+
}
89+
90+
parts := bytes.Split(line, []byte(" "))
91+
if len(parts) != 2 {
92+
return 0, xerrors.Errorf("unexpected line in /sys/fs/cgroup/cpu.stat: %s", line)
93+
}
94+
95+
iused, err := strconv.Atoi(string(parts[1]))
96+
if err != nil {
97+
return 0, xerrors.Errorf("parse /sys/fs/cgroup/cpu.stat: %w", err)
98+
}
99+
100+
return time.Duration(iused) * time.Microsecond, nil
101+
}
102+
103+
return 0, xerrors.Errorf("did not find expected usage_usec in /sys/fs/cgroup/cpu.stat")
104+
}
105+
106+
func (s *Statter) cGroupv2CPUTotal() (total time.Duration, err error) {
107+
var data []byte
108+
var quotaUs int
109+
data, err = afero.ReadFile(s.fs, "/sys/fs/cgroup/cpu.max")
110+
if err != nil {
111+
return 0, xerrors.Errorf("read /sys/fs/cgroup/cpu.max: %w", err)
112+
}
113+
114+
lines := bytes.Split(data, []byte("\n"))
115+
if len(lines) < 1 {
116+
return 0, xerrors.Errorf("unexpected empty /sys/fs/cgroup/cpu.max")
117+
}
118+
119+
line := lines[0]
120+
parts := bytes.Split(line, []byte(" "))
121+
if len(parts) != 2 {
122+
return 0, xerrors.Errorf("unexpected line in /sys/fs/cgroup/cpu.max: %s", line)
123+
}
124+
125+
if bytes.Equal(parts[0], []byte("max")) {
126+
quotaUs = s.nproc * int(time.Second.Microseconds())
127+
} else {
128+
quotaUs, err = strconv.Atoi(string(parts[0]))
129+
if err != nil {
130+
return 0, xerrors.Errorf("parse /sys/fs/cgroup/cpu.max: %w", err)
131+
}
132+
}
133+
134+
return time.Duration(quotaUs) * time.Microsecond, nil
135+
}
136+
137+
func (*Statter) cGroupV1CPU() (time.Duration, time.Duration, error) {
138+
// TODO: implement
139+
return 0, 0, nil
9140
}
10141

11142
// ContainerMemory returns the memory usage of the container cgroup.
12-
// On non-Linux platforms, this always returns nil.
143+
// If the system is not containerized, this always returns nil.
13144
func (s *Statter) ContainerMemory() (*Result, error) {
145+
if ok, err := IsContainerized(s.fs); err != nil || !ok {
146+
return nil, nil
147+
}
148+
149+
if s.isCGroupV2() {
150+
return s.cGroupv2Memory()
151+
}
152+
153+
// Fall back to CGroupv1
154+
return s.cGroupv1Memory()
155+
}
156+
157+
func (*Statter) cGroupv2Memory() (*Result, error) {
158+
// TODO implement
159+
return nil, nil
160+
}
161+
162+
func (*Statter) cGroupv1Memory() (*Result, error) {
163+
// TODO implement
14164
return nil, nil
15165
}

cli/clistat/cgroup_linux.go

Lines changed: 0 additions & 163 deletions
This file was deleted.

cli/clistat/container.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,60 @@
22

33
package clistat
44

5-
import "github.com/spf13/afero"
5+
import (
6+
"bufio"
7+
"bytes"
8+
"os"
9+
10+
"github.com/spf13/afero"
11+
"golang.org/x/xerrors"
12+
)
13+
14+
const procMounts = "/proc/mounts"
15+
const procOneCgroup = "/proc/1/cgroup"
616

717
// IsContainerized returns whether the host is containerized.
818
// This is adapted from https://github.com/elastic/go-sysinfo/tree/main/providers/linux/container.go#L31
919
// with modifications to support Sysbox containers.
1020
// On non-Linux platforms, it always returns false.
11-
func IsContainerized(_ afero.Fs) (bool, error) {
21+
func IsContainerized(fs afero.Fs) (ok bool, err error) {
22+
cgData, err := afero.ReadFile(fs, procOneCgroup)
23+
if err != nil {
24+
if os.IsNotExist(err) {
25+
return false, nil // how?
26+
}
27+
return false, xerrors.Errorf("read file %s: %w", procOneCgroup, err)
28+
}
29+
30+
scn := bufio.NewScanner(bytes.NewReader(cgData))
31+
for scn.Scan() {
32+
line := scn.Bytes()
33+
if bytes.Contains(line, []byte("docker")) ||
34+
bytes.Contains(line, []byte(".slice")) ||
35+
bytes.Contains(line, []byte("lxc")) ||
36+
bytes.Contains(line, []byte("kubepods")) {
37+
return true, nil
38+
}
39+
}
40+
41+
// Last-ditch effort to detect Sysbox containers.
42+
// Check if we have anything mounted as type sysboxfs in /proc/mounts
43+
mountsData, err := afero.ReadFile(fs, procMounts)
44+
if err != nil {
45+
if os.IsNotExist(err) {
46+
return false, nil // how??
47+
}
48+
return false, xerrors.Errorf("read file %s: %w", procMounts, err)
49+
}
50+
51+
scn = bufio.NewScanner(bytes.NewReader(mountsData))
52+
for scn.Scan() {
53+
line := scn.Bytes()
54+
if bytes.Contains(line, []byte("sysboxfs")) {
55+
return true, nil
56+
}
57+
}
58+
59+
// If we get here, we are _probably_ not running in a container.
1260
return false, nil
1361
}

0 commit comments

Comments
 (0)