Move cgroups package into libcontainer

Docker-DCO-1.1-Signed-off-by: Michael Crosby <michael@crosbymichael.com> (github: crosbymichael)
This commit is contained in:
Michael Crosby 2014-05-14 15:21:44 -07:00
parent 9133caa6d3
commit b30f280d2f
25 changed files with 15 additions and 13 deletions

View file

@ -0,0 +1,155 @@
package fs
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/dotcloud/docker/pkg/libcontainer/cgroups"
)
var (
subsystems = map[string]subsystem{
"devices": &devicesGroup{},
"memory": &memoryGroup{},
"cpu": &cpuGroup{},
"cpuset": &cpusetGroup{},
"cpuacct": &cpuacctGroup{},
"blkio": &blkioGroup{},
"perf_event": &perfEventGroup{},
"freezer": &freezerGroup{},
}
)
type subsystem interface {
Set(*data) error
Remove(*data) error
Stats(*data) (map[string]float64, error)
}
type data struct {
root string
cgroup string
c *cgroups.Cgroup
pid int
}
func Apply(c *cgroups.Cgroup, pid int) (cgroups.ActiveCgroup, error) {
// We have two implementation of cgroups support, one is based on
// systemd and the dbus api, and one is based on raw cgroup fs operations
// following the pre-single-writer model docs at:
// http://www.freedesktop.org/wiki/Software/systemd/PaxControlGroups/
//
// we can pick any subsystem to find the root
cgroupRoot, err := cgroups.FindCgroupMountpoint("cpu")
if err != nil {
return nil, err
}
cgroupRoot = filepath.Dir(cgroupRoot)
if _, err := os.Stat(cgroupRoot); err != nil {
return nil, fmt.Errorf("cgroups fs not found")
}
cgroup := c.Name
if c.Parent != "" {
cgroup = filepath.Join(c.Parent, cgroup)
}
d := &data{
root: cgroupRoot,
cgroup: cgroup,
c: c,
pid: pid,
}
for _, sys := range subsystems {
if err := sys.Set(d); err != nil {
d.Cleanup()
return nil, err
}
}
return d, nil
}
func GetStats(c *cgroups.Cgroup, subsystem string, pid int) (map[string]float64, error) {
cgroupRoot, err := cgroups.FindCgroupMountpoint("cpu")
if err != nil {
return nil, err
}
cgroupRoot = filepath.Dir(cgroupRoot)
if _, err := os.Stat(cgroupRoot); err != nil {
return nil, fmt.Errorf("cgroups fs not found")
}
cgroup := c.Name
if c.Parent != "" {
cgroup = filepath.Join(c.Parent, cgroup)
}
d := &data{
root: cgroupRoot,
cgroup: cgroup,
c: c,
pid: pid,
}
sys, exists := subsystems[subsystem]
if !exists {
return nil, fmt.Errorf("subsystem %s does not exist", subsystem)
}
return sys.Stats(d)
}
func (raw *data) parent(subsystem string) (string, error) {
initPath, err := cgroups.GetInitCgroupDir(subsystem)
if err != nil {
return "", err
}
return filepath.Join(raw.root, subsystem, initPath), nil
}
func (raw *data) path(subsystem string) (string, error) {
parent, err := raw.parent(subsystem)
if err != nil {
return "", err
}
return filepath.Join(parent, raw.cgroup), nil
}
func (raw *data) join(subsystem string) (string, error) {
path, err := raw.path(subsystem)
if err != nil {
return "", err
}
if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) {
return "", err
}
if err := writeFile(path, "cgroup.procs", strconv.Itoa(raw.pid)); err != nil {
return "", err
}
return path, nil
}
func (raw *data) Cleanup() error {
for _, sys := range subsystems {
sys.Remove(raw)
}
return nil
}
func writeFile(dir, file, data string) error {
return ioutil.WriteFile(filepath.Join(dir, file), []byte(data), 0700)
}
func removePath(p string, err error) error {
if err != nil {
return err
}
if p != "" {
return os.RemoveAll(p)
}
return nil
}

View file

