diff options
-rw-r--r-- | cmd/podman/checkpoint.go | 73 | ||||
-rw-r--r-- | cmd/podman/container.go | 2 | ||||
-rw-r--r-- | cmd/podman/restore.go | 73 | ||||
-rw-r--r-- | libpod/container_api.go | 30 | ||||
-rw-r--r-- | libpod/container_internal.go | 7 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 158 | ||||
-rw-r--r-- | libpod/container_internal_unsupported.go | 8 | ||||
-rw-r--r-- | libpod/oci.go | 18 | ||||
-rw-r--r-- | libpod/oci_linux.go | 6 | ||||
-rw-r--r-- | libpod/oci_unsupported.go | 2 |
10 files changed, 371 insertions, 6 deletions
diff --git a/cmd/podman/checkpoint.go b/cmd/podman/checkpoint.go new file mode 100644 index 000000000..cbbbcd740 --- /dev/null +++ b/cmd/podman/checkpoint.go @@ -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 +} diff --git a/cmd/podman/container.go b/cmd/podman/container.go index 82c1c824d..ff634278f 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -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, diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go new file mode 100644 index 000000000..43ef87ca2 --- /dev/null +++ b/cmd/podman/restore.go @@ -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 +} diff --git a/libpod/container_api.go b/libpod/container_api.go index 192ccd347..93becb80d 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -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) +} diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 033426817..c925f070b 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -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 } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b77beaf64..0353124dd 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -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() +} diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go index 45b54efab..eed0449a9 100644 --- a/libpod/container_internal_unsupported.go +++ b/libpod/container_internal_unsupported.go @@ -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 +} diff --git a/libpod/oci.go b/libpod/oci.go index e5db06540..cf2b76ab0 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -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()) +} diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go index 210ba57d1..0447670b3 100644 --- a/libpod/oci_linux.go +++ b/libpod/oci_linux.go @@ -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() diff --git a/libpod/oci_unsupported.go b/libpod/oci_unsupported.go index 8cb4994d3..b133eb402 100644 --- a/libpod/oci_unsupported.go +++ b/libpod/oci_unsupported.go @@ -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 } |