From 0028578b432d207e1e5b313c76e587eae275bdac Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Wed, 6 Feb 2019 19:22:46 +0000 Subject: Added support to migrate containers This commit adds an option to the checkpoint command to export a checkpoint into a tar.gz file as well as importing a checkpoint tar.gz file during restore. With all checkpoint artifacts in one file it is possible to easily transfer a checkpoint and thus enabling container migration in Podman. With the following steps it is possible to migrate a running container from one system (source) to another (destination). Source system: * podman container checkpoint -l -e /tmp/checkpoint.tar.gz * scp /tmp/checkpoint.tar.gz destination:/tmp Destination system: * podman pull 'container-image-as-on-source-system' * podman container restore -i /tmp/checkpoint.tar.gz The exported tar.gz file contains the checkpoint image as created by CRIU and a few additional JSON files describing the state of the checkpointed container. Now the container is running on the destination system with the same state just as during checkpointing. If the container is kept running on the source system with the checkpoint flag '-R', the result will be that the same container is running on two different hosts. Signed-off-by: Adrian Reber --- pkg/adapter/checkpoint_restore.go | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 pkg/adapter/checkpoint_restore.go (limited to 'pkg/adapter/checkpoint_restore.go') diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/adapter/checkpoint_restore.go new file mode 100644 index 000000000..9df1704ea --- /dev/null +++ b/pkg/adapter/checkpoint_restore.go @@ -0,0 +1,99 @@ +// +build !remoteclient + +package adapter + +import ( + "context" + "github.com/containers/libpod/libpod" + "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/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) ([]*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") + } + + // 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 + } + + containers = append(containers, container) + return containers, nil +} -- cgit v1.2.3-54-g00ecf From 0e072f9a9785c67f38859ab989267397b57154c8 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Mon, 15 Apr 2019 18:45:52 +0000 Subject: Also download container images during restore If restoring a container from a checkpoint it was necessary that the image the container is based was already available (podman pull). This commit adds the image download to podman container restore if it does not exist. Signed-off-by: Adrian Reber --- pkg/adapter/checkpoint_restore.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'pkg/adapter/checkpoint_restore.go') diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/adapter/checkpoint_restore.go index 9df1704ea..4ca17dd93 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/adapter/checkpoint_restore.go @@ -5,10 +5,12 @@ 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" @@ -83,6 +85,20 @@ func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri return nil, errors.Errorf("Cannot import checkpoints of containers with named volumes or dependencies") } + // 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 { -- cgit v1.2.3-54-g00ecf From bef83c42eaacd83fb5020395f1bbdeb9a6f0f220 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 14 Mar 2019 07:57:16 +0000 Subject: migration: add possibility to restore a container with a new name The option to restore a container from an external checkpoint archive (podman container restore -i /tmp/checkpoint.tar.gz) restores a container with the same name and same ID as id had before checkpointing. This commit adds the option '--name,-n' to 'podman container restore'. With this option the restored container gets the name specified after '--name,-n' and a new ID. This way it is possible to restore one container multiple times. If a container is restored with a new name Podman will not try to request the same IP address for the container as it had during checkpointing. This implicitly assumes that if a container is restored from a checkpoint archive with a different name, that it will be restored multiple times and restoring a container multiple times with the same IP address will fail as each IP address can only be used once. Signed-off-by: Adrian Reber --- cmd/podman/cliconfig/config.go | 1 + cmd/podman/restore.go | 10 ++++++++++ completions/bash/podman | 2 ++ docs/podman-container-restore.1.md | 12 ++++++++++++ libpod/container_api.go | 4 ++++ libpod/container_internal_linux.go | 8 +++++++- libpod/runtime_ctr.go | 15 ++++++++++++++- pkg/adapter/checkpoint_restore.go | 32 +++++++++++++++++++++++++++++++- pkg/adapter/containers.go | 2 +- test/e2e/checkpoint_test.go | 11 +++++++++++ 10 files changed, 93 insertions(+), 4 deletions(-) (limited to 'pkg/adapter/checkpoint_restore.go') diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index d742830ee..466164cc1 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -428,6 +428,7 @@ type RestoreValues struct { Latest bool TcpEstablished bool Import string + Name string } type RmValues struct { diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go index 828ae682f..9c77d4a5e 100644 --- a/cmd/podman/restore.go +++ b/cmd/podman/restore.go @@ -45,6 +45,7 @@ func init() { flags.BoolVarP(&restoreCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of") 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) } @@ -64,6 +65,15 @@ func restoreCmd(c *cliconfig.RestoreValues, cmd *cobra.Command) error { Keep: c.Keep, TCPEstablished: c.TcpEstablished, TargetFile: c.Import, + Name: c.Name, + } + + 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) { diff --git a/completions/bash/podman b/completions/bash/podman index 4972188aa..efb8a6a9b 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -857,6 +857,8 @@ _podman_container_restore() { local options_with_args=" -i --import + -n + --name " local boolean_options=" -a diff --git a/docs/podman-container-restore.1.md b/docs/podman-container-restore.1.md index 578d34ff3..5efc280fe 100644 --- a/docs/podman-container-restore.1.md +++ b/docs/podman-container-restore.1.md @@ -48,6 +48,18 @@ 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 podman container restore mywebserver diff --git a/libpod/container_api.go b/libpod/container_api.go index 72c4d7f37..c27cb85ea 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -820,6 +820,10 @@ type ContainerCheckpointOptions struct { // 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 diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index d471d4191..4acc77afa 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -681,7 +681,13 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti // 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 { + // 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 // container with the same IP address as during checkpointing. defer networkStatusFile.Close() diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 9d0bbf7e8..cf1f5701d 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -76,6 +76,14 @@ func (r *Runtime) initContainerVariables(rSpec *spec.Spec, config *ContainerConf 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) @@ -189,11 +197,16 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container, restore bo } if restore { - // Remove information about /dev/shm mount + // 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 diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/adapter/checkpoint_restore.go index 4ca17dd93..97ba5ecf7 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/adapter/checkpoint_restore.go @@ -41,7 +41,7 @@ func crImportFromJSON(filePath string, v interface{}) error { // 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) ([]*libpod.Container, error) { +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) @@ -85,6 +85,18 @@ func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri 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. @@ -110,6 +122,24 @@ func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri 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 } diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index b7f4c272b..29297fbd5 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -539,7 +539,7 @@ func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues, }) if c.Import != "" { - containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import) + containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) } else if c.All { containers, err = r.GetContainers(filterFuncs...) } else { diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index bd9cdcb8d..d452a062b 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -376,9 +376,20 @@ var _ = Describe("Podman checkpoint", func() { 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") }) }) -- cgit v1.2.3-54-g00ecf