@ -0,0 +1,121 @@
package fs
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/dotcloud/docker/pkg/libcontainer/cgroups"
)
type blkioGroup struct {
}
func (s *blkioGroup) Set(d *data) error {
// we just want to join this group even though we don't set anything
if _, err := d.join("blkio"); err != nil && err != cgroups.ErrNotFound {
return err
}
return nil
}
func (s *blkioGroup) Remove(d *data) error {
return removePath(d.path("blkio"))
}
/*
examples:
blkio.sectors
8:0 6792
blkio.io_service_bytes
8:0 Read 1282048
8:0 Write 2195456
8:0 Sync 2195456
8:0 Async 1282048
8:0 Total 3477504
Total 3477504
blkio.io_serviced
8:0 Read 124
8:0 Write 104
8:0 Sync 104
8:0 Async 124
8:0 Total 228
Total 228
blkio.io_queued
8:0 Read 0
8:0 Write 0
8:0 Sync 0
8:0 Async 0
8:0 Total 0
Total 0
*/
func (s *blkioGroup) Stats(d *data) (map[string]float64, error) {
var (
paramData = make(map[string]float64)
params = []string{
"io_service_bytes_recursive",
"io_serviced_recursive",
"io_queued_recursive",
}
)
path, err := d.path("blkio")
if err != nil {
return nil, err
}
k, v, err := s.getSectors(path)
if err != nil {
return nil, err
}
paramData[fmt.Sprintf("blkio.sectors_recursive:%s", k)] = v
for _, param := range params {
f, err := os.Open(filepath.Join(path, fmt.Sprintf("blkio.%s", param)))
if err != nil {
return nil, err
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
// format: dev type amount
fields := strings.Fields(sc.Text())
switch len(fields) {
case 3:
v, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
return nil, err
}
paramData[fmt.Sprintf("%s:%s:%s", param, fields[0], fields[1])] = v
case 2:
// this is the total line, skip
default:
return nil, ErrNotValidFormat
}
}
}
return paramData, nil
}
func (s *blkioGroup) getSectors(path string) (string, float64, error) {
f, err := os.Open(filepath.Join(path, "blkio.sectors_recursive"))
if err != nil {
return "", 0, err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return "", 0, err
}
return getCgroupParamKeyValue(string(data))
}

View file

