Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to migrate containers #2272

Merged
merged 9 commits into from
Jun 7, 2019
2 changes: 2 additions & 0 deletions cmd/podman/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func init() {
flags.BoolVar(&checkpointCommand.TcpEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections")
flags.BoolVarP(&checkpointCommand.All, "all", "a", false, "Checkpoint all running containers")
flags.BoolVarP(&checkpointCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of")
flags.StringVarP(&checkpointCommand.Export, "export", "e", "", "Export the checkpoint image to a tar.gz")
markFlagHiddenForRemoteClient("latest", flags)
}

Expand All @@ -64,6 +65,7 @@ func checkpointCmd(c *cliconfig.CheckpointValues) error {
Keep: c.Keep,
KeepRunning: c.LeaveRunning,
TCPEstablished: c.TcpEstablished,
TargetFile: c.Export,
}
return runtime.Checkpoint(c, options)
}
3 changes: 3 additions & 0 deletions cmd/podman/cliconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type CheckpointValues struct {
TcpEstablished bool
All bool
Latest bool
Export string
}

type CommitValues struct {
Expand Down Expand Up @@ -426,6 +427,8 @@ type RestoreValues struct {
Keep bool
Latest bool
TcpEstablished bool
Import string
Name string
}

type RmValues struct {
Expand Down
27 changes: 21 additions & 6 deletions cmd/podman/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ var (
restoreCommand.InputArgs = args
restoreCommand.GlobalFlags = MainGlobalOpts
restoreCommand.Remote = remoteclient
return restoreCmd(&restoreCommand)
return restoreCmd(&restoreCommand, cmd)
},
Args: func(cmd *cobra.Command, args []string) error {
return checkAllAndLatest(cmd, args, false)
return checkAllAndLatest(cmd, args, true)
},
Example: `podman container restore ctrID
podman container restore --latest
Expand All @@ -43,13 +43,14 @@ func init() {
flags.BoolVarP(&restoreCommand.All, "all", "a", false, "Restore all checkpointed containers")
flags.BoolVarP(&restoreCommand.Keep, "keep", "k", false, "Keep all temporary checkpoint files")
flags.BoolVarP(&restoreCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of")
// TODO: add ContainerStateCheckpointed
flags.BoolVar(&restoreCommand.TcpEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections")
flags.BoolVar(&restoreCommand.TcpEstablished, "tcp-established", false, "Restore a container with established TCP connections")
flags.StringVarP(&restoreCommand.Import, "import", "i", "", "Restore from exported checkpoint archive (tar.gz)")
flags.StringVarP(&restoreCommand.Name, "name", "n", "", "Specify new name for container restored from exported checkpoint (only works with --import)")

markFlagHiddenForRemoteClient("latest", flags)
}

func restoreCmd(c *cliconfig.RestoreValues) error {
func restoreCmd(c *cliconfig.RestoreValues, cmd *cobra.Command) error {
if rootless.IsRootless() {
return errors.New("restoring a container requires root")
}
Expand All @@ -63,6 +64,20 @@ func restoreCmd(c *cliconfig.RestoreValues) error {
options := libpod.ContainerCheckpointOptions{
Keep: c.Keep,
TCPEstablished: c.TcpEstablished,
TargetFile: c.Import,
Name: c.Name,
}
return runtime.Restore(c, options)

if c.Import == "" && c.Name != "" {
return errors.Errorf("--name can only used with --import")
}

if c.Name != "" && c.TcpEstablished {
return errors.Errorf("--tcp-established cannot be used with --name")
}

if (c.Import != "") && (c.All || c.Latest) {
return errors.Errorf("Cannot use --import and --all or --latest at the same time")
}
return runtime.Restore(getContext(), c, options)
}
26 changes: 24 additions & 2 deletions completions/bash/podman
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ _podman_container_attach() {
}

_podman_container_checkpoint() {
local options_with_args="
-e
--export
"
local boolean_options="
-a
--all
Expand All @@ -755,9 +759,15 @@ _podman_container_checkpoint() {
--leave-running
--tcp-established
"
case "$prev" in
-e|--export)
_filedir
return
;;
esac
case "$cur" in
-*)
COMPREPLY=($(compgen -W "$boolean_options" -- "$cur"))
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
;;
*)
__podman_complete_containers_running
Expand Down Expand Up @@ -844,6 +854,12 @@ _podman_container_restart() {
}

_podman_container_restore() {
local options_with_args="
-i
--import
-n
--name
"
local boolean_options="
-a
--all
Expand All @@ -855,9 +871,15 @@ _podman_container_restore() {
--latest
--tcp-established
"
case "$prev" in
-i|--import)
_filedir
return
;;
esac
case "$cur" in
-*)
COMPREPLY=($(compgen -W "$boolean_options" -- "$cur"))
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
;;
*)
__podman_complete_containers_created
Expand Down
6 changes: 6 additions & 0 deletions docs/podman-container-checkpoint.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ image contains established TCP connections, this options is required during
restore. Defaults to not checkpointing containers with established TCP
connections.

**--export, -e**

Export the checkpoint to a tar.gz file. The exported checkpoint can be used
to import the container on another system and thus enabling container live
migration.

## EXAMPLE

podman container checkpoint mywebserver
Expand Down
18 changes: 18 additions & 0 deletions docs/podman-container-restore.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ If the checkpoint image does not contain established TCP connections this
option is ignored. Defaults to not restoring containers with established TCP
connections.

**--import, -i**

Import a checkpoint tar.gz file, which was exported by Podman. This can be used
to import a checkpointed container from another host. It is not necessary to specify
a container when restoring from an exported checkpoint.

adrianreber marked this conversation as resolved.
Show resolved Hide resolved
**--name, -n**

This is only available in combination with **--import, -i**. If a container is restored
from a checkpoint tar.gz file it is possible to rename it with **--name, -n**. This
way it is possible to restore a container from a checkpoint multiple times with different
names.

If the **--name, -n** option is used, Podman will not attempt to assign the same IP
address to the container it was using before checkpointing as each IP address can only
be used once and the restored container will have another IP address. This also means
that **--name, -n** cannot be used in combination with **--tcp-established**.

## EXAMPLE

podman container restore mywebserver
Expand Down
22 changes: 22 additions & 0 deletions docs/tutorials/podman_tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ After being restored, the container will answer requests again as it did before
curl http://<IP_address>:8080
```

### Migrate the container
To live migrate a container from one host to another the container is checkpointed on the source
system of the migration, transferred to the destination system and then restored on the destination
system. When transferring the checkpoint, it is possible to specify an output-file.

On the source system:
```console
sudo podman container checkpoint <container_id> -e /tmp/checkpoint.tar.gz
scp /tmp/checkpoint.tar.gz <destination_system>:/tmp
```

On the destination system:
```console
sudo podman container restore -i /tmp/checkpoint.tar.gz
```

After being restored, the container will answer requests again as it did before checkpointing. This
time the container will continue to run on the destination system.
```console
curl http://<IP_address>:8080
```

### Stopping the container
To stop the httpd container:
```console
Expand Down
16 changes: 16 additions & 0 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,11 +815,27 @@ type ContainerCheckpointOptions struct {
// TCPEstablished tells the API to checkpoint a container
// even if it contains established TCP connections
TCPEstablished bool
// Export tells the API to write the checkpoint image to
// the filename set in TargetFile
// Import tells the API to read the checkpoint image from
// the filename set in TargetFile
TargetFile string
// Name tells the API that during restore from an exported
// checkpoint archive a new name should be used for the
// restored container
Name string
}

// Checkpoint checkpoints a container
func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointOptions) error {
logrus.Debugf("Trying to checkpoint container %s", c.ID())

if options.TargetFile != "" {
if err := c.prepareCheckpointExport(); err != nil {
return err
}
}

if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
Expand Down
40 changes: 39 additions & 1 deletion libpod/container_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/mount"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"github.com/opencontainers/selinux/go-selinux/label"
opentracing "github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
Expand Down Expand Up @@ -1345,7 +1346,7 @@ func (c *Container) appendStringToRundir(destFile, output string) (string, error
return filepath.Join(c.state.RunDir, destFile), nil
}

// Save OCI spec to disk, replacing any existing specs for the container
// saveSpec saves the OCI spec to disk, replacing any existing specs for the container
func (c *Container) saveSpec(spec *spec.Spec) error {
// If the OCI spec already exists, we need to replace it
// Cannot guarantee some things, e.g. network namespaces, have the same
Expand Down Expand Up @@ -1501,3 +1502,40 @@ func (c *Container) checkReadyForRemoval() error {

return nil
}

// writeJSONFile marshalls and writes the given data to a JSON file
// in the bundle path
func (c *Container) writeJSONFile(v interface{}, file string) (err error) {
fileJSON, err := json.MarshalIndent(v, "", " ")
if err != nil {
return errors.Wrapf(err, "error writing JSON to %s for container %s", file, c.ID())
}
file = filepath.Join(c.bundlePath(), file)
if err := ioutil.WriteFile(file, fileJSON, 0644); err != nil {
return err
}

return nil
}

// prepareCheckpointExport writes the config and spec to
// JSON files for later export
func (c *Container) prepareCheckpointExport() (err error) {
// save live config
if err := c.writeJSONFile(c.Config(), "config.dump"); err != nil {
return err
}

// save spec
jsonPath := filepath.Join(c.bundlePath(), "config.json")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just a file copy?

We can probably use mrunalp/fileutils which has a CopyFile() function in it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is almost a copy, but this is used to output the whole spec which has more information than only the config.json. Using copy would leave out some information.

g, err := generate.NewFromFile(jsonPath)
if err != nil {
logrus.Debugf("generating spec for container %q failed with %v", c.ID(), err)
return err
}
if err := c.writeJSONFile(g.Spec(), "spec.dump"); err != nil {
return err
}

return nil
}
Loading