| // +build linux |
| |
| package systemd |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| systemd "github.com/coreos/go-systemd/dbus" |
| "github.com/docker/libcontainer/cgroups" |
| "github.com/docker/libcontainer/cgroups/fs" |
| "github.com/docker/libcontainer/configs" |
| "github.com/godbus/dbus" |
| ) |
| |
| type Manager struct { |
| Cgroups *configs.Cgroup |
| Paths map[string]string |
| } |
| |
| type subsystem interface { |
| // Returns the stats, as 'stats', corresponding to the cgroup under 'path'. |
| GetStats(path string, stats *cgroups.Stats) error |
| // Set the cgroup represented by cgroup. |
| Set(path string, cgroup *configs.Cgroup) error |
| } |
| |
| var subsystems = map[string]subsystem{ |
| "devices": &fs.DevicesGroup{}, |
| "memory": &fs.MemoryGroup{}, |
| "cpu": &fs.CpuGroup{}, |
| "cpuset": &fs.CpusetGroup{}, |
| "cpuacct": &fs.CpuacctGroup{}, |
| "blkio": &fs.BlkioGroup{}, |
| "perf_event": &fs.PerfEventGroup{}, |
| "freezer": &fs.FreezerGroup{}, |
| } |
| |
| const ( |
| testScopeWait = 4 |
| ) |
| |
| var ( |
| connLock sync.Mutex |
| theConn *systemd.Conn |
| hasStartTransientUnit bool |
| hasTransientDefaultDependencies bool |
| ) |
| |
| func newProp(name string, units interface{}) systemd.Property { |
| return systemd.Property{ |
| Name: name, |
| Value: dbus.MakeVariant(units), |
| } |
| } |
| |
| func UseSystemd() bool { |
| s, err := os.Stat("/run/systemd/system") |
| if err != nil || !s.IsDir() { |
| return false |
| } |
| |
| connLock.Lock() |
| defer connLock.Unlock() |
| |
| if theConn == nil { |
| var err error |
| theConn, err = systemd.New() |
| if err != nil { |
| return false |
| } |
| |
| // Assume we have StartTransientUnit |
| hasStartTransientUnit = true |
| |
| // But if we get UnknownMethod error we don't |
| if _, err := theConn.StartTransientUnit("test.scope", "invalid"); err != nil { |
| if dbusError, ok := err.(dbus.Error); ok { |
| if dbusError.Name == "org.freedesktop.DBus.Error.UnknownMethod" { |
| hasStartTransientUnit = false |
| return hasStartTransientUnit |
| } |
| } |
| } |
| |
| // Ensure the scope name we use doesn't exist. Use the Pid to |
| // avoid collisions between multiple libcontainer users on a |
| // single host. |
| scope := fmt.Sprintf("libcontainer-%d-systemd-test-default-dependencies.scope", os.Getpid()) |
| testScopeExists := true |
| for i := 0; i <= testScopeWait; i++ { |
| if _, err := theConn.StopUnit(scope, "replace"); err != nil { |
| if dbusError, ok := err.(dbus.Error); ok { |
| if strings.Contains(dbusError.Name, "org.freedesktop.systemd1.NoSuchUnit") { |
| testScopeExists = false |
| break |
| } |
| } |
| } |
| time.Sleep(time.Millisecond) |
| } |
| |
| // Bail out if we can't kill this scope without testing for DefaultDependencies |
| if testScopeExists { |
| return hasStartTransientUnit |
| } |
| |
| // Assume StartTransientUnit on a scope allows DefaultDependencies |
| hasTransientDefaultDependencies = true |
| ddf := newProp("DefaultDependencies", false) |
| if _, err := theConn.StartTransientUnit(scope, "replace", ddf); err != nil { |
| if dbusError, ok := err.(dbus.Error); ok { |
| if strings.Contains(dbusError.Name, "org.freedesktop.DBus.Error.PropertyReadOnly") { |
| hasTransientDefaultDependencies = false |
| } |
| } |
| } |
| |
| // Not critical because of the stop unit logic above. |
| theConn.StopUnit(scope, "replace") |
| } |
| return hasStartTransientUnit |
| } |
| |
| func getIfaceForUnit(unitName string) string { |
| if strings.HasSuffix(unitName, ".scope") { |
| return "Scope" |
| } |
| if strings.HasSuffix(unitName, ".service") { |
| return "Service" |
| } |
| return "Unit" |
| } |
| |
| func (m *Manager) Apply(pid int) error { |
| var ( |
| c = m.Cgroups |
| unitName = getUnitName(c) |
| slice = "system.slice" |
| properties []systemd.Property |
| ) |
| |
| if c.Slice != "" { |
| slice = c.Slice |
| } |
| |
| properties = append(properties, |
| systemd.PropSlice(slice), |
| systemd.PropDescription("docker container "+c.Name), |
| newProp("PIDs", []uint32{uint32(pid)}), |
| ) |
| |
| // Always enable accounting, this gets us the same behaviour as the fs implementation, |
| // plus the kernel has some problems with joining the memory cgroup at a later time. |
| properties = append(properties, |
| newProp("MemoryAccounting", true), |
| newProp("CPUAccounting", true), |
| newProp("BlockIOAccounting", true)) |
| |
| if hasTransientDefaultDependencies { |
| properties = append(properties, |
| newProp("DefaultDependencies", false)) |
| } |
| |
| if c.Memory != 0 { |
| properties = append(properties, |
| newProp("MemoryLimit", uint64(c.Memory))) |
| } |
| // TODO: MemoryReservation and MemorySwap not available in systemd |
| |
| if c.CpuShares != 0 { |
| properties = append(properties, |
| newProp("CPUShares", uint64(c.CpuShares))) |
| } |
| |
| if c.BlkioWeight != 0 { |
| properties = append(properties, |
| newProp("BlockIOWeight", uint64(c.BlkioWeight))) |
| } |
| |
| if _, err := theConn.StartTransientUnit(unitName, "replace", properties...); err != nil { |
| return err |
| } |
| |
| if err := joinDevices(c, pid); err != nil { |
| return err |
| } |
| |
| // TODO: CpuQuota and CpuPeriod not available in systemd |
| // we need to manually join the cpu.cfs_quota_us and cpu.cfs_period_us |
| if err := joinCpu(c, pid); err != nil { |
| return err |
| } |
| |
| // -1 disables memorySwap |
| if c.MemorySwap >= 0 && c.Memory != 0 { |
| if err := joinMemory(c, pid); err != nil { |
| return err |
| } |
| |
| } |
| |
| // we need to manually join the freezer and cpuset cgroup in systemd |
| // because it does not currently support it via the dbus api. |
| if err := joinFreezer(c, pid); err != nil { |
| return err |
| } |
| |
| if err := joinCpuset(c, pid); err != nil { |
| return err |
| } |
| |
| paths := make(map[string]string) |
| for sysname := range subsystems { |
| subsystemPath, err := getSubsystemPath(m.Cgroups, sysname) |
| if err != nil { |
| // Don't fail if a cgroup hierarchy was not found, just skip this subsystem |
| if cgroups.IsNotFound(err) { |
| continue |
| } |
| return err |
| } |
| paths[sysname] = subsystemPath |
| } |
| |
| m.Paths = paths |
| |
| return nil |
| } |
| |
| func (m *Manager) Destroy() error { |
| return cgroups.RemovePaths(m.Paths) |
| } |
| |
| func (m *Manager) GetPaths() map[string]string { |
| return m.Paths |
| } |
| |
| func writeFile(dir, file, data string) error { |
| return ioutil.WriteFile(filepath.Join(dir, file), []byte(data), 0700) |
| } |
| |
| func joinCpu(c *configs.Cgroup, pid int) error { |
| path, err := getSubsystemPath(c, "cpu") |
| if err != nil { |
| return err |
| } |
| if c.CpuQuota != 0 { |
| if err = ioutil.WriteFile(filepath.Join(path, "cpu.cfs_quota_us"), []byte(strconv.FormatInt(c.CpuQuota, 10)), 0700); err != nil { |
| return err |
| } |
| } |
| if c.CpuPeriod != 0 { |
| if err = ioutil.WriteFile(filepath.Join(path, "cpu.cfs_period_us"), []byte(strconv.FormatInt(c.CpuPeriod, 10)), 0700); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func joinFreezer(c *configs.Cgroup, pid int) error { |
| path, err := getSubsystemPath(c, "freezer") |
| if err != nil { |
| return err |
| } |
| |
| if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) { |
| return err |
| } |
| |
| return ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700) |
| } |
| |
| func getSubsystemPath(c *configs.Cgroup, subsystem string) (string, error) { |
| mountpoint, err := cgroups.FindCgroupMountpoint(subsystem) |
| if err != nil { |
| return "", err |
| } |
| |
| initPath, err := cgroups.GetInitCgroupDir(subsystem) |
| if err != nil { |
| return "", err |
| } |
| |
| slice := "system.slice" |
| if c.Slice != "" { |
| slice = c.Slice |
| } |
| |
| return filepath.Join(mountpoint, initPath, slice, getUnitName(c)), nil |
| } |
| |
| func (m *Manager) Freeze(state configs.FreezerState) error { |
| path, err := getSubsystemPath(m.Cgroups, "freezer") |
| if err != nil { |
| return err |
| } |
| |
| if err := ioutil.WriteFile(filepath.Join(path, "freezer.state"), []byte(state), 0); err != nil { |
| return err |
| } |
| for { |
| state_, err := ioutil.ReadFile(filepath.Join(path, "freezer.state")) |
| if err != nil { |
| return err |
| } |
| if string(state) == string(bytes.TrimSpace(state_)) { |
| break |
| } |
| time.Sleep(1 * time.Millisecond) |
| } |
| |
| m.Cgroups.Freezer = state |
| |
| return nil |
| } |
| |
| func (m *Manager) GetPids() ([]int, error) { |
| path, err := getSubsystemPath(m.Cgroups, "cpu") |
| if err != nil { |
| return nil, err |
| } |
| |
| return cgroups.ReadProcsFile(path) |
| } |
| |
| func (m *Manager) GetStats() (*cgroups.Stats, error) { |
| stats := cgroups.NewStats() |
| for name, path := range m.Paths { |
| sys, ok := subsystems[name] |
| if !ok || !cgroups.PathExists(path) { |
| continue |
| } |
| if err := sys.GetStats(path, stats); err != nil { |
| return nil, err |
| } |
| } |
| |
| return stats, nil |
| } |
| |
| func (m *Manager) Set(container *configs.Config) error { |
| panic("not implemented") |
| } |
| |
| func getUnitName(c *configs.Cgroup) string { |
| return fmt.Sprintf("%s-%s.scope", c.Parent, c.Name) |
| } |
| |
| // Atm we can't use the systemd device support because of two missing things: |
| // * Support for wildcards to allow mknod on any device |
| // * Support for wildcards to allow /dev/pts support |
| // |
| // The second is available in more recent systemd as "char-pts", but not in e.g. v208 which is |
| // in wide use. When both these are availalable we will be able to switch, but need to keep the old |
| // implementation for backwards compat. |
| // |
| // Note: we can't use systemd to set up the initial limits, and then change the cgroup |
| // because systemd will re-write the device settings if it needs to re-apply the cgroup context. |
| // This happens at least for v208 when any sibling unit is started. |
| func joinDevices(c *configs.Cgroup, pid int) error { |
| path, err := getSubsystemPath(c, "devices") |
| if err != nil { |
| return err |
| } |
| |
| if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) { |
| return err |
| } |
| |
| if err := ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700); err != nil { |
| return err |
| } |
| |
| if !c.AllowAllDevices { |
| if err := writeFile(path, "devices.deny", "a"); err != nil { |
| return err |
| } |
| } |
| for _, dev := range c.AllowedDevices { |
| if err := writeFile(path, "devices.allow", dev.CgroupString()); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // Symmetrical public function to update device based cgroups. Also available |
| // in the fs implementation. |
| func ApplyDevices(c *configs.Cgroup, pid int) error { |
| return joinDevices(c, pid) |
| } |
| |
| func joinMemory(c *configs.Cgroup, pid int) error { |
| memorySwap := c.MemorySwap |
| |
| if memorySwap == 0 { |
| // By default, MemorySwap is set to twice the size of RAM. |
| memorySwap = c.Memory * 2 |
| } |
| |
| path, err := getSubsystemPath(c, "memory") |
| if err != nil { |
| return err |
| } |
| |
| return ioutil.WriteFile(filepath.Join(path, "memory.memsw.limit_in_bytes"), []byte(strconv.FormatInt(memorySwap, 10)), 0700) |
| } |
| |
| // systemd does not atm set up the cpuset controller, so we must manually |
| // join it. Additionally that is a very finicky controller where each |
| // level must have a full setup as the default for a new directory is "no cpus" |
| func joinCpuset(c *configs.Cgroup, pid int) error { |
| path, err := getSubsystemPath(c, "cpuset") |
| if err != nil { |
| return err |
| } |
| |
| s := &fs.CpusetGroup{} |
| |
| return s.ApplyDir(path, c, pid) |
| } |