@ -0,0 +1,169 @@
package fs
import (
"testing"
)
const (
sectorsRecursiveContents = `8:0 1024`
serviceBytesRecursiveContents = `8:0 Read 100
8:0 Write 400
8:0 Sync 200
8:0 Async 300
8:0 Total 500
Total 500`
servicedRecursiveContents = `8:0 Read 10
8:0 Write 40
8:0 Sync 20
8:0 Async 30
8:0 Total 50
Total 50`
queuedRecursiveContents = `8:0 Read 1
8:0 Write 4
8:0 Sync 2
8:0 Async 3
8:0 Total 5
Total 5`
)
func TestBlkioStats(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_service_bytes_recursive": serviceBytesRecursiveContents,
"blkio.io_serviced_recursive": servicedRecursiveContents,
"blkio.io_queued_recursive": queuedRecursiveContents,
"blkio.sectors_recursive": sectorsRecursiveContents,
})
blkio := &blkioGroup{}
stats, err := blkio.Stats(helper.CgroupData)
if err != nil {
t.Fatal(err)
}
// Verify expected stats.
expectedStats := map[string]float64{
"blkio.sectors_recursive:8:0": 1024.0,
// Serviced bytes.
"io_service_bytes_recursive:8:0:Read": 100.0,
"io_service_bytes_recursive:8:0:Write": 400.0,
"io_service_bytes_recursive:8:0:Sync": 200.0,
"io_service_bytes_recursive:8:0:Async": 300.0,
"io_service_bytes_recursive:8:0:Total": 500.0,
// Serviced requests.
"io_serviced_recursive:8:0:Read": 10.0,
"io_serviced_recursive:8:0:Write": 40.0,
"io_serviced_recursive:8:0:Sync": 20.0,
"io_serviced_recursive:8:0:Async": 30.0,
"io_serviced_recursive:8:0:Total": 50.0,
// Queued requests.
"io_queued_recursive:8:0:Read": 1.0,
"io_queued_recursive:8:0:Write": 4.0,
"io_queued_recursive:8:0:Sync": 2.0,
"io_queued_recursive:8:0:Async": 3.0,
"io_queued_recursive:8:0:Total": 5.0,
}
expectStats(t, expectedStats, stats)
}
func TestBlkioStatsNoSectorsFile(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_service_bytes_recursive": serviceBytesRecursiveContents,
"blkio.io_serviced_recursive": servicedRecursiveContents,
"blkio.io_queued_recursive": queuedRecursiveContents,
})
blkio := &blkioGroup{}
_, err := blkio.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not")
}
}
func TestBlkioStatsNoServiceBytesFile(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_serviced_recursive": servicedRecursiveContents,
"blkio.io_queued_recursive": queuedRecursiveContents,
"blkio.sectors_recursive": sectorsRecursiveContents,
})
blkio := &blkioGroup{}
_, err := blkio.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not")
}
}
func TestBlkioStatsNoServicedFile(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_service_bytes_recursive": serviceBytesRecursiveContents,
"blkio.io_queued_recursive": queuedRecursiveContents,
"blkio.sectors_recursive": sectorsRecursiveContents,
})
blkio := &blkioGroup{}
_, err := blkio.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not")
}
}
func TestBlkioStatsNoQueuedFile(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_service_bytes_recursive": serviceBytesRecursiveContents,
"blkio.io_serviced_recursive": servicedRecursiveContents,
"blkio.sectors_recursive": sectorsRecursiveContents,
})
blkio := &blkioGroup{}
_, err := blkio.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not")
}
}
func TestBlkioStatsUnexpectedNumberOfFields(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_service_bytes_recursive": "8:0 Read 100 100",
"blkio.io_serviced_recursive": servicedRecursiveContents,
"blkio.io_queued_recursive": queuedRecursiveContents,
"blkio.sectors_recursive": sectorsRecursiveContents,
})
blkio := &blkioGroup{}
_, err := blkio.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not")
}
}
func TestBlkioStatsUnexpectedFieldType(t *testing.T) {
helper := NewCgroupTestUtil("blkio", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"blkio.io_service_bytes_recursive": "8:0 Read Write",
"blkio.io_serviced_recursive": servicedRecursiveContents,
"blkio.io_queued_recursive": queuedRecursiveContents,
"blkio.sectors_recursive": sectorsRecursiveContents,
})
blkio := &blkioGroup{}
_, err := blkio.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not")
}
}

View file

@ -0,0 +1,64 @@
package fs
import (
"bufio"
"os"
"path/filepath"
"strconv"
)
type cpuGroup struct {
}
func (s *cpuGroup) Set(d *data) error {
// We always want to join the cpu group, to allow fair cpu scheduling
// on a container basis
dir, err := d.join("cpu")
if err != nil {
return err
}
if d.c.CpuShares != 0 {
if err := writeFile(dir, "cpu.shares", strconv.FormatInt(d.c.CpuShares, 10)); err != nil {
return err
}
}
if d.c.CpuPeriod != 0 {
if err := writeFile(dir, "cpu.cfs_period_us", strconv.FormatInt(d.c.CpuPeriod, 10)); err != nil {
return err
}
}
if d.c.CpuQuota != 0 {
if err := writeFile(dir, "cpu.cfs_quota_us", strconv.FormatInt(d.c.CpuQuota, 10)); err != nil {
return err
}
}
return nil
}
func (s *cpuGroup) Remove(d *data) error {
return removePath(d.path("cpu"))
}
func (s *cpuGroup) Stats(d *data) (map[string]float64, error) {
paramData := make(map[string]float64)
path, err := d.path("cpu")
if err != nil {
return nil, err
}
f, err := os.Open(filepath.Join(path, "cpu.stat"))
if err != nil {
return nil, err
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
t, v, err := getCgroupParamKeyValue(sc.Text())
if err != nil {
return nil, err
}
paramData[t] = v
}
return paramData, nil
}

View file

