mirror of
https://github.com/containers/podman.git
synced 2025-08-06 19:44:14 +08:00
Add support to checkpoint/restore containers
runc uses CRIU to support checkpoint and restore of containers. This brings an initial checkpoint/restore implementation to podman. None of the additional runc flags are yet supported and container migration optimization (pre-copy/post-copy) is also left for the future. The current status is that it is possible to checkpoint and restore a container. I am testing on RHEL-7.x and as the combination of RHEL-7 and CRIU has seccomp troubles I have to create the container without seccomp. With the following steps I am able to checkpoint and restore a container: # podman run --security-opt="seccomp=unconfined" -d registry.fedoraproject.org/f27/httpd # curl -I 10.22.0.78:8080 HTTP/1.1 403 Forbidden # <-- this is actually a good answer # podman container checkpoint <container> # curl -I 10.22.0.78:8080 curl: (7) Failed connect to 10.22.0.78:8080; No route to host # podman container restore <container> # curl -I 10.22.0.78:8080 HTTP/1.1 403 Forbidden I am using CRIU, runc and conmon from git. All required changes for checkpoint/restore support in podman have been merged in the corresponding projects. To have the same IP address in the restored container as before checkpointing, CNI is told which IP address to use. If the saved network configuration cannot be found during restore, the container is restored with a new IP address. For CRIU to restore established TCP connections the IP address of the network namespace used for restore needs to be the same. For TCP connections in the listening state the IP address can change. During restore only one network interface with one IP address is handled correctly. Support to restore containers with more advanced network configuration will be implemented later. v2: * comment typo * print debug messages during cleanup of restore files * use createContainer() instead of createOCIContainer() * introduce helper CheckpointPath() * do not try to restore a container that is paused * use existing helper functions for cleanup * restructure code flow for better readability * do not try to restore if checkpoint/inventory.img is missing * git add checkpoint.go restore.go v3: * move checkpoint/restore under 'podman container' v4: * incorporated changes from latest reviews Signed-off-by: Adrian Reber <areber@redhat.com>
This commit is contained in:

committed by
Adrian Reber

