mirror of
https://github.com/containers/podman.git
synced 2025-06-28 22:53:21 +08:00
Merge pull request #2272 from adrianreber/migration
Add support to migrate containers
This commit is contained in:
@ -46,6 +46,7 @@ func init() {
|
|||||||
flags.BoolVar(&checkpointCommand.TcpEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections")
|
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.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.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)
|
markFlagHiddenForRemoteClient("latest", flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +65,7 @@ func checkpointCmd(c *cliconfig.CheckpointValues) error {
|
|||||||
Keep: c.Keep,
|
Keep: c.Keep,
|
||||||
KeepRunning: c.LeaveRunning,
|
KeepRunning: c.LeaveRunning,
|
||||||
TCPEstablished: c.TcpEstablished,
|
TCPEstablished: c.TcpEstablished,
|
||||||
|
TargetFile: c.Export,
|
||||||
}
|
}
|
||||||
return runtime.Checkpoint(c, options)
|
return runtime.Checkpoint(c, options)
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,7 @@ type CheckpointValues struct {
|
|||||||
TcpEstablished bool
|
TcpEstablished bool
|
||||||
All bool
|
All bool
|
||||||
Latest bool
|
Latest bool
|
||||||
|
Export string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommitValues struct {
|
type CommitValues struct {
|
||||||
@ -428,6 +429,8 @@ type RestoreValues struct {
|
|||||||
Keep bool
|
Keep bool
|
||||||
Latest bool
|
Latest bool
|
||||||
TcpEstablished bool
|
TcpEstablished bool
|
||||||
|
Import string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RmValues struct {
|
type RmValues struct {
|
||||||
|
@ -24,10 +24,10 @@ var (
|
|||||||
restoreCommand.InputArgs = args
|
restoreCommand.InputArgs = args
|
||||||
restoreCommand.GlobalFlags = MainGlobalOpts
|
restoreCommand.GlobalFlags = MainGlobalOpts
|
||||||
restoreCommand.Remote = remoteclient
|
restoreCommand.Remote = remoteclient
|
||||||
return restoreCmd(&restoreCommand)
|
return restoreCmd(&restoreCommand, cmd)
|
||||||
},
|
},
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
return checkAllAndLatest(cmd, args, false)
|
return checkAllAndLatest(cmd, args, true)
|
||||||
},
|
},
|
||||||
Example: `podman container restore ctrID
|
Example: `podman container restore ctrID
|
||||||
podman container restore --latest
|
podman container restore --latest
|
||||||
@ -43,13 +43,14 @@ func init() {
|
|||||||
flags.BoolVarP(&restoreCommand.All, "all", "a", false, "Restore all checkpointed containers")
|
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.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")
|
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, "Restore a container with established TCP connections")
|
||||||
flags.BoolVar(&restoreCommand.TcpEstablished, "tcp-established", false, "Checkpoint 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)
|
markFlagHiddenForRemoteClient("latest", flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreCmd(c *cliconfig.RestoreValues) error {
|
func restoreCmd(c *cliconfig.RestoreValues, cmd *cobra.Command) error {
|
||||||
if rootless.IsRootless() {
|
if rootless.IsRootless() {
|
||||||
return errors.New("restoring a container requires root")
|
return errors.New("restoring a container requires root")
|
||||||
}
|
}
|
||||||
@ -63,6 +64,20 @@ func restoreCmd(c *cliconfig.RestoreValues) error {
|
|||||||
options := libpod.ContainerCheckpointOptions{
|
options := libpod.ContainerCheckpointOptions{
|
||||||
Keep: c.Keep,
|
Keep: c.Keep,
|
||||||
TCPEstablished: c.TcpEstablished,
|
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)
|
||||||
}
|
}
|
||||||
|
@ -742,6 +742,10 @@ _podman_container_attach() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_podman_container_checkpoint() {
|
_podman_container_checkpoint() {
|
||||||
|
local options_with_args="
|
||||||
|
-e
|
||||||
|
--export
|
||||||
|
"
|
||||||
local boolean_options="
|
local boolean_options="
|
||||||
-a
|
-a
|
||||||
--all
|
--all
|
||||||
@ -755,9 +759,15 @@ _podman_container_checkpoint() {
|
|||||||
--leave-running
|
--leave-running
|
||||||
--tcp-established
|
--tcp-established
|
||||||
"
|
"
|
||||||
|
case "$prev" in
|
||||||
|
-e|--export)
|
||||||
|
_filedir
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=($(compgen -W "$boolean_options" -- "$cur"))
|
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__podman_complete_containers_running
|
__podman_complete_containers_running
|
||||||
@ -844,6 +854,12 @@ _podman_container_restart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_podman_container_restore() {
|
_podman_container_restore() {
|
||||||
|
local options_with_args="
|
||||||
|
-i
|
||||||
|
--import
|
||||||
|
-n
|
||||||
|
--name
|
||||||
|
"
|
||||||
local boolean_options="
|
local boolean_options="
|
||||||
-a
|
-a
|
||||||
--all
|
--all
|
||||||
@ -855,9 +871,15 @@ _podman_container_restore() {
|
|||||||
--latest
|
--latest
|
||||||
--tcp-established
|
--tcp-established
|
||||||
"
|
"
|
||||||
|
case "$prev" in
|
||||||
|
-i|--import)
|
||||||
|
_filedir
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=($(compgen -W "$boolean_options" -- "$cur"))
|
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__podman_complete_containers_created
|
__podman_complete_containers_created
|
||||||
|
@ -38,6 +38,12 @@ image contains established TCP connections, this options is required during
|
|||||||
restore. Defaults to not checkpointing containers with established TCP
|
restore. Defaults to not checkpointing containers with established TCP
|
||||||
connections.
|
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
|
## EXAMPLE
|
||||||
|
|
||||||
podman container checkpoint mywebserver
|
podman container checkpoint mywebserver
|
||||||
|
@ -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
|
option is ignored. Defaults to not restoring containers with established TCP
|
||||||
connections.
|
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.
|
||||||
|
|
||||||
|
**--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
|
## EXAMPLE
|
||||||
|
|
||||||
podman container restore mywebserver
|
podman container restore mywebserver
|
||||||
|
@ -96,6 +96,28 @@ After being restored, the container will answer requests again as it did before
|
|||||||
curl http://<IP_address>:8080
|
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
|
### Stopping the container
|
||||||
To stop the httpd container:
|
To stop the httpd container:
|
||||||
```console
|
```console
|
||||||
|
@ -815,11 +815,27 @@ type ContainerCheckpointOptions struct {
|
|||||||
// TCPEstablished tells the API to checkpoint a container
|
// TCPEstablished tells the API to checkpoint a container
|
||||||
// even if it contains established TCP connections
|
// even if it contains established TCP connections
|
||||||
TCPEstablished bool
|
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
|
// Checkpoint checkpoints a container
|
||||||
func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointOptions) error {
|
func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointOptions) error {
|
||||||
logrus.Debugf("Trying to checkpoint container %s", c.ID())
|
logrus.Debugf("Trying to checkpoint container %s", c.ID())
|
||||||
|
|
||||||
|
if options.TargetFile != "" {
|
||||||
|
if err := c.prepareCheckpointExport(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !c.batched {
|
if !c.batched {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/containers/storage/pkg/archive"
|
"github.com/containers/storage/pkg/archive"
|
||||||
"github.com/containers/storage/pkg/mount"
|
"github.com/containers/storage/pkg/mount"
|
||||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
"github.com/opencontainers/runtime-tools/generate"
|
||||||
"github.com/opencontainers/selinux/go-selinux/label"
|
"github.com/opencontainers/selinux/go-selinux/label"
|
||||||
opentracing "github.com/opentracing/opentracing-go"
|
opentracing "github.com/opentracing/opentracing-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -1345,7 +1346,7 @@ func (c *Container) appendStringToRundir(destFile, output string) (string, error
|
|||||||
return filepath.Join(c.state.RunDir, destFile), nil
|
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 {
|
func (c *Container) saveSpec(spec *spec.Spec) error {
|
||||||
// If the OCI spec already exists, we need to replace it
|
// If the OCI spec already exists, we need to replace it
|
||||||
// Cannot guarantee some things, e.g. network namespaces, have the same
|
// Cannot guarantee some things, e.g. network namespaces, have the same
|
||||||
@ -1501,3 +1502,40 @@ func (c *Container) checkReadyForRemoval() error {
|
|||||||
|
|
||||||
return nil
|
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")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ package libpod
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@ -25,6 +26,7 @@ import (
|
|||||||
"github.com/containers/libpod/pkg/lookup"
|
"github.com/containers/libpod/pkg/lookup"
|
||||||
"github.com/containers/libpod/pkg/resolvconf"
|
"github.com/containers/libpod/pkg/resolvconf"
|
||||||
"github.com/containers/libpod/pkg/rootless"
|
"github.com/containers/libpod/pkg/rootless"
|
||||||
|
"github.com/containers/storage/pkg/archive"
|
||||||
securejoin "github.com/cyphar/filepath-securejoin"
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
"github.com/opencontainers/runc/libcontainer/user"
|
"github.com/opencontainers/runc/libcontainer/user"
|
||||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
@ -496,6 +498,45 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) exportCheckpoint(dest string) (err error) {
|
||||||
|
if (len(c.config.NamedVolumes) > 0) || (len(c.Dependencies()) > 0) {
|
||||||
|
return errors.Errorf("Cannot export checkpoints of containers with named volumes or dependencies")
|
||||||
|
}
|
||||||
|
logrus.Debugf("Exporting checkpoint image of container %q to %q", c.ID(), dest)
|
||||||
|
input, err := archive.TarWithOptions(c.bundlePath(), &archive.TarOptions{
|
||||||
|
Compression: archive.Gzip,
|
||||||
|
IncludeSourceDir: true,
|
||||||
|
IncludeFiles: []string{
|
||||||
|
"checkpoint",
|
||||||
|
"artifacts",
|
||||||
|
"ctr.log",
|
||||||
|
"config.dump",
|
||||||
|
"spec.dump",
|
||||||
|
"network.status"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error reading checkpoint directory %q", c.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error creating checkpoint export file %q", dest)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
if err := os.Chmod(dest, 0600); err != nil {
|
||||||
|
return errors.Wrapf(err, "cannot chmod %q", dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(outFile, input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) checkpointRestoreSupported() (err error) {
|
func (c *Container) checkpointRestoreSupported() (err error) {
|
||||||
if !criu.CheckForCriu() {
|
if !criu.CheckForCriu() {
|
||||||
return errors.Errorf("Checkpoint/Restore requires at least CRIU %d", criu.MinCriuVersion)
|
return errors.Errorf("Checkpoint/Restore requires at least CRIU %d", criu.MinCriuVersion)
|
||||||
@ -549,6 +590,12 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.TargetFile != "" {
|
||||||
|
if err = c.exportCheckpoint(options.TargetFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logrus.Debugf("Checkpointed container %s", c.ID())
|
logrus.Debugf("Checkpointed container %s", c.ID())
|
||||||
|
|
||||||
if !options.KeepRunning {
|
if !options.KeepRunning {
|
||||||
@ -561,15 +608,50 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !options.Keep {
|
if !options.Keep {
|
||||||
// Remove log file
|
cleanup := []string{
|
||||||
os.Remove(filepath.Join(c.bundlePath(), "dump.log"))
|
"dump.log",
|
||||||
// Remove statistic file
|
"stats-dump",
|
||||||
os.Remove(filepath.Join(c.bundlePath(), "stats-dump"))
|
"config.dump",
|
||||||
|
"spec.dump",
|
||||||
|
}
|
||||||
|
for _, delete := range cleanup {
|
||||||
|
file := filepath.Join(c.bundlePath(), delete)
|
||||||
|
os.Remove(file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.save()
|
return c.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) importCheckpoint(input string) (err error) {
|
||||||
|
archiveFile, err := os.Open(input)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to open checkpoint archive %s for import", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer archiveFile.Close()
|
||||||
|
options := &archive.TarOptions{
|
||||||
|
ExcludePatterns: []string{
|
||||||
|
// config.dump and spec.dump are only required
|
||||||
|
// container creation
|
||||||
|
"config.dump",
|
||||||
|
"spec.dump",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = archive.Untar(archiveFile, c.bundlePath(), options)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the newly created config.json exists on disk
|
||||||
|
g := generate.NewFromSpec(c.config.Spec)
|
||||||
|
if err = c.saveSpec(g.Spec()); err != nil {
|
||||||
|
return errors.Wrap(err, "Saving imported container specification for restore failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) (err error) {
|
func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) (err error) {
|
||||||
|
|
||||||
if err := c.checkpointRestoreSupported(); err != nil {
|
if err := c.checkpointRestoreSupported(); err != nil {
|
||||||
@ -580,6 +662,12 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
|
|||||||
return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID())
|
return errors.Wrapf(ErrCtrStateInvalid, "container %s is running or paused, cannot restore", c.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.TargetFile != "" {
|
||||||
|
if err = c.importCheckpoint(options.TargetFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Let's try to stat() CRIU's inventory file. If it does not exist, it makes
|
// 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.
|
// 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) {
|
if _, err := os.Stat(filepath.Join(c.CheckpointPath(), "inventory.img")); os.IsNotExist(err) {
|
||||||
@ -593,7 +681,13 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
|
|||||||
// Read network configuration from checkpoint
|
// Read network configuration from checkpoint
|
||||||
// Currently only one interface with one IP is supported.
|
// Currently only one interface with one IP is supported.
|
||||||
networkStatusFile, err := os.Open(filepath.Join(c.bundlePath(), "network.status"))
|
networkStatusFile, err := os.Open(filepath.Join(c.bundlePath(), "network.status"))
|
||||||
if err == nil {
|
// If the restored container should get a new name, the IP address of
|
||||||
|
// the container will not be restored. This assumes that if a new name is
|
||||||
|
// specified, the container is restored multiple times.
|
||||||
|
// TODO: This implicit restoring with or without IP depending on an
|
||||||
|
// unrelated restore parameter (--name) does not seem like the
|
||||||
|
// best solution.
|
||||||
|
if err == nil && options.Name == "" {
|
||||||
// The file with the network.status does exist. Let's restore the
|
// The file with the network.status does exist. Let's restore the
|
||||||
// container with the same IP address as during checkpointing.
|
// container with the same IP address as during checkpointing.
|
||||||
defer networkStatusFile.Close()
|
defer networkStatusFile.Close()
|
||||||
@ -637,23 +731,44 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restoring from an import means that we are doing migration
|
||||||
|
if options.TargetFile != "" {
|
||||||
|
g.SetRootPath(c.state.Mountpoint)
|
||||||
|
}
|
||||||
|
|
||||||
// We want to have the same network namespace as before.
|
// We want to have the same network namespace as before.
|
||||||
if c.config.CreateNetNS {
|
if c.config.CreateNetNS {
|
||||||
g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path())
|
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 {
|
if err := c.makeBindMounts(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.TargetFile != "" {
|
||||||
|
for dstPath, srcPath := range c.state.BindMounts {
|
||||||
|
newMount := spec.Mount{
|
||||||
|
Type: "bind",
|
||||||
|
Source: srcPath,
|
||||||
|
Destination: dstPath,
|
||||||
|
Options: []string{"bind", "private"},
|
||||||
|
}
|
||||||
|
if c.IsReadOnly() && dstPath != "/dev/shm" {
|
||||||
|
newMount.Options = append(newMount.Options, "ro", "nosuid", "noexec", "nodev")
|
||||||
|
}
|
||||||
|
if !MountExists(g.Mounts(), dstPath) {
|
||||||
|
g.AddMount(newMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup for a working restore.
|
// Cleanup for a working restore.
|
||||||
c.removeConmonFiles()
|
c.removeConmonFiles()
|
||||||
|
|
||||||
|
// Save the OCI spec to disk
|
||||||
|
if err := c.saveSpec(g.Spec()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil {
|
if err := c.runtime.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/containers/storage"
|
"github.com/containers/storage"
|
||||||
"github.com/containers/storage/pkg/stringid"
|
"github.com/containers/storage/pkg/stringid"
|
||||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
"github.com/opencontainers/runtime-tools/generate"
|
||||||
opentracing "github.com/opentracing/opentracing-go"
|
opentracing "github.com/opentracing/opentracing-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -34,7 +35,7 @@ type CtrCreateOption func(*Container) error
|
|||||||
// A true return will include the container, a false return will exclude it.
|
// A true return will include the container, a false return will exclude it.
|
||||||
type ContainerFilter func(*Container) bool
|
type ContainerFilter func(*Container) bool
|
||||||
|
|
||||||
// NewContainer creates a new container from a given OCI config
|
// NewContainer creates a new container from a given OCI config.
|
||||||
func (r *Runtime) NewContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) {
|
func (r *Runtime) NewContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) {
|
||||||
r.lock.Lock()
|
r.lock.Lock()
|
||||||
defer r.lock.Unlock()
|
defer r.lock.Unlock()
|
||||||
@ -44,20 +45,46 @@ func (r *Runtime) NewContainer(ctx context.Context, rSpec *spec.Spec, options ..
|
|||||||
return r.newContainer(ctx, rSpec, options...)
|
return r.newContainer(ctx, rSpec, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) {
|
// RestoreContainer re-creates a container from an imported checkpoint
|
||||||
span, _ := opentracing.StartSpanFromContext(ctx, "newContainer")
|
func (r *Runtime) RestoreContainer(ctx context.Context, rSpec *spec.Spec, config *ContainerConfig) (c *Container, err error) {
|
||||||
span.SetTag("type", "runtime")
|
r.lock.Lock()
|
||||||
defer span.Finish()
|
defer r.lock.Unlock()
|
||||||
|
if !r.valid {
|
||||||
|
return nil, ErrRuntimeStopped
|
||||||
|
}
|
||||||
|
|
||||||
|
ctr, err := r.initContainerVariables(rSpec, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error initializing container variables")
|
||||||
|
}
|
||||||
|
return r.setupContainer(ctx, ctr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runtime) initContainerVariables(rSpec *spec.Spec, config *ContainerConfig) (c *Container, err error) {
|
||||||
if rSpec == nil {
|
if rSpec == nil {
|
||||||
return nil, errors.Wrapf(ErrInvalidArg, "must provide a valid runtime spec to create container")
|
return nil, errors.Wrapf(ErrInvalidArg, "must provide a valid runtime spec to create container")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctr := new(Container)
|
ctr := new(Container)
|
||||||
ctr.config = new(ContainerConfig)
|
ctr.config = new(ContainerConfig)
|
||||||
ctr.state = new(ContainerState)
|
ctr.state = new(ContainerState)
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
ctr.config.ID = stringid.GenerateNonCryptoID()
|
ctr.config.ID = stringid.GenerateNonCryptoID()
|
||||||
|
ctr.config.ShmSize = DefaultShmSize
|
||||||
|
} else {
|
||||||
|
// This is a restore from an imported checkpoint
|
||||||
|
if err := JSONDeepCopy(config, ctr.config); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error copying container config for restore")
|
||||||
|
}
|
||||||
|
// If the ID is empty a new name for the restored container was requested
|
||||||
|
if ctr.config.ID == "" {
|
||||||
|
ctr.config.ID = stringid.GenerateNonCryptoID()
|
||||||
|
// Fixup ExitCommand with new ID
|
||||||
|
ctr.config.ExitCommand[len(ctr.config.ExitCommand)-1] = ctr.config.ID
|
||||||
|
}
|
||||||
|
// Reset the log path to point to the default
|
||||||
|
ctr.config.LogPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
ctr.config.Spec = new(spec.Spec)
|
ctr.config.Spec = new(spec.Spec)
|
||||||
if err := JSONDeepCopy(rSpec, ctr.config.Spec); err != nil {
|
if err := JSONDeepCopy(rSpec, ctr.config.Spec); err != nil {
|
||||||
@ -65,8 +92,6 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
|
|||||||
}
|
}
|
||||||
ctr.config.CreatedTime = time.Now()
|
ctr.config.CreatedTime = time.Now()
|
||||||
|
|
||||||
ctr.config.ShmSize = DefaultShmSize
|
|
||||||
|
|
||||||
ctr.state.BindMounts = make(map[string]string)
|
ctr.state.BindMounts = make(map[string]string)
|
||||||
|
|
||||||
ctr.config.StopTimeout = CtrRemoveTimeout
|
ctr.config.StopTimeout = CtrRemoveTimeout
|
||||||
@ -80,12 +105,29 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctr.runtime = r
|
ctr.runtime = r
|
||||||
|
|
||||||
|
return ctr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) {
|
||||||
|
span, _ := opentracing.StartSpanFromContext(ctx, "newContainer")
|
||||||
|
span.SetTag("type", "runtime")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
|
ctr, err := r.initContainerVariables(rSpec, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error initializing container variables")
|
||||||
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
if err := option(ctr); err != nil {
|
if err := option(ctr); err != nil {
|
||||||
return nil, errors.Wrapf(err, "error running container create option")
|
return nil, errors.Wrapf(err, "error running container create option")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return r.setupContainer(ctx, ctr, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runtime) setupContainer(ctx context.Context, ctr *Container, restore bool) (c *Container, err error) {
|
||||||
// Allocate a lock for the container
|
// Allocate a lock for the container
|
||||||
lock, err := r.lockManager.AllocateLock()
|
lock, err := r.lockManager.AllocateLock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -154,6 +196,19 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
|
|||||||
return nil, errors.Wrapf(ErrInvalidArg, "unsupported CGroup manager: %s - cannot validate cgroup parent", r.config.CgroupManager)
|
return nil, errors.Wrapf(ErrInvalidArg, "unsupported CGroup manager: %s - cannot validate cgroup parent", r.config.CgroupManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if restore {
|
||||||
|
// Remove information about bind mount
|
||||||
|
// for new container from imported checkpoint
|
||||||
|
g := generate.Generator{Config: ctr.config.Spec}
|
||||||
|
g.RemoveMount("/dev/shm")
|
||||||
|
ctr.config.ShmDir = ""
|
||||||
|
g.RemoveMount("/etc/resolv.conf")
|
||||||
|
g.RemoveMount("/etc/hostname")
|
||||||
|
g.RemoveMount("/etc/hosts")
|
||||||
|
g.RemoveMount("/run/.containerenv")
|
||||||
|
g.RemoveMount("/run/secrets")
|
||||||
|
}
|
||||||
|
|
||||||
// Set up storage for the container
|
// Set up storage for the container
|
||||||
if err := ctr.setupStorage(ctx); err != nil {
|
if err := ctr.setupStorage(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
145
pkg/adapter/checkpoint_restore.go
Normal file
145
pkg/adapter/checkpoint_restore.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// +build !remoteclient
|
||||||
|
|
||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/containers/libpod/libpod"
|
||||||
|
"github.com/containers/libpod/libpod/image"
|
||||||
|
"github.com/containers/storage/pkg/archive"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prefixing the checkpoint/restore related functions with 'cr'
|
||||||
|
|
||||||
|
// crImportFromJSON imports the JSON files stored in the exported
|
||||||
|
// checkpoint tarball
|
||||||
|
func crImportFromJSON(filePath string, v interface{}) error {
|
||||||
|
jsonFile, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to open container definition %s for restore", filePath)
|
||||||
|
}
|
||||||
|
defer jsonFile.Close()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(jsonFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to read container definition %s for restore", filePath)
|
||||||
|
}
|
||||||
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
if err = json.Unmarshal([]byte(content), v); err != nil {
|
||||||
|
return errors.Wrapf(err, "Failed to unmarshal container definition %s for restore", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crImportCheckpoint it the function which imports the information
|
||||||
|
// from checkpoint tarball and re-creates the container from that information
|
||||||
|
func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) {
|
||||||
|
// First get the container definition from the
|
||||||
|
// tarball to a temporary directory
|
||||||
|
archiveFile, err := os.Open(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "Failed to open checkpoint archive %s for import", input)
|
||||||
|
}
|
||||||
|
defer archiveFile.Close()
|
||||||
|
options := &archive.TarOptions{
|
||||||
|
// Here we only need the files config.dump and spec.dump
|
||||||
|
ExcludePatterns: []string{
|
||||||
|
"checkpoint",
|
||||||
|
"artifacts",
|
||||||
|
"ctr.log",
|
||||||
|
"network.status",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dir, err := ioutil.TempDir("", "checkpoint")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
err = archive.Untar(archiveFile, dir, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load spec.dump from temporary directory
|
||||||
|
spec := new(spec.Spec)
|
||||||
|
if err := crImportFromJSON(filepath.Join(dir, "spec.dump"), spec); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config.dump from temporary directory
|
||||||
|
config := new(libpod.ContainerConfig)
|
||||||
|
if err = crImportFromJSON(filepath.Join(dir, "config.dump"), config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should not happen as checkpoints with these options are not exported.
|
||||||
|
if (len(config.Dependencies) > 0) || (len(config.NamedVolumes) > 0) {
|
||||||
|
return nil, errors.Errorf("Cannot import checkpoints of containers with named volumes or dependencies")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrID := config.ID
|
||||||
|
newName := false
|
||||||
|
|
||||||
|
// Check if the restored container gets a new name
|
||||||
|
if name != "" {
|
||||||
|
config.ID = ""
|
||||||
|
config.Name = name
|
||||||
|
newName = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrName := config.Name
|
||||||
|
|
||||||
|
// The code to load the images is copied from create.go
|
||||||
|
var writer io.Writer
|
||||||
|
// In create.go this only set if '--quiet' does not exist.
|
||||||
|
writer = os.Stderr
|
||||||
|
rtc, err := runtime.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = runtime.ImageRuntime().New(ctx, config.RootfsImageName, rtc.SignaturePolicyPath, "", writer, nil, image.SigningOptions{}, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now create a new container from the just loaded information
|
||||||
|
container, err := runtime.RestoreContainer(ctx, spec, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers []*libpod.Container
|
||||||
|
if container == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
containerConfig := container.Config()
|
||||||
|
if containerConfig.Name != ctrName {
|
||||||
|
return nil, errors.Errorf("Name of restored container (%s) does not match requested name (%s)", containerConfig.Name, ctrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newName == false {
|
||||||
|
// Only check ID for a restore with the same name.
|
||||||
|
// Using -n to request a new name for the restored container, will also create a new ID
|
||||||
|
if containerConfig.ID != ctrID {
|
||||||
|
return nil, errors.Errorf("ID of restored container (%s) does not match requested ID (%s)", containerConfig.ID, ctrID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the ExitCommand points to the correct container ID
|
||||||
|
if containerConfig.ExitCommand[len(containerConfig.ExitCommand)-1] != containerConfig.ID {
|
||||||
|
return nil, errors.Errorf("'ExitCommandID' uses ID %s instead of container ID %s", containerConfig.ExitCommand[len(containerConfig.ExitCommand)-1], containerConfig.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
containers = append(containers, container)
|
||||||
|
return containers, nil
|
||||||
|
}
|
@ -526,7 +526,7 @@ func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore one or more containers
|
// Restore one or more containers
|
||||||
func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error {
|
func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error {
|
||||||
var (
|
var (
|
||||||
containers []*libpod.Container
|
containers []*libpod.Container
|
||||||
err, lastError error
|
err, lastError error
|
||||||
@ -538,7 +538,9 @@ func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.Contai
|
|||||||
return state == libpod.ContainerStateExited
|
return state == libpod.ContainerStateExited
|
||||||
})
|
})
|
||||||
|
|
||||||
if c.All {
|
if c.Import != "" {
|
||||||
|
containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import, c.Name)
|
||||||
|
} else if c.All {
|
||||||
containers, err = r.GetContainers(filterFuncs...)
|
containers, err = r.GetContainers(filterFuncs...)
|
||||||
} else {
|
} else {
|
||||||
containers, err = shortcuts.GetContainersByContext(false, c.Latest, c.InputArgs, r.Runtime)
|
containers, err = shortcuts.GetContainersByContext(false, c.Latest, c.InputArgs, r.Runtime)
|
||||||
|
@ -664,6 +664,10 @@ func (r *LocalRuntime) Attach(ctx context.Context, c *cliconfig.AttachValues) er
|
|||||||
|
|
||||||
// Checkpoint one or more containers
|
// Checkpoint one or more containers
|
||||||
func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod.ContainerCheckpointOptions) error {
|
func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod.ContainerCheckpointOptions) error {
|
||||||
|
if c.Export != "" {
|
||||||
|
return errors.New("the remote client does not support exporting checkpoints")
|
||||||
|
}
|
||||||
|
|
||||||
var lastError error
|
var lastError error
|
||||||
ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs)
|
ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -699,7 +703,11 @@ func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues, options libpod.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore one or more containers
|
// Restore one or more containers
|
||||||
func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error {
|
func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues, options libpod.ContainerCheckpointOptions) error {
|
||||||
|
if c.Import != "" {
|
||||||
|
return errors.New("the remote client does not support importing checkpoints")
|
||||||
|
}
|
||||||
|
|
||||||
var lastError error
|
var lastError error
|
||||||
ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs)
|
ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -347,4 +347,49 @@ var _ = Describe("Podman checkpoint", func() {
|
|||||||
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// This test does the same steps which are necessary for migrating
|
||||||
|
// a container from one host to another
|
||||||
|
It("podman checkpoint container with export (migration)", func() {
|
||||||
|
// CRIU does not work with seccomp correctly on RHEL7
|
||||||
|
session := podmanTest.Podman([]string{"run", "-it", "--security-opt", "seccomp=unconfined", "-d", ALPINE, "top"})
|
||||||
|
session.WaitWithDefaultTimeout()
|
||||||
|
Expect(session.ExitCode()).To(Equal(0))
|
||||||
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1))
|
||||||
|
|
||||||
|
result := podmanTest.Podman([]string{"container", "checkpoint", "-l", "-e", "/tmp/checkpoint.tar.gz"})
|
||||||
|
result.WaitWithDefaultTimeout()
|
||||||
|
|
||||||
|
Expect(result.ExitCode()).To(Equal(0))
|
||||||
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
|
||||||
|
Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited"))
|
||||||
|
|
||||||
|
// Remove all containers to simulate migration
|
||||||
|
result = podmanTest.Podman([]string{"rm", "-fa"})
|
||||||
|
result.WaitWithDefaultTimeout()
|
||||||
|
Expect(result.ExitCode()).To(Equal(0))
|
||||||
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
|
||||||
|
|
||||||
|
result = podmanTest.Podman([]string{"container", "restore", "-i", "/tmp/checkpoint.tar.gz"})
|
||||||
|
result.WaitWithDefaultTimeout()
|
||||||
|
|
||||||
|
Expect(result.ExitCode()).To(Equal(0))
|
||||||
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1))
|
||||||
|
Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up"))
|
||||||
|
|
||||||
|
// Restore container a second time with different name
|
||||||
|
result = podmanTest.Podman([]string{"container", "restore", "-i", "/tmp/checkpoint.tar.gz", "-n", "restore_again"})
|
||||||
|
result.WaitWithDefaultTimeout()
|
||||||
|
|
||||||
|
Expect(result.ExitCode()).To(Equal(0))
|
||||||
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(2))
|
||||||
|
Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up"))
|
||||||
|
|
||||||
|
result = podmanTest.Podman([]string{"rm", "-fa"})
|
||||||
|
result.WaitWithDefaultTimeout()
|
||||||
|
Expect(result.ExitCode()).To(Equal(0))
|
||||||
|
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
|
||||||
|
|
||||||
|
// Remove exported checkpoint
|
||||||
|
os.Remove("/tmp/checkpoint.tar.gz")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user