Files
docker/daemon/execdriver/clr/driver.go
2016-02-12 13:26:08 +00:00

965 lines
23 KiB
Go

// +build linux
package clr
import (
"crypto/sha512"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/execdriver"
"github.com/docker/docker/pkg/mount"
sysinfo "github.com/docker/docker/pkg/system"
"github.com/docker/docker/pkg/term"
"github.com/docker/libnetwork/netlabel"
"github.com/kr/pty"
"github.com/opencontainers/runc/libcontainer/configs"
)
const (
// Clear Linux for Intel(R) Architecture
driverName = "clr"
envVarPrefix = "CLR_"
// variable that allows clrURL to be modified
clrEnvURL = envVarPrefix + "DOWNLOAD_URL"
// Command used for lkvm control
lkvmName = "lkvm"
// default value for clrURL
defaultClrURL = "https://download.clearlinux.org"
// upstream latest release file (found below clrURL)
latestFile = "latest"
// local "latest" information
clrFile = "latest"
// ASCII file containing a checksums for the downloaded image.
clrChecksumFile = "SHA512SUMS"
// clr kernel (not bzimage)
clrKernel = "/usr/lib/kernel/vmlinux.container"
)
var (
// upstream base URL
clrURL = defaultClrURL
)
type driver struct {
root string // root path for the driver to use
libPath string
initPath string
version string
apparmor bool
sharedRoot bool
activeContainers map[string]*activeContainer
machineMemory int64
containerPid int
sync.Mutex
ipaddr string
gateway string
macaddr string
}
type activeContainer struct {
container *configs.Config
cmd *exec.Cmd
}
type ProgressReader struct {
// The real reader
io.Reader
// Total size of download
total uint64
// Bytes received
bytes uint64
// Rate-limiting counter
lastShown int
rawURL string
}
// Read implements the Reader interface by calling the _real_ reader
// and periodically displaying progress information.
func (r *ProgressReader) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if err != nil && err != io.EOF {
// A true error occured
return n, err
}
r.bytes += uint64(n)
if r.total == 0 {
// http header did not provide size
logrus.Debugf("Read %v bytes from %v", r.bytes, r.rawURL)
} else {
percent := (float64(r.bytes) / float64(r.total)) * 100
// rate-limit to only display messages for every 10% downloaded
next := int(percent / 10)
if r.lastShown == -1 || next > r.lastShown {
logrus.Debugf("Read %v of %v bytes from %v (%2.2f%%)", r.bytes, r.total, r.rawURL, percent)
r.lastShown = next
}
}
return n, err
}
func getTapIf(c *execdriver.Command) string {
return fmt.Sprintf("tb-%s", c.ID[:12])
}
// getClrVersion reads the latest image version from the
// locally-downloaded "latest" file.
func getClrVersion(libPath string) string {
txt, err := ioutil.ReadFile(path.Join(libPath, clrFile))
if err != nil {
return ""
}
return strings.Split(string(txt), "\n")[0]
}
func getURL(rawURL, outfile string) error {
_url, err := url.Parse(rawURL)
if err != nil {
return err
}
out, err := os.Create(outfile)
if err != nil {
return err
}
defer out.Close()
transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
client := &http.Client{Transport: transport}
// Determine size of download by consulting the headers
response, err := client.Head(rawURL)
if err != nil {
return err
}
lenStr := response.Header.Get("Content-Length")
dataLen, _ := strconv.Atoi(lenStr)
request, err := http.NewRequest("GET", _url.String(), nil)
if err != nil {
return err
}
response, err = client.Do(request)
defer response.Body.Close()
reader := &ProgressReader{
Reader: response.Body,
total: uint64(dataLen),
lastShown: -1,
rawURL: rawURL,
}
_, err = io.Copy(out, reader)
return err
}
// fetchLatest downloads the "latest" file which contains the version
// number of the latest image.
func fetchLatest(libPath string) error {
file := path.Join(libPath, clrFile)
return getURL(clrURL+"/"+latestFile, file)
}
// NewDriver creates a new clear linux execution driver.
func NewDriver(root, libPath, initPath string, apparmor bool) (*driver, error) {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
meminfo, err := sysinfo.ReadMemInfo()
if err != nil {
return nil, err
}
version, err := prepareClr(libPath)
if err != nil {
return nil, err
}
return &driver{
apparmor: apparmor,
root: root,
libPath: libPath,
initPath: initPath,
version: version,
sharedRoot: false,
activeContainers: make(map[string]*activeContainer),
// FIXME:
machineMemory: meminfo.MemTotal,
}, nil
}
// fetchImage downloads the latest image and verifies its checksum.
func fetchImage(version, outfile, outpath string) error {
url := fmt.Sprintf("%s/releases/%s/clear/%s", clrURL, version, outfile)
logrus.Debugf("Fetching clr version: %s, %s", version, outpath)
return getURL(url, outpath)
}
// uncompressImage handles decompressing the image specified by path.
func uncompressImage(path string) error {
// decompress the file
cmd := exec.Command("unxz", path)
cmd.Dir = filepath.Dir(path)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("Unable to extract image %s: %s", path, output)
}
return nil
}
// getChecksumLine reads the file specified and returns the checksum for
// the filename specified.
func getChecksumLine(file string, filename string) string {
data, err := ioutil.ReadFile(file)
if err != nil {
return ""
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasSuffix(line, filename) {
fields := strings.Fields(line)
if len(fields) == 2 {
return fields[0]
}
}
}
return ""
}
// getChecksum downloads the checksum file for the image version specified,
// storing it under libPath, then extracts the checksum corresponding to
// imageFile and returns it.
func getChecksum(version string, libPath string, imageFile string) (string, error) {
outfile := path.Join(libPath, clrChecksumFile)
checkurl := fmt.Sprintf("%s/releases/%s/clear/%s", clrURL, version, clrChecksumFile)
err := getURL(checkurl, outfile)
if err != nil {
return "", err
}
// Now, extract the checksum from the downloaded file.
return getChecksumLine(outfile, imageFile), nil
}
func verifyChecksum(fullPath, expectedChecksum string) error {
if expectedChecksum == "" {
return errors.New("blank expectedChecksum")
}
if fullPath == "" {
return errors.New("blank path")
}
data, err := ioutil.ReadFile(fullPath)
if err != nil {
return err
}
calculatedChecksum := fmt.Sprintf("%x", sha512.Sum512(data))
if calculatedChecksum != expectedChecksum {
return fmt.Errorf("Checksum mismatch: %v %v", calculatedChecksum, expectedChecksum)
}
logrus.Debugf("Checksum for %v correct: %v", fullPath, expectedChecksum)
return nil
}
// alreadyDownloaded determines if the uncompressed version of the file
// specified by outpath already exists and seems sane.
func alreadyDownloaded(outpath string) bool {
// strip off the (first) extension
base := path.Base(outpath)
extension := filepath.Ext(base)
uncompressed := strings.TrimSuffix(outpath, extension)
st, err := os.Stat(uncompressed)
if err != nil {
return false
}
if st.IsDir() {
return false
}
if st.Size() == 0 {
return false
}
return true
}
// prepareClr handles downloading and checking clr images, returning the
// version of clr that will be used.
func prepareClr(libPath string) (string, error) {
logrus.Debugf("%s preparing environment", driverName)
tmp := os.Getenv(clrEnvURL)
if tmp != "" {
clrURL = tmp
logrus.Debugf("%s using alternate download URL: %s", driverName, clrURL)
}
var version = getClrVersion(libPath)
err := fetchLatest(libPath)
if err != nil {
return "", err
}
nversion := getClrVersion(libPath)
if nversion == "" {
return "", errors.New("unable to determine latest clr version")
}
outfile := fmt.Sprintf("clear-%s-containers.img.xz", nversion)
outpath := path.Join(libPath, outfile)
// If the download originally failed, there may be a partial .xz
// file. However, this will be ignored and a new download triggered
// since we only only check for the uncompressed file (which would
// only exist if the download was successful).
if alreadyDownloaded(outpath) && nversion == version {
logrus.Debugf("Using clr version: %s", nversion)
return nversion, nil
}
// Determine the expected checksum for the file about to be
// downloaded.
checksum, err := getChecksum(nversion, libPath, outfile)
if err != nil {
return "", err
}
if nversion != version && version != "" {
logrus.Debugf("Updating to clr version: %s", nversion)
} else if version == "" {
logrus.Debugf("Installing clr version: %s", nversion)
}
err = fetchImage(nversion, outfile, outpath)
if err != nil {
return "", err
}
if err = verifyChecksum(outpath, checksum); err != nil {
return "", err
}
err = uncompressImage(outpath)
if err != nil {
return "", err
}
return nversion, nil
}
func (d *driver) Name() string {
return fmt.Sprintf("%s-%s", driverName, d.version)
}
func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, hooks execdriver.Hooks) (execdriver.ExitStatus, error) {
var (
term execdriver.Terminal
err error
)
container, err := d.createContainer(c)
if err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
memoryMiB := c.HostConfig.Memory
if memoryMiB == 0 {
memoryMiB = 1024
} else {
// docker passes the value as bytes
memoryMiB = memoryMiB / int64(math.Pow(2, 20))
}
workingDirVar := fmt.Sprintf("%s%s=%q", envVarPrefix, "WORKINGDIR", c.WorkingDir)
c.ProcessConfig.Cmd.Env = append(c.ProcessConfig.Cmd.Env, workingDirVar)
userVar := fmt.Sprintf("%s%s=%q", envVarPrefix, "USER", c.ProcessConfig.User)
c.ProcessConfig.Cmd.Env = append(c.ProcessConfig.Cmd.Env, userVar)
if err := d.setupNetwork(c); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
if c.ProcessConfig.Tty {
term, err = NewTtyConsole(&c.ProcessConfig, pipes)
} else {
term, err = execdriver.NewStdConsole(&c.ProcessConfig, pipes)
}
if err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
c.ProcessConfig.Terminal = term
d.Lock()
d.activeContainers[c.ID] = &activeContainer{
container: container,
cmd: &c.ProcessConfig.Cmd,
}
d.Unlock()
if err := d.generateEnvConfig(c); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
if err := d.generateDockerInit(c); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
for _, m := range c.Mounts {
dest := path.Join(c.Rootfs, m.Destination)
if m.Destination == "/etc/hostname" {
continue
}
if !pathExists(m.Source) {
continue
}
opts := "bind"
if m.Private {
opts = opts + ",rprivate"
}
if m.Slave {
opts = opts + ",rslave"
}
// This may look racy, but it isn't since the VM isn't
// running yet.
//
// The check is necessary to handle bind mounting of
// regular files correctly since without it we may be
// attempting to create a directory where there already
// exists a normal file.
if !pathExists(dest) {
if err := os.MkdirAll(dest, 0750); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
}
if err := mount.Mount(m.Source, dest, "", opts); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
if !m.Writable {
if err := mount.Mount("", dest, "", "bind,remount,ro"); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
}
defer mount.Unmount(dest)
}
var args []string
// various things for lkvm
ifname := getTapIf(c)
// FIXME: Should be real hostname from like process/container struct
hostname := c.ID[0:12]
img := fmt.Sprintf("%s/clear-%s-containers.img", d.libPath, d.version)
memory := fmt.Sprintf("%d", memoryMiB)
// FIXME: Locked cores to 6 ?
cores := fmt.Sprintf("%d", 6)
args = append(args, c.ProcessConfig.Entrypoint)
args = append(args, c.ProcessConfig.Arguments...)
rootParams := fmt.Sprintf("root=/dev/plkvm0p1 rootfstype=ext4 rootflags=dax,data=ordered "+
"init=/usr/lib/systemd/systemd systemd.unit=container.target rw tsc=reliable "+
"systemd.show_status=false "+
"no_timer_check rcupdate.rcu_expedited=1 console=hvc0 quiet ip=%s::%s::%s::off",
d.ipaddr, d.gateway, hostname)
params := []string{
lkvmName, "run", "-c", cores, "-m", memory,
"--name", c.ID, "--console", "virtio",
"--kernel", clrKernel,
"--params", rootParams,
"--shmem", fmt.Sprintf("0x200000000:0:file=%s:private", img),
"--network", fmt.Sprintf("mode=tap,script=none,tapif=%s,guest_mac=%s", ifname, d.macaddr),
"--9p", fmt.Sprintf("%s,rootfs", c.Rootfs),
}
logrus.Debugf("%s params %s", driverName, params)
var (
name = params[0]
arg = params[1:]
)
aname, err := exec.LookPath(name)
if err != nil {
aname = name
}
c.ProcessConfig.Path = aname
c.ProcessConfig.Args = append([]string{name}, arg...)
c.ProcessConfig.Env = []string{fmt.Sprintf("HOME=%s", d.root)}
// Start the container. Since it runs synchronously, we don't Wait()
// for it since we need to check the status to determine if it did
// actually start successfully.
if err := c.ProcessConfig.Start(); err != nil {
return execdriver.ExitStatus{ExitCode: -1}, err
}
var (
waitErr error
waitLock = make(chan struct{})
)
go func() {
if err := c.ProcessConfig.Wait(); err != nil {
if _, ok := err.(*exec.ExitError); !ok { // Do not propagate the error if it's simply a status code != 0
waitErr = err
}
}
close(waitLock)
}()
// FIXME: need to create state.json for Stats() to work.
pid := c.ProcessConfig.Process.Pid
c.ContainerPid = pid
d.containerPid = pid
if hooks.Start != nil {
logrus.Debugf("Invoking startCallback")
chOOM := make(chan struct{})
close(chOOM)
hooks.Start(&c.ProcessConfig, pid, chOOM)
}
// FIXME:
oomKill := false
// Wait for the VM to shutdown
<-waitLock
exitCode := getExitCode(c)
cExitStatus, cerr := d.cleanupVM(c)
if cerr != nil {
waitErr = cerr
exitCode = cExitStatus
}
// check oom error
if oomKill {
exitCode = 137
}
return execdriver.ExitStatus{ExitCode: exitCode, OOMKilled: false}, waitErr
}
func pathExists(path string) bool {
if _, err := os.Stat(path); err == nil {
return true
}
return false
}
func pathExecutable(path string) bool {
s, err := os.Stat(path)
if err != nil {
return false
}
mode := s.Mode()
if mode&0111 != 0 {
return true
}
return false
}
func (d *driver) cleanupVM(c *execdriver.Command) (exitStatus int, err error) {
cmd := exec.Command("ip", "tuntap", "del", "dev", getTapIf(c), "mode", "tap")
var output []byte
if output, err = cmd.CombinedOutput(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitStatus = waitStatus.ExitStatus()
}
logrus.Debugf("teardown failed for vm %s: %s (%s)", c.ID, string(output), err.Error())
}
// doesn't matter if this fails
// lkvm could have removed it, and stale sockets are not fatal
_ = os.Remove(fmt.Sprintf("%s/.lkvm/%s.sock", d.root, c.ID))
return exitStatus, err
}
// createContainer populates and configures the container type with the
// data provided by the execdriver.Command
func (d *driver) createContainer(c *execdriver.Command) (*configs.Config, error) {
return execdriver.InitContainer(c), nil
}
/// Return the exit code of the process
// if the process has not exited -1 will be returned
func getExitCode(c *execdriver.Command) int {
if c.ProcessConfig.ProcessState == nil {
return -1
}
return c.ProcessConfig.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
}
func (d *driver) lkvmCommand(c *execdriver.Command, arg string) ([]byte, error) {
args := append([]string{lkvmName}, arg)
if c != nil {
args = append(args, "--name", c.ID)
}
cmd := exec.Command(lkvmName, args...)
cmd.Env = []string{fmt.Sprintf("HOME=%s", d.root)}
return cmd.Output()
}
// Kill sends a signal to workload
func (d *driver) Kill(c *execdriver.Command, sig int) error {
// Not supported
return nil
}
func (d *driver) Pause(c *execdriver.Command) error {
_, err := d.lkvmCommand(c, "pause")
return err
}
func (d *driver) Unpause(c *execdriver.Command) error {
_, err := d.lkvmCommand(c, "resume")
return err
}
// Terminate forcibly stops a container
func (d *driver) Terminate(c *execdriver.Command) error {
_, err := d.lkvmCommand(c, "stop")
return err
}
func (d *driver) containerDir(containerID string) string {
return path.Join(d.libPath, "containers", containerID)
}
// isDigit returns true if s can be represented as an integer
func isDigit(s string) bool {
if _, err := strconv.Atoi(s); err == nil {
return true
}
return false
}
func (d *driver) getInfo(id string) ([]byte, error) {
output, err := d.lkvmCommand(nil, "list")
if err != nil {
return nil, err
}
for i, line := range strings.Split(string(output), "\n") {
if i < 2 {
continue
}
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) != 3 {
continue
}
if !isDigit(fields[0]) {
continue
}
if fields[1] != id {
continue
}
return []byte(line), nil
}
return []byte(fmt.Sprintf("-1 %s stopped", id)), nil
}
type info struct {
ID string
driver *driver
}
func (i *info) IsRunning() bool {
output, err := i.driver.getInfo(i.ID)
if err != nil {
logrus.Errorf("Error getting info for %s container %s: %s (%s)",
driverName, i.ID, err, output)
return false
}
clrInfo, err := parseClrInfo(i.ID, string(output))
if err != nil {
return false
}
return clrInfo.Running
}
func (d *driver) Info(id string) execdriver.Info {
return &info{
ID: id,
driver: d,
}
}
func (d *driver) GetPidsForContainer(id string) ([]int, error) {
// The VM doesn't expose the worload pid(s), so the only meaningful
// pid is that of the VM
return []int{d.containerPid}, nil
}
// TtyConsole is a type to represent a pseud-oterminal (see pty(7))
type TtyConsole struct {
MasterPty *os.File
SlavePty *os.File
}
// NewTtyConsole returns a new TtyConsole object.
func NewTtyConsole(processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes) (*TtyConsole, error) {
// lxc is special in that we cannot create the master outside of the container without
// opening the slave because we have nothing to provide to the cmd. We have to open both then do
// the crazy setup on command right now instead of passing the console path to lxc and telling it
// to open up that console. we save a couple of openfiles in the native driver because we can do
// this.
ptyMaster, ptySlave, err := pty.Open()
if err != nil {
return nil, err
}
tty := &TtyConsole{
MasterPty: ptyMaster,
SlavePty: ptySlave,
}
if err := tty.AttachPipes(&processConfig.Cmd, pipes); err != nil {
tty.Close()
return nil, err
}
processConfig.Console = tty.SlavePty.Name()
return tty, nil
}
// Master returns the master end of the pty
func (t *TtyConsole) Master() *os.File {
return t.MasterPty
}
// Resize modifies the size of the pty terminal being used.
func (t *TtyConsole) Resize(h, w int) error {
return term.SetWinsize(t.MasterPty.Fd(), &term.Winsize{Height: uint16(h), Width: uint16(w)})
}
// AttachPipes associates the specified pipes with the pty master.
func (t *TtyConsole) AttachPipes(command *exec.Cmd, pipes *execdriver.Pipes) error {
command.Stdout = t.SlavePty
command.Stderr = t.SlavePty
go func() {
if wb, ok := pipes.Stdout.(interface {
CloseWriters() error
}); ok {
defer wb.CloseWriters()
}
io.Copy(pipes.Stdout, t.MasterPty)
}()
if pipes.Stdin != nil {
command.Stdin = t.SlavePty
command.SysProcAttr.Setctty = true
go func() {
io.Copy(t.MasterPty, pipes.Stdin)
pipes.Stdin.Close()
}()
}
return nil
}
// Close closes both ends of the pty.
func (t *TtyConsole) Close() error {
t.SlavePty.Close()
return t.MasterPty.Close()
}
func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, hooks execdriver.Hooks) (int, error) {
return -1, fmt.Errorf("Unsupported: Exec is not supported by the %q driver", driverName)
}
// Clean up after an Exec
func (d *driver) Clean(id string) error {
return nil
}
func (d *driver) generateEnvConfig(c *execdriver.Command) error {
data := []byte(strings.Join(c.ProcessConfig.Env, "\n"))
p := path.Join(d.libPath, "containers", c.ID, "config.env")
c.Mounts = append(c.Mounts, execdriver.Mount{
Source: p,
Destination: "/.dockerenv",
Writable: false,
Private: true,
})
return ioutil.WriteFile(p, data, 0600)
}
func (d *driver) generateDockerInit(c *execdriver.Command) error {
p := fmt.Sprintf("%s/.containerexec", c.Rootfs)
var args []string
if pathExecutable(p) {
return nil
}
args = append(args, c.ProcessConfig.Entrypoint)
args = append(args, c.ProcessConfig.Arguments...)
data := []byte(fmt.Sprintf("#!/bin/sh\n%s\n", strings.Join(args, " ")))
return ioutil.WriteFile(p, data, 0755)
}
func (d *driver) linkExists(name string) bool {
cmd := exec.Command("ip", "link", "show", name)
err := cmd.Run()
return err == nil
}
func (d *driver) setupNetwork(c *execdriver.Command) error {
ifname := getTapIf(c)
var bridgeName string
var bridgeLinkName string
var output []byte
var err error
var ok bool
bridge := c.NetworkSettings.Networks["bridge"]
if bridge == nil {
return fmt.Errorf("no bridge network available")
}
d.ipaddr = bridge.IPAddress
d.macaddr = bridge.MacAddress
d.gateway = bridge.Gateway
// Extract the bridge details from the relevant endpoint.
for _, info := range c.EndpointInfo {
if _, ok = info[netlabel.BridgeEID].(string); !ok {
continue
}
if bridgeName, ok = info[netlabel.BridgeName].(string); !ok {
return fmt.Errorf("unable to determine bridge name")
}
if bridgeLinkName, ok = info[netlabel.BridgeLinkName].(string); !ok {
return fmt.Errorf("unable to determine bridge link name")
}
}
// Strip existing veth if it exists
if bridgeLinkName != "" && d.linkExists(bridgeLinkName) {
cmd := exec.Command("ip", "link", "del", bridgeLinkName)
if output, err = cmd.CombinedOutput(); err != nil {
logrus.Debugf("%s setupNetwork error: %v, %s", driverName, cmd.Args, output)
return err
}
}
cmd := exec.Command("ip", "tuntap", "add", "dev", ifname, "mode", "tap", "vnet_hdr")
if output, err = cmd.CombinedOutput(); err != nil {
logrus.Debugf("%s setupNetwork error: %v, %s", driverName, cmd.Args, output)
return err
}
cmd = exec.Command("ip", "link", "set", "dev", ifname, "master", bridgeName)
if output, err = cmd.CombinedOutput(); err != nil {
logrus.Debugf("%s setupNetwork error: %v, %s", driverName, cmd.Args, output)
return err
}
cmd = exec.Command("ip", "link", "set", "dev", ifname, "up")
if output, err = cmd.CombinedOutput(); err != nil {
logrus.Debugf("%s setupNetwork error: %v, %s", driverName, cmd.Args, output)
return err
}
return err
}
func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) {
if _, ok := d.activeContainers[id]; !ok {
return nil, fmt.Errorf("%s is not a key in active containers", id)
}
// FIXME:
return execdriver.Stats(d.containerDir(id), d.activeContainers[id].container.Cgroups.Memory, d.machineMemory)
}
func (d *driver) SupportsHooks() bool {
return false
}