@ -0,0 +1,57 @@
package fs
import (
"testing"
)
func TestCpuStats(t *testing.T) {
helper := NewCgroupTestUtil("cpu", t)
defer helper.cleanup()
cpuStatContent := `nr_periods 2000
nr_throttled 200
throttled_time 42424242424`
helper.writeFileContents(map[string]string{
"cpu.stat": cpuStatContent,
})
cpu := &cpuGroup{}
stats, err := cpu.Stats(helper.CgroupData)
if err != nil {
t.Fatal(err)
}
expected_stats := map[string]float64{
"nr_periods": 2000.0,
"nr_throttled": 200.0,
"throttled_time": 42424242424.0,
}
expectStats(t, expected_stats, stats)
}
func TestNoCpuStatFile(t *testing.T) {
helper := NewCgroupTestUtil("cpu", t)
defer helper.cleanup()
cpu := &cpuGroup{}
_, err := cpu.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected to fail, but did not.")
}
}
func TestInvalidCpuStat(t *testing.T) {
helper := NewCgroupTestUtil("cpu", t)
defer helper.cleanup()
cpuStatContent := `nr_periods 2000
nr_throttled 200
throttled_time fortytwo`
helper.writeFileContents(map[string]string{
"cpu.stat": cpuStatContent,
})
cpu := &cpuGroup{}
_, err := cpu.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failed stat parsing.")
}
}

View file

@ -0,0 +1,143 @@
package fs
import (
"bufio"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/dotcloud/docker/pkg/libcontainer/cgroups"
"github.com/dotcloud/docker/pkg/system"
)
var (
cpuCount = float64(runtime.NumCPU())
clockTicks = float64(system.GetClockTicks())
)
type cpuacctGroup struct {
}
func (s *cpuacctGroup) Set(d *data) error {
// we just want to join this group even though we don't set anything
if _, err := d.join("cpuacct"); err != nil && err != cgroups.ErrNotFound {
return err
}
return nil
}
func (s *cpuacctGroup) Remove(d *data) error {
return removePath(d.path("cpuacct"))
}
func (s *cpuacctGroup) Stats(d *data) (map[string]float64, error) {
var (
startCpu, lastCpu, startSystem, lastSystem, startUsage, lastUsage float64
percentage float64
paramData = make(map[string]float64)
)
path, err := d.path("cpuacct")
if startCpu, err = s.getCpuUsage(d, path); err != nil {
return nil, err
}
if startSystem, err = s.getSystemCpuUsage(d); err != nil {
return nil, err
}
startUsageTime := time.Now()
if startUsage, err = getCgroupParamFloat64(path, "cpuacct.usage"); err != nil {
return nil, err
}
// sample for 100ms
time.Sleep(100 * time.Millisecond)
if lastCpu, err = s.getCpuUsage(d, path); err != nil {
return nil, err
}
if lastSystem, err = s.getSystemCpuUsage(d); err != nil {
return nil, err
}
usageSampleDuration := time.Since(startUsageTime)
if lastUsage, err = getCgroupParamFloat64(path, "cpuacct.usage"); err != nil {
return nil, err
}
var (
deltaProc = lastCpu - startCpu
deltaSystem = lastSystem - startSystem
deltaUsage = lastUsage - startUsage
)
if deltaSystem > 0.0 {
percentage = ((deltaProc / deltaSystem) * clockTicks) * cpuCount
}
// NOTE: a percentage over 100% is valid for POSIX because that means the
// processes is using multiple cores
paramData["percentage"] = percentage
// Delta usage is in nanoseconds of CPU time so get the usage (in cores) over the sample time.
paramData["usage"] = deltaUsage / float64(usageSampleDuration.Nanoseconds())
return paramData, nil
}
func (s *cpuacctGroup) getProcStarttime(d *data) (float64, error) {
rawStart, err := system.GetProcessStartTime(d.pid)
if err != nil {
return 0, err
}
return strconv.ParseFloat(rawStart, 64)
}
func (s *cpuacctGroup) getSystemCpuUsage(d *data) (float64, error) {
f, err := os.Open("/proc/stat")
if err != nil {
return 0, err
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
parts := strings.Fields(sc.Text())
switch parts[0] {
case "cpu":
if len(parts) < 8 {
return 0, fmt.Errorf("invalid number of cpu fields")
}
var total float64
for _, i := range parts[1:8] {
v, err := strconv.ParseFloat(i, 64)
if err != nil {
return 0.0, fmt.Errorf("Unable to convert value %s to float: %s", i, err)
}
total += v
}
return total, nil
default:
continue
}
}
return 0, fmt.Errorf("invalid stat format")
}
func (s *cpuacctGroup) getCpuUsage(d *data, path string) (float64, error) {
cpuTotal := 0.0
f, err := os.Open(filepath.Join(path, "cpuacct.stat"))
if err != nil {
return 0.0, err
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
_, v, err := getCgroupParamKeyValue(sc.Text())
if err != nil {
return 0.0, err
}
// set the raw data in map
cpuTotal += v
}
return cpuTotal, nil
}

