github.com/jaegerpicker/docker@v0.7.7-0.20150325003727-22dba32b4dab/daemon/execdriver/lxc/driver.go (about) 1 package lxc 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "strconv" 14 "strings" 15 "sync" 16 "syscall" 17 "time" 18 19 log "github.com/Sirupsen/logrus" 20 "github.com/docker/docker/daemon/execdriver" 21 sysinfo "github.com/docker/docker/pkg/system" 22 "github.com/docker/docker/pkg/term" 23 "github.com/docker/docker/pkg/version" 24 "github.com/docker/docker/utils" 25 "github.com/docker/libcontainer" 26 "github.com/docker/libcontainer/cgroups" 27 "github.com/docker/libcontainer/configs" 28 "github.com/docker/libcontainer/system" 29 "github.com/docker/libcontainer/user" 30 "github.com/kr/pty" 31 ) 32 33 const DriverName = "lxc" 34 35 var ErrExec = errors.New("Unsupported: Exec is not supported by the lxc driver") 36 37 type driver struct { 38 root string // root path for the driver to use 39 initPath string 40 apparmor bool 41 sharedRoot bool 42 activeContainers map[string]*activeContainer 43 machineMemory int64 44 sync.Mutex 45 } 46 47 type activeContainer struct { 48 container *configs.Config 49 cmd *exec.Cmd 50 } 51 52 func NewDriver(root, initPath string, apparmor bool) (*driver, error) { 53 if err := os.MkdirAll(root, 0700); err != nil { 54 return nil, err 55 } 56 // setup unconfined symlink 57 if err := linkLxcStart(root); err != nil { 58 return nil, err 59 } 60 meminfo, err := sysinfo.ReadMemInfo() 61 if err != nil { 62 return nil, err 63 } 64 return &driver{ 65 apparmor: apparmor, 66 root: root, 67 initPath: initPath, 68 sharedRoot: rootIsShared(), 69 activeContainers: make(map[string]*activeContainer), 70 machineMemory: meminfo.MemTotal, 71 }, nil 72 } 73 74 func (d *driver) Name() string { 75 version := d.version() 76 return fmt.Sprintf("%s-%s", DriverName, version) 77 } 78 79 func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (execdriver.ExitStatus, error) { 80 var ( 81 term execdriver.Terminal 82 err error 83 dataPath = d.containerDir(c.ID) 84 ) 85 86 if c.ProcessConfig.Tty { 87 term, err = NewTtyConsole(&c.ProcessConfig, pipes) 88 } else { 89 term, err = execdriver.NewStdConsole(&c.ProcessConfig, pipes) 90 } 91 c.ProcessConfig.Terminal = term 92 container, err := d.createContainer(c) 93 if err != nil { 94 return execdriver.ExitStatus{ExitCode: -1}, err 95 } 96 d.Lock() 97 d.activeContainers[c.ID] = &activeContainer{ 98 container: container, 99 cmd: &c.ProcessConfig.Cmd, 100 } 101 d.Unlock() 102 103 c.Mounts = append(c.Mounts, execdriver.Mount{ 104 Source: d.initPath, 105 Destination: c.InitPath, 106 Writable: false, 107 Private: true, 108 }) 109 110 if err := d.generateEnvConfig(c); err != nil { 111 return execdriver.ExitStatus{ExitCode: -1}, err 112 } 113 configPath, err := d.generateLXCConfig(c) 114 if err != nil { 115 return execdriver.ExitStatus{ExitCode: -1}, err 116 } 117 params := []string{ 118 "lxc-start", 119 "-n", c.ID, 120 "-f", configPath, 121 } 122 123 // From lxc>=1.1 the default behavior is to daemonize containers after start 124 lxcVersion := version.Version(d.version()) 125 if lxcVersion.GreaterThanOrEqualTo(version.Version("1.1")) { 126 params = append(params, "-F") 127 } 128 129 if c.Network.ContainerID != "" { 130 params = append(params, 131 "--share-net", c.Network.ContainerID, 132 ) 133 } 134 if c.Ipc != nil { 135 if c.Ipc.ContainerID != "" { 136 params = append(params, 137 "--share-ipc", c.Ipc.ContainerID, 138 ) 139 } else if c.Ipc.HostIpc { 140 params = append(params, 141 "--share-ipc", "1", 142 ) 143 } 144 } 145 146 params = append(params, 147 "--", 148 c.InitPath, 149 ) 150 if c.Network.Interface != nil { 151 params = append(params, 152 "-g", c.Network.Interface.Gateway, 153 "-i", fmt.Sprintf("%s/%d", c.Network.Interface.IPAddress, c.Network.Interface.IPPrefixLen), 154 ) 155 } 156 params = append(params, 157 "-mtu", strconv.Itoa(c.Network.Mtu), 158 ) 159 160 if c.ProcessConfig.User != "" { 161 params = append(params, "-u", c.ProcessConfig.User) 162 } 163 164 if c.ProcessConfig.Privileged { 165 if d.apparmor { 166 params[0] = path.Join(d.root, "lxc-start-unconfined") 167 168 } 169 params = append(params, "-privileged") 170 } 171 172 if c.WorkingDir != "" { 173 params = append(params, "-w", c.WorkingDir) 174 } 175 176 params = append(params, "--", c.ProcessConfig.Entrypoint) 177 params = append(params, c.ProcessConfig.Arguments...) 178 179 if d.sharedRoot { 180 // lxc-start really needs / to be non-shared, or all kinds of stuff break 181 // when lxc-start unmount things and those unmounts propagate to the main 182 // mount namespace. 183 // What we really want is to clone into a new namespace and then 184 // mount / MS_REC|MS_SLAVE, but since we can't really clone or fork 185 // without exec in go we have to do this horrible shell hack... 186 shellString := 187 "mount --make-rslave /; exec " + 188 utils.ShellQuoteArguments(params) 189 190 params = []string{ 191 "unshare", "-m", "--", "/bin/sh", "-c", shellString, 192 } 193 } 194 log.Debugf("lxc params %s", params) 195 var ( 196 name = params[0] 197 arg = params[1:] 198 ) 199 aname, err := exec.LookPath(name) 200 if err != nil { 201 aname = name 202 } 203 c.ProcessConfig.Path = aname 204 c.ProcessConfig.Args = append([]string{name}, arg...) 205 206 if err := createDeviceNodes(c.Rootfs, c.AutoCreatedDevices); err != nil { 207 return execdriver.ExitStatus{ExitCode: -1}, err 208 } 209 210 if err := c.ProcessConfig.Start(); err != nil { 211 return execdriver.ExitStatus{ExitCode: -1}, err 212 } 213 214 var ( 215 waitErr error 216 waitLock = make(chan struct{}) 217 ) 218 219 go func() { 220 if err := c.ProcessConfig.Wait(); err != nil { 221 if _, ok := err.(*exec.ExitError); !ok { // Do not propagate the error if it's simply a status code != 0 222 waitErr = err 223 } 224 } 225 close(waitLock) 226 }() 227 228 terminate := func(terr error) (execdriver.ExitStatus, error) { 229 if c.ProcessConfig.Process != nil { 230 c.ProcessConfig.Process.Kill() 231 c.ProcessConfig.Wait() 232 } 233 return execdriver.ExitStatus{ExitCode: -1}, terr 234 } 235 // Poll lxc for RUNNING status 236 pid, err := d.waitForStart(c, waitLock) 237 if err != nil { 238 return terminate(err) 239 } 240 241 cgroupPaths, err := cgroupPaths(c.ID) 242 if err != nil { 243 return terminate(err) 244 } 245 246 state := &libcontainer.State{ 247 InitProcessPid: pid, 248 CgroupPaths: cgroupPaths, 249 } 250 251 f, err := os.Create(filepath.Join(dataPath, "state.json")) 252 if err != nil { 253 return terminate(err) 254 } 255 defer f.Close() 256 257 if err := json.NewEncoder(f).Encode(state); err != nil { 258 return terminate(err) 259 } 260 261 c.ContainerPid = pid 262 263 if startCallback != nil { 264 log.Debugf("Invoking startCallback") 265 startCallback(&c.ProcessConfig, pid) 266 } 267 268 oomKill := false 269 oomKillNotification, err := notifyOnOOM(cgroupPaths) 270 271 <-waitLock 272 273 if err == nil { 274 _, oomKill = <-oomKillNotification 275 log.Debugf("oomKill error %s waitErr %s", oomKill, waitErr) 276 } else { 277 log.Warnf("Your kernel does not support OOM notifications: %s", err) 278 } 279 280 // check oom error 281 exitCode := getExitCode(c) 282 if oomKill { 283 exitCode = 137 284 } 285 return execdriver.ExitStatus{ExitCode: exitCode, OOMKilled: oomKill}, waitErr 286 } 287 288 // copy from libcontainer 289 func notifyOnOOM(paths map[string]string) (<-chan struct{}, error) { 290 dir := paths["memory"] 291 if dir == "" { 292 return nil, fmt.Errorf("There is no path for %q in state", "memory") 293 } 294 oomControl, err := os.Open(filepath.Join(dir, "memory.oom_control")) 295 if err != nil { 296 return nil, err 297 } 298 fd, _, syserr := syscall.RawSyscall(syscall.SYS_EVENTFD2, 0, syscall.FD_CLOEXEC, 0) 299 if syserr != 0 { 300 oomControl.Close() 301 return nil, syserr 302 } 303 304 eventfd := os.NewFile(fd, "eventfd") 305 306 eventControlPath := filepath.Join(dir, "cgroup.event_control") 307 data := fmt.Sprintf("%d %d", eventfd.Fd(), oomControl.Fd()) 308 if err := ioutil.WriteFile(eventControlPath, []byte(data), 0700); err != nil { 309 eventfd.Close() 310 oomControl.Close() 311 return nil, err 312 } 313 ch := make(chan struct{}) 314 go func() { 315 defer func() { 316 close(ch) 317 eventfd.Close() 318 oomControl.Close() 319 }() 320 buf := make([]byte, 8) 321 for { 322 if _, err := eventfd.Read(buf); err != nil { 323 return 324 } 325 // When a cgroup is destroyed, an event is sent to eventfd. 326 // So if the control path is gone, return instead of notifying. 327 if _, err := os.Lstat(eventControlPath); os.IsNotExist(err) { 328 return 329 } 330 ch <- struct{}{} 331 } 332 }() 333 return ch, nil 334 } 335 336 // createContainer populates and configures the container type with the 337 // data provided by the execdriver.Command 338 func (d *driver) createContainer(c *execdriver.Command) (*configs.Config, error) { 339 container := execdriver.InitContainer(c) 340 if err := execdriver.SetupCgroups(container, c); err != nil { 341 return nil, err 342 } 343 return container, nil 344 } 345 346 // Return an map of susbystem -> container cgroup 347 func cgroupPaths(containerId string) (map[string]string, error) { 348 subsystems, err := cgroups.GetAllSubsystems() 349 if err != nil { 350 return nil, err 351 } 352 log.Debugf("subsystems: %s", subsystems) 353 paths := make(map[string]string) 354 for _, subsystem := range subsystems { 355 cgroupRoot, cgroupDir, err := findCgroupRootAndDir(subsystem) 356 log.Debugf("cgroup path %s %s", cgroupRoot, cgroupDir) 357 if err != nil { 358 //unsupported subystem 359 continue 360 } 361 path := filepath.Join(cgroupRoot, cgroupDir, "lxc", containerId) 362 paths[subsystem] = path 363 } 364 365 return paths, nil 366 } 367 368 // this is copy from old libcontainer nodes.go 369 func createDeviceNodes(rootfs string, nodesToCreate []*configs.Device) error { 370 oldMask := syscall.Umask(0000) 371 defer syscall.Umask(oldMask) 372 373 for _, node := range nodesToCreate { 374 if err := createDeviceNode(rootfs, node); err != nil { 375 return err 376 } 377 } 378 return nil 379 } 380 381 // Creates the device node in the rootfs of the container. 382 func createDeviceNode(rootfs string, node *configs.Device) error { 383 var ( 384 dest = filepath.Join(rootfs, node.Path) 385 parent = filepath.Dir(dest) 386 ) 387 388 if err := os.MkdirAll(parent, 0755); err != nil { 389 return err 390 } 391 392 fileMode := node.FileMode 393 switch node.Type { 394 case 'c': 395 fileMode |= syscall.S_IFCHR 396 case 'b': 397 fileMode |= syscall.S_IFBLK 398 default: 399 return fmt.Errorf("%c is not a valid device type for device %s", node.Type, node.Path) 400 } 401 402 if err := syscall.Mknod(dest, uint32(fileMode), node.Mkdev()); err != nil && !os.IsExist(err) { 403 return fmt.Errorf("mknod %s %s", node.Path, err) 404 } 405 406 if err := syscall.Chown(dest, int(node.Uid), int(node.Gid)); err != nil { 407 return fmt.Errorf("chown %s to %d:%d", node.Path, node.Uid, node.Gid) 408 } 409 410 return nil 411 } 412 413 // setupUser changes the groups, gid, and uid for the user inside the container 414 // copy from libcontainer, cause not it's private 415 func setupUser(userSpec string) error { 416 // Set up defaults. 417 defaultExecUser := user.ExecUser{ 418 Uid: syscall.Getuid(), 419 Gid: syscall.Getgid(), 420 Home: "/", 421 } 422 passwdPath, err := user.GetPasswdPath() 423 if err != nil { 424 return err 425 } 426 groupPath, err := user.GetGroupPath() 427 if err != nil { 428 return err 429 } 430 execUser, err := user.GetExecUserPath(userSpec, &defaultExecUser, passwdPath, groupPath) 431 if err != nil { 432 return err 433 } 434 if err := syscall.Setgroups(execUser.Sgids); err != nil { 435 return err 436 } 437 if err := system.Setgid(execUser.Gid); err != nil { 438 return err 439 } 440 if err := system.Setuid(execUser.Uid); err != nil { 441 return err 442 } 443 // if we didn't get HOME already, set it based on the user's HOME 444 if envHome := os.Getenv("HOME"); envHome == "" { 445 if err := os.Setenv("HOME", execUser.Home); err != nil { 446 return err 447 } 448 } 449 return nil 450 } 451 452 /// Return the exit code of the process 453 // if the process has not exited -1 will be returned 454 func getExitCode(c *execdriver.Command) int { 455 if c.ProcessConfig.ProcessState == nil { 456 return -1 457 } 458 return c.ProcessConfig.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() 459 } 460 461 func (d *driver) Kill(c *execdriver.Command, sig int) error { 462 return KillLxc(c.ID, sig) 463 } 464 465 func (d *driver) Pause(c *execdriver.Command) error { 466 _, err := exec.LookPath("lxc-freeze") 467 if err == nil { 468 output, errExec := exec.Command("lxc-freeze", "-n", c.ID).CombinedOutput() 469 if errExec != nil { 470 return fmt.Errorf("Err: %s Output: %s", errExec, output) 471 } 472 } 473 474 return err 475 } 476 477 func (d *driver) Unpause(c *execdriver.Command) error { 478 _, err := exec.LookPath("lxc-unfreeze") 479 if err == nil { 480 output, errExec := exec.Command("lxc-unfreeze", "-n", c.ID).CombinedOutput() 481 if errExec != nil { 482 return fmt.Errorf("Err: %s Output: %s", errExec, output) 483 } 484 } 485 486 return err 487 } 488 489 func (d *driver) Terminate(c *execdriver.Command) error { 490 return KillLxc(c.ID, 9) 491 } 492 493 func (d *driver) version() string { 494 var ( 495 version string 496 output []byte 497 err error 498 ) 499 if _, errPath := exec.LookPath("lxc-version"); errPath == nil { 500 output, err = exec.Command("lxc-version").CombinedOutput() 501 } else { 502 output, err = exec.Command("lxc-start", "--version").CombinedOutput() 503 } 504 if err == nil { 505 version = strings.TrimSpace(string(output)) 506 if parts := strings.SplitN(version, ":", 2); len(parts) == 2 { 507 version = strings.TrimSpace(parts[1]) 508 } 509 } 510 return version 511 } 512 513 func KillLxc(id string, sig int) error { 514 var ( 515 err error 516 output []byte 517 ) 518 _, err = exec.LookPath("lxc-kill") 519 if err == nil { 520 output, err = exec.Command("lxc-kill", "-n", id, strconv.Itoa(sig)).CombinedOutput() 521 } else { 522 output, err = exec.Command("lxc-stop", "-k", "-n", id, strconv.Itoa(sig)).CombinedOutput() 523 } 524 if err != nil { 525 return fmt.Errorf("Err: %s Output: %s", err, output) 526 } 527 return nil 528 } 529 530 // wait for the process to start and return the pid for the process 531 func (d *driver) waitForStart(c *execdriver.Command, waitLock chan struct{}) (int, error) { 532 var ( 533 err error 534 output []byte 535 ) 536 // We wait for the container to be fully running. 537 // Timeout after 5 seconds. In case of broken pipe, just retry. 538 // Note: The container can run and finish correctly before 539 // the end of this loop 540 for now := time.Now(); time.Since(now) < 5*time.Second; { 541 select { 542 case <-waitLock: 543 // If the process dies while waiting for it, just return 544 return -1, nil 545 default: 546 } 547 548 output, err = d.getInfo(c.ID) 549 if err == nil { 550 info, err := parseLxcInfo(string(output)) 551 if err != nil { 552 return -1, err 553 } 554 if info.Running { 555 return info.Pid, nil 556 } 557 } 558 time.Sleep(50 * time.Millisecond) 559 } 560 return -1, execdriver.ErrNotRunning 561 } 562 563 func (d *driver) getInfo(id string) ([]byte, error) { 564 return exec.Command("lxc-info", "-n", id).CombinedOutput() 565 } 566 567 type info struct { 568 ID string 569 driver *driver 570 } 571 572 func (i *info) IsRunning() bool { 573 var running bool 574 575 output, err := i.driver.getInfo(i.ID) 576 if err != nil { 577 log.Errorf("Error getting info for lxc container %s: %s (%s)", i.ID, err, output) 578 return false 579 } 580 if strings.Contains(string(output), "RUNNING") { 581 running = true 582 } 583 return running 584 } 585 586 func (d *driver) Info(id string) execdriver.Info { 587 return &info{ 588 ID: id, 589 driver: d, 590 } 591 } 592 593 func findCgroupRootAndDir(subsystem string) (string, string, error) { 594 cgroupRoot, err := cgroups.FindCgroupMountpoint(subsystem) 595 if err != nil { 596 return "", "", err 597 } 598 599 cgroupDir, err := cgroups.GetThisCgroupDir(subsystem) 600 if err != nil { 601 return "", "", err 602 } 603 return cgroupRoot, cgroupDir, nil 604 } 605 606 func (d *driver) GetPidsForContainer(id string) ([]int, error) { 607 pids := []int{} 608 609 // cpu is chosen because it is the only non optional subsystem in cgroups 610 subsystem := "cpu" 611 cgroupRoot, cgroupDir, err := findCgroupRootAndDir(subsystem) 612 if err != nil { 613 return pids, err 614 } 615 616 filename := filepath.Join(cgroupRoot, cgroupDir, id, "tasks") 617 if _, err := os.Stat(filename); os.IsNotExist(err) { 618 // With more recent lxc versions use, cgroup will be in lxc/ 619 filename = filepath.Join(cgroupRoot, cgroupDir, "lxc", id, "tasks") 620 } 621 622 output, err := ioutil.ReadFile(filename) 623 if err != nil { 624 return pids, err 625 } 626 for _, p := range strings.Split(string(output), "\n") { 627 if len(p) == 0 { 628 continue 629 } 630 pid, err := strconv.Atoi(p) 631 if err != nil { 632 return pids, fmt.Errorf("Invalid pid '%s': %s", p, err) 633 } 634 pids = append(pids, pid) 635 } 636 return pids, nil 637 } 638 639 func linkLxcStart(root string) error { 640 sourcePath, err := exec.LookPath("lxc-start") 641 if err != nil { 642 return err 643 } 644 targetPath := path.Join(root, "lxc-start-unconfined") 645 646 if _, err := os.Lstat(targetPath); err != nil && !os.IsNotExist(err) { 647 return err 648 } else if err == nil { 649 if err := os.Remove(targetPath); err != nil { 650 return err 651 } 652 } 653 return os.Symlink(sourcePath, targetPath) 654 } 655 656 // TODO: This can be moved to the mountinfo reader in the mount pkg 657 func rootIsShared() bool { 658 if data, err := ioutil.ReadFile("/proc/self/mountinfo"); err == nil { 659 for _, line := range strings.Split(string(data), "\n") { 660 cols := strings.Split(line, " ") 661 if len(cols) >= 6 && cols[4] == "/" { 662 return strings.HasPrefix(cols[6], "shared") 663 } 664 } 665 } 666 667 // No idea, probably safe to assume so 668 return true 669 } 670 671 func (d *driver) containerDir(containerId string) string { 672 return path.Join(d.root, "containers", containerId) 673 } 674 675 func (d *driver) generateLXCConfig(c *execdriver.Command) (string, error) { 676 root := path.Join(d.containerDir(c.ID), "config.lxc") 677 678 fo, err := os.Create(root) 679 if err != nil { 680 return "", err 681 } 682 defer fo.Close() 683 684 if err := LxcTemplateCompiled.Execute(fo, struct { 685 *execdriver.Command 686 AppArmor bool 687 }{ 688 Command: c, 689 AppArmor: d.apparmor, 690 }); err != nil { 691 return "", err 692 } 693 694 return root, nil 695 } 696 697 func (d *driver) generateEnvConfig(c *execdriver.Command) error { 698 data, err := json.Marshal(c.ProcessConfig.Env) 699 if err != nil { 700 return err 701 } 702 p := path.Join(d.root, "containers", c.ID, "config.env") 703 c.Mounts = append(c.Mounts, execdriver.Mount{ 704 Source: p, 705 Destination: "/.dockerenv", 706 Writable: false, 707 Private: true, 708 }) 709 710 return ioutil.WriteFile(p, data, 0600) 711 } 712 713 // Clean not implemented for lxc 714 func (d *driver) Clean(id string) error { 715 return nil 716 } 717 718 type TtyConsole struct { 719 MasterPty *os.File 720 SlavePty *os.File 721 } 722 723 func NewTtyConsole(processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes) (*TtyConsole, error) { 724 // lxc is special in that we cannot create the master outside of the container without 725 // opening the slave because we have nothing to provide to the cmd. We have to open both then do 726 // the crazy setup on command right now instead of passing the console path to lxc and telling it 727 // to open up that console. we save a couple of openfiles in the native driver because we can do 728 // this. 729 ptyMaster, ptySlave, err := pty.Open() 730 if err != nil { 731 return nil, err 732 } 733 734 tty := &TtyConsole{ 735 MasterPty: ptyMaster, 736 SlavePty: ptySlave, 737 } 738 739 if err := tty.AttachPipes(&processConfig.Cmd, pipes); err != nil { 740 tty.Close() 741 return nil, err 742 } 743 744 processConfig.Console = tty.SlavePty.Name() 745 746 return tty, nil 747 } 748 749 func (t *TtyConsole) Master() *os.File { 750 return t.MasterPty 751 } 752 753 func (t *TtyConsole) Resize(h, w int) error { 754 return term.SetWinsize(t.MasterPty.Fd(), &term.Winsize{Height: uint16(h), Width: uint16(w)}) 755 } 756 757 func (t *TtyConsole) AttachPipes(command *exec.Cmd, pipes *execdriver.Pipes) error { 758 command.Stdout = t.SlavePty 759 command.Stderr = t.SlavePty 760 761 go func() { 762 if wb, ok := pipes.Stdout.(interface { 763 CloseWriters() error 764 }); ok { 765 defer wb.CloseWriters() 766 } 767 768 io.Copy(pipes.Stdout, t.MasterPty) 769 }() 770 771 if pipes.Stdin != nil { 772 command.Stdin = t.SlavePty 773 command.SysProcAttr.Setctty = true 774 775 go func() { 776 io.Copy(t.MasterPty, pipes.Stdin) 777 778 pipes.Stdin.Close() 779 }() 780 } 781 return nil 782 } 783 784 func (t *TtyConsole) Close() error { 785 t.SlavePty.Close() 786 return t.MasterPty.Close() 787 } 788 789 func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { 790 return -1, ErrExec 791 } 792 793 func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { 794 return execdriver.Stats(d.containerDir(id), d.activeContainers[id].container.Cgroups.Memory, d.machineMemory) 795 }