From c76caba367583c5d4f1e8fe3f111383148d4d175 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Wed, 17 Nov 2021 16:55:43 +0000 Subject: Use same runtime to restore a container as during checkpointing There are at least two runtimes that support checkpoint and restore: runc and crun. Although the checkpoints created by these are almost compatible, it is not (yet) possible to restore a checkpoint created with one runtime with the other runtime. To make checkpoint/restore usage more comfortable this adds code to look into the checkpoint archive during restore and to set the runtime to the one used during checkpointing. This also adds a check, if the user explicitly sets a runtime during restore, that the runtime is also the same as used during checkpointing. If a different runtime is selected than the one used during checkpointing the restore will fail early. If runc and crun will create compatible checkpoints in the future the check can be changed to treat crun and runc as compatible checkpoint/restore runtimes. Signed-off-by: Adrian Reber --- cmd/podman/root.go | 43 +++++++++++++++++ docs/source/markdown/podman-container-restore.1.md | 6 +++ pkg/checkpoint/checkpoint_restore.go | 26 +--------- pkg/checkpoint/crutils/checkpoint_restore_utils.go | 55 ++++++++++++++++++++++ 4 files changed, 106 insertions(+), 24 deletions(-) diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 9e4c8d24d..bccc559ce 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -15,6 +15,7 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/checkpoint/crutils" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/parallel" "github.com/containers/podman/v3/pkg/rootless" @@ -114,6 +115,48 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { cfg := registry.PodmanConfig() + // Currently it is only possible to restore a container with the same runtime + // as used for checkpointing. It should be possible to make crun and runc + // compatible to restore a container with another runtime then checkpointed. + // Currently that does not work. + // To make it easier for users we will look into the checkpoint archive and + // set the runtime to the one used during checkpointing. + if !registry.IsRemote() && cmd.Name() == "restore" { + if cmd.Flag("import").Changed { + runtime, err := crutils.CRGetRuntimeFromArchive(cmd.Flag("import").Value.String()) + if err != nil { + return errors.Wrapf( + err, + "failed extracting runtime information from %s", + cmd.Flag("import").Value.String(), + ) + } + if cfg.RuntimePath == "" { + // If the user did not select a runtime, this takes the one from + // the checkpoint archives and tells Podman to use it for the restore. + runtimeFlag := cmd.Root().Flags().Lookup("runtime") + if runtimeFlag == nil { + return errors.Errorf( + "Unexcpected error setting runtime to '%s' for restore", + *runtime, + ) + } + runtimeFlag.Value.Set(*runtime) + runtimeFlag.Changed = true + logrus.Debugf("Checkpoint was created using '%s'. Restore will use the same runtime", *runtime) + } else if cfg.RuntimePath != *runtime { + // If the user selected a runtime on the command-line this checks if + // it is the same then during checkpointing and errors out if not. + return errors.Errorf( + "checkpoint archive %s was created with runtime '%s' and cannot be restored with runtime '%s'", + cmd.Flag("import").Value.String(), + *runtime, + cfg.RuntimePath, + ) + } + } + } + // --connection is not as "special" as --remote so we can wait and process it here conn := cmd.Root().LocalFlags().Lookup("connection") if conn != nil && conn.Changed { diff --git a/docs/source/markdown/podman-container-restore.1.md b/docs/source/markdown/podman-container-restore.1.md index 10477fc77..a4630dedf 100644 --- a/docs/source/markdown/podman-container-restore.1.md +++ b/docs/source/markdown/podman-container-restore.1.md @@ -77,6 +77,12 @@ Import a checkpoint tar.gz file, which was exported by Podman. This can be used to import a checkpointed *container* from another host.\ *IMPORTANT: This OPTION does not need a container name or ID as input argument.* +During the import of a checkpoint file Podman will select the same container runtime +which was used during checkpointing. This is especially important if a specific +(non-default) container runtime was specified during container creation. Podman will +also abort the restore if the container runtime specified during restore does +not much the container runtime used for container creation. + #### **--import-previous**=*file* Import a pre-checkpoint tar.gz file which was exported by Podman. This option diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index 85fe6a77e..c371adf5b 100644 --- a/pkg/checkpoint/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -6,7 +6,6 @@ import ( "os" metadata "github.com/checkpoint-restore/checkpointctl/lib" - "github.com/checkpoint-restore/go-criu/v5/stats" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" @@ -14,10 +13,8 @@ import ( "github.com/containers/podman/v3/pkg/checkpoint/crutils" "github.com/containers/podman/v3/pkg/criu" "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/errorhandling" "github.com/containers/podman/v3/pkg/specgen/generate" "github.com/containers/podman/v3/pkg/specgenutil" - "github.com/containers/storage/pkg/archive" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -30,24 +27,6 @@ import ( func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions) ([]*libpod.Container, error) { // First get the container definition from the // tarball to a temporary directory - archiveFile, err := os.Open(restoreOptions.Import) - if err != nil { - return nil, errors.Wrap(err, "failed to open checkpoint archive for import") - } - defer errorhandling.CloseQuiet(archiveFile) - options := &archive.TarOptions{ - // Here we only need the files config.dump and spec.dump - ExcludePatterns: []string{ - "volumes", - "ctr.log", - "artifacts", - stats.StatsDump, - metadata.RootFsDiffTar, - metadata.DeletedFilesFile, - metadata.NetworkStatusFile, - metadata.CheckpointDirectory, - }, - } dir, err := ioutil.TempDir("", "checkpoint") if err != nil { return nil, err @@ -57,9 +36,8 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt logrus.Errorf("Could not recursively remove %s: %q", dir, err) } }() - err = archive.Untar(archiveFile, dir, options) - if err != nil { - return nil, errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", restoreOptions.Import) + if err := crutils.CRImportCheckpointConfigOnly(dir, restoreOptions.Import); err != nil { + return nil, err } // Load spec.dump from temporary directory diff --git a/pkg/checkpoint/crutils/checkpoint_restore_utils.go b/pkg/checkpoint/crutils/checkpoint_restore_utils.go index 3b77368bb..2765d18e8 100644 --- a/pkg/checkpoint/crutils/checkpoint_restore_utils.go +++ b/pkg/checkpoint/crutils/checkpoint_restore_utils.go @@ -3,11 +3,13 @@ package crutils import ( "bytes" "io" + "io/ioutil" "os" "os/exec" "path/filepath" metadata "github.com/checkpoint-restore/checkpointctl/lib" + "github.com/checkpoint-restore/go-criu/v5/stats" "github.com/containers/storage/pkg/archive" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" @@ -39,6 +41,36 @@ func CRImportCheckpointWithoutConfig(destination, input string) error { return nil } +// CRImportCheckpointConfigOnly only imports the checkpoint configuration +// from the checkpoint archive (input) into the directory destination. +// Only the files "config.dump" and "spec.dump" are extracted. +func CRImportCheckpointConfigOnly(destination, input string) 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{ + // Here we only need the files config.dump and spec.dump + ExcludePatterns: []string{ + "volumes", + "ctr.log", + "artifacts", + stats.StatsDump, + metadata.RootFsDiffTar, + metadata.DeletedFilesFile, + metadata.NetworkStatusFile, + metadata.CheckpointDirectory, + }, + } + if err = archive.Untar(archiveFile, destination, options); err != nil { + return errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input) + } + + return nil +} + // CRRemoveDeletedFiles loads the list of deleted files and if // it exists deletes all files listed. func CRRemoveDeletedFiles(id, baseDirectory, containerRootDirectory string) error { @@ -200,3 +232,26 @@ func CRRuntimeSupportsPodCheckpointRestore(runtimePath string) bool { out, _ := cmd.CombinedOutput() return bytes.Contains(out, []byte("flag needs an argument")) } + +// CRGetRuntimeFromArchive extracts the checkpoint metadata from the +// given checkpoint archive and returns the runtime used to create +// the given checkpoint archive. +func CRGetRuntimeFromArchive(input string) (*string, error) { + dir, err := ioutil.TempDir("", "checkpoint") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + + if err := CRImportCheckpointConfigOnly(dir, input); err != nil { + return nil, err + } + + // Load config.dump from temporary directory + ctrConfig := new(metadata.ContainerConfig) + if _, err = metadata.ReadJSONFile(ctrConfig, dir, metadata.ConfigDumpFile); err != nil { + return nil, err + } + + return &ctrConfig.OCIRuntime, nil +} -- cgit v1.2.3-54-g00ecf From ced0ffbe8f6dae9031a1ae3b48d4f24bca93b5bd Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 18 Nov 2021 17:16:26 +0000 Subject: Add tests for restore runtime verification On container restore ensures that the same container runtime is used as during checkpointing and it also ensures that the user does not select a different runtime. Signed-off-by: Adrian Reber --- test/e2e/checkpoint_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index 6b9a96e9f..e34c07d49 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -1377,4 +1377,177 @@ var _ = Describe("Podman checkpoint", func() { Expect(result).Should(Exit(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) }) + + It("podman checkpoint container with export and verify runtime", func() { + SkipIfRemote("podman-remote does not support --runtime flag") + localRunString := getRunString([]string{ + "--rm", + ALPINE, + "top", + }) + session := podmanTest.Podman(localRunString) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + cid := session.OutputToString() + + session = podmanTest.Podman([]string{ + "inspect", + "--format", + "{{.OCIRuntime}}", + cid, + }) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + runtime := session.OutputToString() + + fileName := "/tmp/checkpoint-" + cid + ".tar.gz" + + result := podmanTest.Podman([]string{ + "container", + "checkpoint", + cid, "-e", + fileName, + }) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + result = podmanTest.Podman([]string{ + "container", + "restore", + "-i", + fileName, + }) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up")) + + // The restored container should have the same runtime as the original container + result = podmanTest.Podman([]string{ + "inspect", + "--format", + "{{.OCIRuntime}}", + cid, + }) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(session.OutputToString()).To(Equal(runtime)) + + // Remove exported checkpoint + os.Remove(fileName) + }) + + It("podman checkpoint container with export and try to change the runtime", func() { + SkipIfRemote("podman-remote does not support --runtime flag") + // This test will only run if runc and crun both exist + if !strings.Contains(podmanTest.OCIRuntime, "crun") { + Skip("Test requires crun and runc") + } + cmd := exec.Command("runc") + if err := cmd.Start(); err != nil { + Skip("Test requires crun and runc") + } + if err := cmd.Wait(); err != nil { + Skip("Test requires crun and runc") + } + localRunString := getRunString([]string{ + "--rm", + ALPINE, + "top", + }) + // Let's start a container with runc and try to restore it with crun (expected to fail) + localRunString = append( + []string{ + "--runtime", + "runc", + }, + localRunString..., + ) + session := podmanTest.Podman(localRunString) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + cid := session.OutputToString() + + session = podmanTest.Podman([]string{ + "inspect", + "--format", + "{{.OCIRuntime}}", + cid, + }) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + runtime := session.OutputToString() + + fileName := "/tmp/checkpoint-" + cid + ".tar.gz" + + result := podmanTest.Podman([]string{ + "container", + "checkpoint", + cid, "-e", + fileName, + }) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // This should fail as the container was checkpointed with runc + result = podmanTest.Podman([]string{ + "--runtime", + "crun", + "container", + "restore", + "-i", + fileName, + }) + result.WaitWithDefaultTimeout() + + Expect(result).Should(Exit(125)) + Expect(result.ErrorToString()).To( + ContainSubstring("and cannot be restored with runtime"), + ) + + result = podmanTest.Podman([]string{ + "--runtime", + "runc", + "container", + "restore", + "-i", + fileName, + }) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + + result = podmanTest.Podman([]string{ + "inspect", + "--format", + "{{.OCIRuntime}}", + cid, + }) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(result.OutputToString()).To(Equal(runtime)) + + result = podmanTest.Podman([]string{ + "--runtime", + "runc", + "rm", + "-fa", + }) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + // Remove exported checkpoint + os.Remove(fileName) + }) }) -- cgit v1.2.3-54-g00ecf