View file

@ -0,0 +1,108 @@
package fs
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strconv"
)
type cpusetGroup struct {
}
func (s *cpusetGroup) Set(d *data) error {
// we don't want to join this cgroup unless it is specified
if d.c.CpusetCpus != "" {
dir, err := d.path("cpuset")
if err != nil {
return err
}
if err := s.ensureParent(dir); err != nil {
return err
}
// because we are not using d.join we need to place the pid into the procs file
// unlike the other subsystems
if err := writeFile(dir, "cgroup.procs", strconv.Itoa(d.pid)); err != nil {
return err
}
if err := writeFile(dir, "cpuset.cpus", d.c.CpusetCpus); err != nil {
return err
}
}
return nil
}
func (s *cpusetGroup) Remove(d *data) error {
return removePath(d.path("cpuset"))
}
func (s *cpusetGroup) Stats(d *data) (map[string]float64, error) {
return nil, ErrNotSupportStat
}
func (s *cpusetGroup) getSubsystemSettings(parent string) (cpus []byte, mems []byte, err error) {
if cpus, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.cpus")); err != nil {
return
}
if mems, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.mems")); err != nil {
return
}
return cpus, mems, nil
}
// ensureParent ensures that the parent directory of current is created
// with the proper cpus and mems files copied from it's parent if the values
// are a file with a new line char
func (s *cpusetGroup) ensureParent(current string) error {
parent := filepath.Dir(current)
if _, err := os.Stat(parent); err != nil {
if !os.IsNotExist(err) {
return err
}
if err := s.ensureParent(parent); err != nil {
return err
}
}
if err := os.MkdirAll(current, 0755); err != nil && !os.IsExist(err) {
return err
}
return s.copyIfNeeded(current, parent)
}
// copyIfNeeded copies the cpuset.cpus and cpuset.mems from the parent
// directory to the current directory if the file's contents are 0
func (s *cpusetGroup) copyIfNeeded(current, parent string) error {
var (
err error
currentCpus, currentMems []byte
parentCpus, parentMems []byte
)
if currentCpus, currentMems, err = s.getSubsystemSettings(current); err != nil {
return err
}
if parentCpus, parentMems, err = s.getSubsystemSettings(parent); err != nil {
return err
}
if s.isEmpty(currentCpus) {
if err := writeFile(current, "cpuset.cpus", string(parentCpus)); err != nil {
return err
}
}
if s.isEmpty(currentMems) {
if err := writeFile(current, "cpuset.mems", string(parentMems)); err != nil {
return err
}
}
return nil
}
func (s *cpusetGroup) isEmpty(b []byte) bool {
return len(bytes.Trim(b, "\n")) == 0
}

View file