parent
3750b35ae2
commit
f7c8fd8a3d
73
cmd/podman/checkpoint.go
Normal file
73
cmd/podman/checkpoint.go
Normal file
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containers/libpod/cmd/podman/libpodruntime"
|
||||
"github.com/containers/libpod/pkg/rootless"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
checkpointDescription = `
|
||||
podman container checkpoint
|
||||
|
||||
Checkpoints one or more running containers. The container name or ID can be used.
|
||||
`
|
||||
checkpointFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "keep, k",
|
||||
Usage: "keep all temporary checkpoint files",
|
||||
},
|
||||
}
|
||||
checkpointCommand = cli.Command{
|
||||
Name: "checkpoint",
|
||||
Usage: "Checkpoints one or more containers",
|
||||
Description: checkpointDescription,
|
||||
Flags: checkpointFlags,
|
||||
Action: checkpointCmd,
|
||||
ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
|
||||
}
|
||||
)
|
||||
|
||||
func checkpointCmd(c *cli.Context) error {
|
||||
if rootless.IsRootless() {
|
||||
return errors.New("checkpointing a container requires root")
|
||||
}
|
||||
|
||||
runtime, err := libpodruntime.GetRuntime(c)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get runtime")
|
||||
}
|
||||
defer runtime.Shutdown(false)
|
||||
|
||||
keep := c.Bool("keep")
|
||||
args := c.Args()
|
||||
if len(args) < 1 {
|
||||
return errors.Errorf("you must provide at least one container name or id")
|
||||
}
|
||||
|
||||
var lastError error
|
||||
for _, arg := range args {
|
||||
ctr, err := runtime.LookupContainer(arg)
|
||||
if err != nil {
|
||||
if lastError != nil {
|
||||
fmt.Fprintln(os.Stderr, lastError)
|
||||
}
|
||||
lastError = errors.Wrapf(err, "error looking up container %q", arg)
|
||||
continue
|
||||
}
|
||||
if err = ctr.Checkpoint(context.TODO(), keep); err != nil {
|
||||
if lastError != nil {
|
||||
fmt.Fprintln(os.Stderr, lastError)
|
||||
}
|
||||
lastError = errors.Wrapf(err, "failed to checkpoint container %v", ctr.ID())
|
||||
} else {
|
||||
fmt.Println(ctr.ID())
|
||||
}
|
||||
}
|
||||
return lastError
|
||||
}
|
@ -7,6 +7,7 @@ import (
|
||||
var (
|
||||
subCommands = []cli.Command{
|
||||
attachCommand,
|
||||
checkpointCommand,
|
||||
cleanupCommand,
|
||||
commitCommand,
|
||||
createCommand,
|
||||
@ -23,6 +24,7 @@ var (
|
||||
// pruneCommand,
|
||||
refreshCommand,
|
||||
restartCommand,
|
||||
restoreCommand,
|
||||
rmCommand,
|
||||
runCommand,
|
||||
runlabelCommand,
|
||||
|
73
cmd/podman/restore.go
Normal file
73
cmd/podman/restore.go
Normal file
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containers/libpod/cmd/podman/libpodruntime"
|
||||
"github.com/containers/libpod/pkg/rootless"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
restoreDescription = `
|
||||
podman container restore
|
||||
|
||||
Restores a container from a checkpoint. The container name or ID can be used.
|
||||
`
|
||||
restoreFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "keep, k",
|
||||
Usage: "keep all temporary checkpoint files",
|
||||
},
|
||||
}
|
||||
restoreCommand = cli.Command{
|
||||
Name: "restore",
|
||||
Usage: "Restores one or more containers from a checkpoint",
|
||||
Description: restoreDescription,
|
||||
Flags: restoreFlags,
|
||||
Action: restoreCmd,
|
||||
ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
|
||||
}
|
||||
)
|
||||
|
||||
func restoreCmd(c *cli.Context) error {
|
||||
if rootless.IsRootless() {
|
||||
return errors.New("restoring a container requires root")
|
||||
}
|
||||
|
||||
runtime, err := libpodruntime.GetRuntime(c)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get runtime")
|
||||
}
|
||||
defer runtime.Shutdown(false)
|
||||
|
||||
keep := c.Bool("keep")
|
||||
args := c.Args()
|
||||
if len(args) < 1 {
|
||||
return errors.Errorf("you must provide at least one container name or id")
|
||||
}
|
||||
|
||||
var lastError error
|
||||
for _, arg := range args {
|
||||
ctr, err := runtime.LookupContainer(arg)
|
||||
if err != nil {
|
||||
if lastError != nil {
|
||||
fmt.Fprintln(os.Stderr, lastError)
|
||||
}
|
||||
lastError = errors.Wrapf(err, "error looking up container %q", arg)
|
||||
continue
|
||||
}
|
||||
if err = ctr.Restore(context.TODO(), keep); err != nil {
|
||||
if lastError != nil {
|
||||
fmt.Fprintln(os.Stderr, lastError)
|
||||
}
|
||||
lastError = errors.Wrapf(err, "failed to restore container %v", ctr.ID())
|
||||
} else {
|
||||
fmt.Println(ctr.ID())
|
||||
}
|
||||
}
|
||||
return lastError
|
||||
}
|
@ -832,3 +832,33 @@ func (c *Container) Refresh(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checkpoint checkpoints a container
|
||||
func (c *Container) Checkpoint(ctx context.Context, keep bool) error {
|
||||
logrus.Debugf("Trying to checkpoint container %s", c)
|
||||
if !c.batched {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if err := c.syncContainer(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.checkpoint(ctx, keep)
|
||||
}
|
||||
|
||||
// Restore restores a container
|
||||
func (c *Container) Restore(ctx context.Context, keep bool) (err error) {
|
||||
logrus.Debugf("Trying to restore container %s", c)
|
||||
if !c.batched {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if err := c.syncContainer(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.restore(ctx, keep)
|
||||
}
|
||||
|
@ -129,6 +129,11 @@ func (c *Container) ControlSocketPath() string {
|
||||
return filepath.Join(c.bundlePath(), "ctl")
|
||||
}
|
||||
|
||||
// CheckpointPath returns the path to the directory containing the checkpoint
|
||||
func (c *Container) CheckpointPath() string {
|
||||
return filepath.Join(c.bundlePath(), "checkpoint")
|
||||
}
|
||||
|
||||
// AttachSocketPath retrieves the path of the container's attach socket
|
||||
func (c *Container) AttachSocketPath() string {
|
||||
return filepath.Join(c.runtime.ociRuntime.socketsDir, c.ID(), "attach")
|
||||
@ -523,7 +528,7 @@ func (c *Container) init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// With the spec complete, do an OCI create
|
||||
if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent); err != nil {
|
||||
if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,18 @@ package libpod
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
cnitypes "github.com/containernetworking/cni/pkg/types/current"
|
||||
crioAnnotations "github.com/containers/libpod/pkg/annotations"
|
||||
"github.com/containers/libpod/pkg/chrootuser"
|
||||
"github.com/containers/libpod/pkg/rootless"
|
||||
@ -307,3 +313,155 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) checkpoint(ctx context.Context, keep bool) (err error) {
|
||||
|
||||
if c.state.State != ContainerStateRunning {
|
||||
return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, cannot checkpoint", c.state.State)
|
||||
}
|
||||
if err := c.runtime.ociRuntime.checkpointContainer(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save network.status. This is needed to restore the container with
|
||||
// the same IP. Currently limited to one IP address in a container
|
||||
// with one interface.
|
||||
formatJSON, err := json.MarshalIndent(c.state.NetworkStatus, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(c.bundlePath(), "network.status"), formatJSON, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Checkpointed container %s", c.ID())
|
||||
|
||||
c.state.State = ContainerStateStopped
|
||||
|
||||
// Cleanup Storage and Network
|
||||
if err := c.cleanup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !keep {
|
||||
// Remove log file
|
||||
os.Remove(filepath.Join(c.bundlePath(), "dump.log"))
|
||||
// Remove statistic file
|
||||
os.Remove(filepath.Join(c.bundlePath(), "stats-dump"))
|
||||
}
|
||||
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *Container) restore(ctx context.Context, keep bool) (err error) {
|
||||
|
||||
if (c.state.State != ContainerStateConfigured) && (c.state.State != ContainerStateExited) {
|
||||
return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID())
|
||||
}
|
||||
|
||||
// Let's try to stat() CRIU's inventory file. If it does not exist, it makes
|
||||
// no sense to try a restore. This is a minimal check if a checkpoint exist.
|
||||
if _, err := os.Stat(filepath.Join(c.CheckpointPath(), "inventory.img")); os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "A complete checkpoint for this container cannot be found, cannot restore")
|
||||
}
|
||||
|
||||
// Read network configuration from checkpoint
|
||||
// Currently only one interface with one IP is supported.
|
||||
networkStatusFile, err := os.Open(filepath.Join(c.bundlePath(), "network.status"))
|
||||
if err == nil {
|
||||
// The file with the network.status does exist. Let's restore the
|
||||
// container with the same IP address as during checkpointing.
|
||||
defer networkStatusFile.Close()
|
||||
var networkStatus []*cnitypes.Result
|
||||
networkJSON, err := ioutil.ReadAll(networkStatusFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
json.Unmarshal(networkJSON, &networkStatus)
|
||||
// Take the first IP address
|
||||
var IP net.IP
|
||||
if len(networkStatus) > 0 {
|
||||
if len(networkStatus[0].IPs) > 0 {
|
||||
IP = networkStatus[0].IPs[0].Address.IP
|
||||
}
|
||||
}
|
||||
if IP != nil {
|
||||
env := fmt.Sprintf("IP=%s", IP)
|
||||
// Tell CNI which IP address we want.
|
||||
os.Setenv("CNI_ARGS", env)
|
||||
logrus.Debugf("Restoring container with %s", env)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.prepare(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if err2 := c.cleanup(ctx); err2 != nil {
|
||||
logrus.Errorf("error cleaning up container %s: %v", c.ID(), err2)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO: use existing way to request static IPs, once it is merged in ocicni
|
||||
// https://github.com/cri-o/ocicni/pull/23/
|
||||
|
||||
// CNI_ARGS was used to request a certain IP address. Unconditionally remove it.
|
||||
os.Unsetenv("CNI_ARGS")
|
||||
|
||||
// Read config
|
||||
jsonPath := filepath.Join(c.bundlePath(), "config.json")
|
||||
logrus.Debugf("generate.NewFromFile at %v", jsonPath)
|
||||
g, err := generate.NewFromFile(jsonPath)
|
||||
if err != nil {
|
||||
logrus.Debugf("generate.NewFromFile failed with %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// We want to have the same network namespace as before.
|
||||
if c.config.CreateNetNS {
|
||||
g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path())
|
||||
}
|
||||
|
||||
// Save the OCI spec to disk
|
||||
if err := c.saveSpec(g.Spec()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.makeBindMounts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cleanup for a working restore.
|
||||
c.removeConmonFiles()
|
||||
|
||||
if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Restored container %s", c.ID())
|
||||
|
||||
c.state.State = ContainerStateRunning
|
||||
|
||||
if !keep {
|
||||
// Delete all checkpoint related files. At this point, in theory, all files
|
||||
// should exist. Still ignoring errors for now as the container should be
|
||||
// restored and running. Not erroring out just because some cleanup operation
|
||||
// failed. Starting with the checkpoint directory
|
||||
err = os.RemoveAll(c.CheckpointPath())
|
||||
if err != nil {
|
||||
logrus.Debugf("Non-fatal: removal of checkpoint directory (%s) failed: %v", c.CheckpointPath(), err)
|
||||
}
|
||||
cleanup := [...]string{"restore.log", "dump.log", "stats-dump", "stats-restore", "network.status"}
|
||||
for _, delete := range cleanup {
|
||||
file := filepath.Join(c.bundlePath(), delete)
|
||||
err = os.Remove(file)
|
||||
if err != nil {
|
||||
logrus.Debugf("Non-fatal: removal of checkpoint file (%s) failed: %v", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.save()
|
||||
}
|
||||
|
@ -27,3 +27,11 @@ func (c *Container) cleanupNetwork() error {
|
||||
func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Container) checkpoint(ctx context.Context, keep bool) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Container) restore(ctx context.Context, keep bool) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string) (err error) {
|
||||
func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) {
|
||||
var stderrBuf bytes.Buffer
|
||||
|
||||
runtimeDir, err := GetRootlessRuntimeDir()
|
||||
@ -289,6 +289,10 @@ func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string) (er
|
||||
args = append(args, "--syslog")
|
||||
}
|
||||
|
||||
if restoreContainer {
|
||||
args = append(args, "--restore", ctr.CheckpointPath())
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"args": args,
|
||||
}).Debugf("running conmon: %s", r.conmonPath)
|
||||
@ -766,3 +770,15 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkpointContainer checkpoints the given container
|
||||
func (r *OCIRuntime) checkpointContainer(ctr *Container) error {
|
||||
// imagePath is used by CRIU to store the actual checkpoint files
|
||||
imagePath := ctr.CheckpointPath()
|
||||
// workPath will be used to store dump.log and stats-dump
|
||||
workPath := ctr.bundlePath()
|
||||
logrus.Debugf("Writing checkpoint to %s", imagePath)
|
||||
logrus.Debugf("Writing checkpoint logs to %s", workPath)
|
||||
return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "checkpoint",
|
||||
"--image-path", imagePath, "--work-path", workPath, ctr.ID())
|
||||
}
|
||||
|
@ -63,10 +63,10 @@ func newPipe() (parent *os.File, child *os.File, err error) {
|
||||
// CreateContainer creates a container in the OCI runtime
|
||||
// TODO terminal support for container
|
||||
// Presently just ignoring conmon opts related to it
|
||||
func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err error) {
|
||||
func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) {
|
||||
if ctr.state.UserNSRoot == "" {
|
||||
// no need of an intermediate mount ns
|
||||
return r.createOCIContainer(ctr, cgroupParent)
|
||||
return r.createOCIContainer(ctr, cgroupParent, restoreContainer)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
@ -103,7 +103,7 @@ func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err e
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.createOCIContainer(ctr, cgroupParent)
|
||||
err = r.createOCIContainer(ctr, cgroupParent, restoreContainer)
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
|
@ -15,7 +15,7 @@ func newPipe() (parent *os.File, child *os.File, err error) {
|
||||
return nil, nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string) (err error) {
|
||||
func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreContainer bool) (err error) {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user