@ -0,0 +1,69 @@
package fs
import (
"os"
)
type devicesGroup struct {
}
func (s *devicesGroup) Set(d *data) error {
dir, err := d.join("devices")
if err != nil {
return err
}
defer func() {
if err != nil {
os.RemoveAll(dir)
}
}()
if !d.c.DeviceAccess {
if err := writeFile(dir, "devices.deny", "a"); err != nil {
return err
}
allow := []string{
// allow mknod for any device
"c *:* m",
"b *:* m",
// /dev/null, zero, full
"c 1:3 rwm",
"c 1:5 rwm",
"c 1:7 rwm",
// consoles
"c 5:1 rwm",
"c 5:0 rwm",
"c 4:0 rwm",
"c 4:1 rwm",
// /dev/urandom,/dev/random
"c 1:9 rwm",
"c 1:8 rwm",
// /dev/pts/ - pts namespaces are "coming soon"
"c 136:* rwm",
"c 5:2 rwm",
// tuntap
"c 10:200 rwm",
}
for _, val := range allow {
if err := writeFile(dir, "devices.allow", val); err != nil {
return err
}
}
}
return nil
}
func (s *devicesGroup) Remove(d *data) error {
return removePath(d.path("devices"))
}
func (s *devicesGroup) Stats(d *data) (map[string]float64, error) {
return nil, ErrNotSupportStat
}

View file

@ -0,0 +1,72 @@
package fs
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/dotcloud/docker/pkg/libcontainer/cgroups"
)
type freezerGroup struct {
}
func (s *freezerGroup) Set(d *data) error {
dir, err := d.join("freezer")
if err != nil {
if err != cgroups.ErrNotFound {
return err
}
return nil
}
if d.c.Freezer != "" {
if err := writeFile(dir, "freezer.state", d.c.Freezer); err != nil {
return err
}
}
return nil
}
func (s *freezerGroup) Remove(d *data) error {
return removePath(d.path("freezer"))
}
func (s *freezerGroup) Stats(d *data) (map[string]float64, error) {
var (
paramData = make(map[string]float64)
params = []string{
"parent_freezing",
"self_freezing",
// comment out right now because this is string "state",
}
)
path, err := d.path("freezer")
if err != nil {
return nil, err
}
for _, param := range params {
f, err := os.Open(filepath.Join(path, fmt.Sprintf("freezer.%s", param)))
if err != nil {
return nil, err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
v, err := strconv.ParseFloat(strings.TrimSuffix(string(data), "\n"), 64)
if err != nil {
return nil, err
}
paramData[param] = v
}
return paramData, nil
}

View file

@ -0,0 +1,90 @@
package fs
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
)
type memoryGroup struct {
}
func (s *memoryGroup) Set(d *data) error {
dir, err := d.join("memory")
// only return an error for memory if it was not specified
if err != nil && (d.c.Memory != 0 || d.c.MemoryReservation != 0 || d.c.MemorySwap != 0) {
return err
}
defer func() {
if err != nil {
os.RemoveAll(dir)
}
}()
// Only set values if some config was specified.
if d.c.Memory != 0 || d.c.MemoryReservation != 0 || d.c.MemorySwap != 0 {
if d.c.Memory != 0 {
if err := writeFile(dir, "memory.limit_in_bytes", strconv.FormatInt(d.c.Memory, 10)); err != nil {
return err
}
}
if d.c.MemoryReservation != 0 {
if err := writeFile(dir, "memory.soft_limit_in_bytes", strconv.FormatInt(d.c.MemoryReservation, 10)); err != nil {
return err
}
}
// By default, MemorySwap is set to twice the size of RAM.
// If you want to omit MemorySwap, set it to `-1'.
if d.c.MemorySwap != -1 {
if err := writeFile(dir, "memory.memsw.limit_in_bytes", strconv.FormatInt(d.c.Memory*2, 10)); err != nil {
return err
}
}
}
return nil
}
func (s *memoryGroup) Remove(d *data) error {
return removePath(d.path("memory"))
}
func (s *memoryGroup) Stats(d *data) (map[string]float64, error) {
paramData := make(map[string]float64)
path, err := d.path("memory")
if err != nil {
return nil, err
}
// Set stats from memory.stat.
statsFile, err := os.Open(filepath.Join(path, "memory.stat"))
if err != nil {
return nil, err
}
defer statsFile.Close()
sc := bufio.NewScanner(statsFile)
for sc.Scan() {
t, v, err := getCgroupParamKeyValue(sc.Text())
if err != nil {
return nil, err
}
paramData[t] = v
}
// Set memory usage and max historical usage.
params := []string{
"usage_in_bytes",
"max_usage_in_bytes",
}
for _, param := range params {
value, err := getCgroupParamFloat64(path, fmt.Sprintf("memory.%s", param))
if err != nil {
return nil, err
}
paramData[param] = value
}
return paramData, nil
}

View file

@ -0,0 +1,123 @@
package fs
import (
"testing"
)
const (
memoryStatContents = `cache 512
rss 1024`
memoryUsageContents = "2048\n"
memoryMaxUsageContents = "4096\n"
)
func TestMemoryStats(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.stat": memoryStatContents,
"memory.usage_in_bytes": memoryUsageContents,
"memory.max_usage_in_bytes": memoryMaxUsageContents,
})
memory := &memoryGroup{}
stats, err := memory.Stats(helper.CgroupData)
if err != nil {
t.Fatal(err)
}
expectedStats := map[string]float64{"cache": 512.0, "rss": 1024.0, "usage_in_bytes": 2048.0, "max_usage_in_bytes": 4096.0}
expectStats(t, expectedStats, stats)
}
func TestMemoryStatsNoStatFile(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.usage_in_bytes": memoryUsageContents,
"memory.max_usage_in_bytes": memoryMaxUsageContents,
})
memory := &memoryGroup{}
_, err := memory.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failure")
}
}
func TestMemoryStatsNoUsageFile(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.stat": memoryStatContents,
"memory.max_usage_in_bytes": memoryMaxUsageContents,
})
memory := &memoryGroup{}
_, err := memory.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failure")
}
}
func TestMemoryStatsNoMaxUsageFile(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.stat": memoryStatContents,
"memory.usage_in_bytes": memoryUsageContents,
})
memory := &memoryGroup{}
_, err := memory.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failure")
}
}
func TestMemoryStatsBadStatFile(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.stat": "rss rss",
"memory.usage_in_bytes": memoryUsageContents,
"memory.max_usage_in_bytes": memoryMaxUsageContents,
})
memory := &memoryGroup{}
_, err := memory.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failure")
}
}
func TestMemoryStatsBadUsageFile(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.stat": memoryStatContents,
"memory.usage_in_bytes": "bad",
"memory.max_usage_in_bytes": memoryMaxUsageContents,
})
memory := &memoryGroup{}
_, err := memory.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failure")
}
}
func TestMemoryStatsBadMaxUsageFile(t *testing.T) {
helper := NewCgroupTestUtil("memory", t)
defer helper.cleanup()
helper.writeFileContents(map[string]string{
"memory.stat": memoryStatContents,
"memory.usage_in_bytes": memoryUsageContents,
"memory.max_usage_in_bytes": "bad",
})
memory := &memoryGroup{}
_, err := memory.Stats(helper.CgroupData)
if err == nil {
t.Fatal("Expected failure")
}
}

View file

@ -0,0 +1,24 @@
package fs
import (
"github.com/dotcloud/docker/pkg/libcontainer/cgroups"
)
type perfEventGroup struct {
}
func (s *perfEventGroup) Set(d *data) error {
// we just want to join this group even though we don't set anything
if _, err := d.join("perf_event"); err != nil && err != cgroups.ErrNotFound {
return err
}
return nil
}
func (s *perfEventGroup) Remove(d *data) error {
return removePath(d.path("perf_event"))
}
func (s *perfEventGroup) Stats(d *data) (map[string]float64, error) {
return nil, ErrNotSupportStat
}

View file

@ -0,0 +1,75 @@
/*
Utility for testing cgroup operations.
Creates a mock of the cgroup filesystem for the duration of the test.
*/
package fs
import (
"fmt"
"io/ioutil"
"log"
"os"
"testing"
)
type cgroupTestUtil struct {
// data to use in tests.
CgroupData *data
// Path to the mock cgroup directory.
CgroupPath string
// Temporary directory to store mock cgroup filesystem.
tempDir string
t *testing.T
}
// Creates a new test util for the specified subsystem
func NewCgroupTestUtil(subsystem string, t *testing.T) *cgroupTestUtil {
d := &data{}
tempDir, err := ioutil.TempDir("", fmt.Sprintf("%s_cgroup_test", subsystem))
if err != nil {
t.Fatal(err)
}
d.root = tempDir
testCgroupPath, err := d.path(subsystem)
if err != nil {
t.Fatal(err)
}
// Ensure the full mock cgroup path exists.
err = os.MkdirAll(testCgroupPath, 0755)
if err != nil {
t.Fatal(err)
}
return &cgroupTestUtil{CgroupData: d, CgroupPath: testCgroupPath, tempDir: tempDir, t: t}
}
func (c *cgroupTestUtil) cleanup() {
os.RemoveAll(c.tempDir)
}
// Write the specified contents on the mock of the specified cgroup files.
func (c *cgroupTestUtil) writeFileContents(fileContents map[string]string) {
for file, contents := range fileContents {
err := writeFile(c.CgroupPath, file, contents)
if err != nil {
c.t.Fatal(err)
}
}
}
// Expect the specified stats.
func expectStats(t *testing.T, expected, actual map[string]float64) {
for stat, expectedValue := range expected {
actualValue, ok := actual[stat]
if !ok {
log.Printf("Expected stat %s to exist: %s", stat, actual)
t.Fail()
} else if actualValue != expectedValue {
log.Printf("Expected stats %s to have value %f but had %f instead", stat, expectedValue, actualValue)
t.Fail()
}
}
}

View file

@ -0,0 +1,40 @@
package fs
import (
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strconv"
"strings"
)
var (
ErrNotSupportStat = errors.New("stats are not supported for subsystem")
ErrNotValidFormat = errors.New("line is not a valid key value format")
)
// Parses a cgroup param and returns as name, value
// i.e. "io_service_bytes 1234" will return as io_service_bytes, 1234
func getCgroupParamKeyValue(t string) (string, float64, error) {
parts := strings.Fields(t)
switch len(parts) {
case 2:
value, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return "", 0.0, fmt.Errorf("Unable to convert param value to float: %s", err)
}
return parts[0], value, nil
default:
return "", 0.0, ErrNotValidFormat
}
}
// Gets a single float64 value from the specified cgroup file.
func getCgroupParamFloat64(cgroupPath, cgroupFile string) (float64, error) {
contents, err := ioutil.ReadFile(filepath.Join(cgroupPath, cgroupFile))
if err != nil {
return -1.0, err
}
return strconv.ParseFloat(strings.TrimSpace(string(contents)), 64)
}

View file

@ -0,0 +1,68 @@
package fs
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
const (
cgroupFile = "cgroup.file"
floatValue = 2048.0
floatString = "2048"
)
func TestGetCgroupParamsFloat64(t *testing.T) {
// Setup tempdir.
tempDir, err := ioutil.TempDir("", "cgroup_utils_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
tempFile := filepath.Join(tempDir, cgroupFile)
// Success.
err = ioutil.WriteFile(tempFile, []byte(floatString), 0755)
if err != nil {
t.Fatal(err)
}
value, err := getCgroupParamFloat64(tempDir, cgroupFile)
if err != nil {
t.Fatal(err)
} else if value != floatValue {
t.Fatalf("Expected %f to equal %f", value, floatValue)
}
// Success with new line.
err = ioutil.WriteFile(tempFile, []byte(floatString+"\n"), 0755)
if err != nil {
t.Fatal(err)
}
value, err = getCgroupParamFloat64(tempDir, cgroupFile)
if err != nil {
t.Fatal(err)
} else if value != floatValue {
t.Fatalf("Expected %f to equal %f", value, floatValue)
}
// Not a float.
err = ioutil.WriteFile(tempFile, []byte("not-a-float"), 0755)
if err != nil {
t.Fatal(err)
}
_, err = getCgroupParamFloat64(tempDir, cgroupFile)
if err == nil {
t.Fatal("Expecting error, got none")
}
// Unknown file.
err = os.Remove(tempFile)
if err != nil {
t.Fatal(err)
}
_, err = getCgroupParamFloat64(tempDir, cgroupFile)
if err == nil {
t.Fatal("Expecting error, got none")
}
}