From d5f1caaf505a87f093b3c715e9c53ee304917ac4 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Wed, 26 Jun 2019 12:01:55 +0000 Subject: Add function to get a filtered tarstream diff The newly added function GetDiffTarStream() mirrors the GetDiff() function. It tries to get the correct layer ID from getLayerID() and it filters out containerMounts from the tarstream. Thus the behavior is the same as GetDiff(), but it returns a tarstream. This also adds the function ApplyDiffTarStream() to apply the tarstream generated by GetDiffTarStream(). These functions are targeted to support container migration with root file-system changes. Signed-off-by: Adrian Reber --- libpod/diff.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'libpod') diff --git a/libpod/diff.go b/libpod/diff.go index f348e6b81..925bda927 100644 --- a/libpod/diff.go +++ b/libpod/diff.go @@ -1,6 +1,9 @@ package libpod import ( + "archive/tar" + "io" + "github.com/containers/libpod/libpod/layers" "github.com/containers/storage/pkg/archive" "github.com/pkg/errors" @@ -44,6 +47,59 @@ func (r *Runtime) GetDiff(from, to string) ([]archive.Change, error) { return rchanges, err } +// skipFileInTarAchive is an archive.TarModifierFunc function +// which tells archive.ReplaceFileTarWrapper to skip files +// from the tarstream +func skipFileInTarAchive(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) { + return nil, nil, nil +} + +// GetDiffTarStream returns the differences between the two images, layers, or containers. +// It is the same functionality as GetDiff() except that it returns a tarstream +func (r *Runtime) GetDiffTarStream(from, to string) (io.ReadCloser, error) { + toLayer, err := r.getLayerID(to) + if err != nil { + return nil, err + } + fromLayer := "" + if from != "" { + fromLayer, err = r.getLayerID(from) + if err != nil { + return nil, err + } + } + rc, err := r.store.Diff(fromLayer, toLayer, nil) + if err != nil { + return nil, err + } + + // Skip files in the tar archive which are listed + // in containerMounts map. Just as in the GetDiff() + // function from above + filterMap := make(map[string]archive.TarModifierFunc) + for key := range containerMounts { + filterMap[key[1:]] = skipFileInTarAchive + // In the tarstream directories always include a trailing '/'. + // For simplicity this duplicates every entry from + // containerMounts with a trailing '/', as containerMounts + // does not use trailing '/' for directories. + filterMap[key[1:]+"/"] = skipFileInTarAchive + } + + filteredTarStream := archive.ReplaceFileTarWrapper(rc, filterMap) + return filteredTarStream, nil +} + +// ApplyDiffTarStream applies the changes stored in 'diff' to the layer 'to' +func (r *Runtime) ApplyDiffTarStream(to string, diff io.Reader) error { + toLayer, err := r.getLayerID(to) + if err != nil { + return err + } + _, err = r.store.ApplyDiff(toLayer, diff) + return err +} + // GetLayerID gets a full layer id given a full or partial id // If the id matches a container or image, the id of the top layer is returned // If the id matches a layer, the top layer id is returned -- cgit v1.2.3-54-g00ecf From 217f2e77f86c95babc9697545e8880af38ec2e89 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 27 Jun 2019 06:16:32 +0000 Subject: Include root file-system changes in container migration One of the last limitations when migrating a container using Podman's 'podman container checkpoint --export=/path/to/archive.tar.gz' was that it was necessary to manually handle changes to the container's root file-system. The recommendation was to mount everything as --tmpfs where the root file-system was changed. This extends the checkpoint export functionality to also include all changes to the root file-system in the checkpoint archive. The checkpoint archive now includes a tarstream of the result from 'podman diff'. This tarstream will be applied to the restored container before restoring the container. With this any container can now be migrated, even it there are changes to the root file-system. There was some discussion before implementing this to base the root file-system migration on 'podman commit', but it seemed wrong to do a 'podman commit' before the migration as that would change the parent layer the restored container is referencing. Probably not really a problem, but it would have meant that a migrated container will always reference another storage top layer than it used to reference during initial creation. Signed-off-by: Adrian Reber --- libpod/container_internal_linux.go | 39 +++++++++++++++++++++++++++++++++++++- pkg/adapter/checkpoint_restore.go | 1 + 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'libpod') diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index aa477611f..77f607f0b 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -515,6 +515,25 @@ func (c *Container) exportCheckpoint(dest string) (err error) { 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) + + // Get root file-system changes included in the checkpoint archive + rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") + rootfsDiffFile, err := os.Create(rootfsDiffPath) + if err != nil { + return errors.Wrapf(err, "error creating root file-system diff file %q", rootfsDiffPath) + } + tarStream, err := c.runtime.GetDiffTarStream("", c.ID()) + if err != nil { + return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + } + _, err = io.Copy(rootfsDiffFile, tarStream) + if err != nil { + return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + } + tarStream.Close() + rootfsDiffFile.Close() + + input, err := archive.TarWithOptions(c.bundlePath(), &archive.TarOptions{ Compression: archive.Gzip, IncludeSourceDir: true, @@ -524,6 +543,7 @@ func (c *Container) exportCheckpoint(dest string) (err error) { "ctr.log", "config.dump", "spec.dump", + "rootfs-diff.tar", "network.status"}, }) @@ -546,6 +566,8 @@ func (c *Container) exportCheckpoint(dest string) (err error) { return err } + os.Remove(rootfsDiffPath) + return nil } @@ -792,6 +814,21 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti if err := c.saveSpec(g.Spec()); err != nil { return err } + + // Before actually restarting the container, apply the root file-system changes + rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") + if _, err := os.Stat(rootfsDiffPath); err == nil { + // Only do this if a rootfs-diff.tar actually exists + rootfsDiffFile, err := os.Open(rootfsDiffPath) + if err != nil { + return errors.Wrapf(err, "Failed to open root file-system diff file %s", rootfsDiffPath) + } + if err := c.runtime.ApplyDiffTarStream(c.ID(), rootfsDiffFile); err != nil { + return errors.Wrapf(err, "Failed to apply root file-system diff file %s", rootfsDiffPath) + } + rootfsDiffFile.Close() + } + if err := c.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil { return err } @@ -809,7 +846,7 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti 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"} + cleanup := [...]string{"restore.log", "dump.log", "stats-dump", "stats-restore", "network.status", "rootfs-diff.tar"} for _, del := range cleanup { file := filepath.Join(c.bundlePath(), del) err = os.Remove(file) diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/adapter/checkpoint_restore.go index 97ba5ecf7..1514a3414 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/adapter/checkpoint_restore.go @@ -55,6 +55,7 @@ func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri "checkpoint", "artifacts", "ctr.log", + "rootfs-diff.tar", "network.status", }, } -- cgit v1.2.3-54-g00ecf From 1a3207488477876a1f4018bf0df95ed8eaf85afc Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 27 Jun 2019 07:10:27 +0000 Subject: Fix typo in checkpoint/restore related texts Signed-off-by: Adrian Reber --- cmd/podman/restore.go | 2 +- libpod/container_api.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) (limited to 'libpod') diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go index c4146eff0..7ecec24ee 100644 --- a/cmd/podman/restore.go +++ b/cmd/podman/restore.go @@ -61,7 +61,7 @@ func restoreCmd(c *cliconfig.RestoreValues, cmd *cobra.Command) error { defer runtime.DeferredShutdown(false) if c.Import == "" && c.Name != "" { - return errors.Errorf("--name can only used with --import") + return errors.Errorf("--name can only be used with --import") } if c.Name != "" && c.TcpEstablished { diff --git a/libpod/container_api.go b/libpod/container_api.go index 3dd84b02c..00467d2bf 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -792,10 +792,8 @@ 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 tells the API to read (or write) the checkpoint image + // from (or to) 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 -- cgit v1.2.3-54-g00ecf From 05549e8b2904427a1fb6d0b36889ac43360db073 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Thu, 27 Jun 2019 07:56:39 +0000 Subject: Add --ignore-rootfs option for checkpoint/restore The newly added functionality to include the container's root file-system changes into the checkpoint archive can now be explicitly disabled. Either during checkpoint or during restore. If a container changes a lot of files during its runtime it might be more effective to migrated the root file-system changes in some other way and to not needlessly increase the size of the checkpoint archive. If a checkpoint archive does not contain the root file-system changes information it will automatically be skipped. If the root file-system changes are part of the checkpoint archive it is also possible to tell Podman to ignore these changes. Signed-off-by: Adrian Reber --- cmd/podman/checkpoint.go | 1 + cmd/podman/cliconfig/config.go | 2 + cmd/podman/restore.go | 5 +++ completions/bash/podman | 2 + docs/podman-container-checkpoint.1.md | 10 ++++- docs/podman-container-restore.1.md | 7 ++++ libpod/container_api.go | 3 ++ libpod/container_internal_linux.go | 73 +++++++++++++++++++---------------- pkg/adapter/containers.go | 5 +++ pkg/adapter/containers_remote.go | 6 +++ 10 files changed, 79 insertions(+), 35 deletions(-) (limited to 'libpod') diff --git a/cmd/podman/checkpoint.go b/cmd/podman/checkpoint.go index 6755bb073..22cdb1f39 100644 --- a/cmd/podman/checkpoint.go +++ b/cmd/podman/checkpoint.go @@ -46,6 +46,7 @@ func init() { 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") + flags.BoolVar(&checkpointCommand.IgnoreRootfs, "ignore-rootfs", false, "Do not include root file-system changes when exporting") markFlagHiddenForRemoteClient("latest", flags) } diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index e3e2edb95..be380cda0 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -92,6 +92,7 @@ type CheckpointValues struct { All bool Latest bool Export string + IgnoreRootfs bool } type CommitValues struct { @@ -433,6 +434,7 @@ type RestoreValues struct { TcpEstablished bool Import string Name string + IgnoreRootfs bool } type RmValues struct { diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go index 7ecec24ee..3ae141d41 100644 --- a/cmd/podman/restore.go +++ b/cmd/podman/restore.go @@ -45,6 +45,7 @@ func init() { 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)") + flags.BoolVar(&restoreCommand.IgnoreRootfs, "ignore-rootfs", false, "Do not apply root file-system changes when importing from exported checkpoint") markFlagHiddenForRemoteClient("latest", flags) } @@ -60,6 +61,10 @@ func restoreCmd(c *cliconfig.RestoreValues, cmd *cobra.Command) error { } defer runtime.DeferredShutdown(false) + if c.Import == "" && c.IgnoreRootfs { + return errors.Errorf("--ignore-rootfs can only be used with --import") + } + if c.Import == "" && c.Name != "" { return errors.Errorf("--name can only be used with --import") } diff --git a/completions/bash/podman b/completions/bash/podman index 65c6308cc..619c9e1e6 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -758,6 +758,7 @@ _podman_container_checkpoint() { -R --leave-running --tcp-established + --ignore-rootfs " case "$prev" in -e|--export) @@ -870,6 +871,7 @@ _podman_container_restore() { -l --latest --tcp-established + --ignore-rootfs " case "$prev" in -i|--import) diff --git a/docs/podman-container-checkpoint.1.md b/docs/podman-container-checkpoint.1.md index 90c3919a9..034d338bb 100644 --- a/docs/podman-container-checkpoint.1.md +++ b/docs/podman-container-checkpoint.1.md @@ -42,7 +42,15 @@ connections. 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. +migration. This checkpoint archive also includes all changes to the container's +root file-system, if not explicitly disabled using **--ignore-rootfs** + +**--ignore-rootfs** + +This only works in combination with **--export, -e**. If a checkpoint is +exported to a tar.gz file it is possible with the help of **--ignore-rootfs** +to explicitly disable including changes to the root file-system into +the checkpoint archive file. ## EXAMPLE diff --git a/docs/podman-container-restore.1.md b/docs/podman-container-restore.1.md index c96a37f80..544a096d8 100644 --- a/docs/podman-container-restore.1.md +++ b/docs/podman-container-restore.1.md @@ -60,6 +60,13 @@ address to the container it was using before checkpointing as each IP address ca 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**. +**--ignore-rootfs** + +This is only available in combination with **--import, -i**. If a container is restored +from a checkpoint tar.gz file it is possible that it also contains all root file-system +changes. With **--ignore-rootfs** it is possible to explicitly disable applying these +root file-system changes to the restored container. + ## EXAMPLE podman container restore mywebserver diff --git a/libpod/container_api.go b/libpod/container_api.go index 00467d2bf..c6e478846 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -799,6 +799,9 @@ type ContainerCheckpointOptions struct { // checkpoint archive a new name should be used for the // restored container Name string + // IgnoreRootfs tells the API to not export changes to + // the container's root file-system (or to not import) + IgnoreRootfs bool } // Checkpoint checkpoints a container diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 77f607f0b..399220b9a 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -510,41 +510,44 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } -func (c *Container) exportCheckpoint(dest string) (err error) { +func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) (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) + includeFiles := []string{ + "checkpoint", + "artifacts", + "ctr.log", + "config.dump", + "spec.dump", + "network.status"} + // Get root file-system changes included in the checkpoint archive rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") - rootfsDiffFile, err := os.Create(rootfsDiffPath) - if err != nil { - return errors.Wrapf(err, "error creating root file-system diff file %q", rootfsDiffPath) - } - tarStream, err := c.runtime.GetDiffTarStream("", c.ID()) - if err != nil { - return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) - } - _, err = io.Copy(rootfsDiffFile, tarStream) - if err != nil { - return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + if !ignoreRootfs { + rootfsDiffFile, err := os.Create(rootfsDiffPath) + if err != nil { + return errors.Wrapf(err, "error creating root file-system diff file %q", rootfsDiffPath) + } + tarStream, err := c.runtime.GetDiffTarStream("", c.ID()) + if err != nil { + return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + } + _, err = io.Copy(rootfsDiffFile, tarStream) + if err != nil { + return errors.Wrapf(err, "error exporting root file-system diff to %q", rootfsDiffPath) + } + tarStream.Close() + rootfsDiffFile.Close() + includeFiles = append(includeFiles, "rootfs-diff.tar") } - tarStream.Close() - rootfsDiffFile.Close() - input, err := archive.TarWithOptions(c.bundlePath(), &archive.TarOptions{ Compression: archive.Gzip, IncludeSourceDir: true, - IncludeFiles: []string{ - "checkpoint", - "artifacts", - "ctr.log", - "config.dump", - "spec.dump", - "rootfs-diff.tar", - "network.status"}, + IncludeFiles: includeFiles, }) if err != nil { @@ -627,7 +630,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO } if options.TargetFile != "" { - if err = c.exportCheckpoint(options.TargetFile); err != nil { + if err = c.exportCheckpoint(options.TargetFile, options.IgnoreRootfs); err != nil { return err } } @@ -816,17 +819,19 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti } // Before actually restarting the container, apply the root file-system changes - rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") - if _, err := os.Stat(rootfsDiffPath); err == nil { - // Only do this if a rootfs-diff.tar actually exists - rootfsDiffFile, err := os.Open(rootfsDiffPath) - if err != nil { - return errors.Wrapf(err, "Failed to open root file-system diff file %s", rootfsDiffPath) - } - if err := c.runtime.ApplyDiffTarStream(c.ID(), rootfsDiffFile); err != nil { - return errors.Wrapf(err, "Failed to apply root file-system diff file %s", rootfsDiffPath) + if !options.IgnoreRootfs { + rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar") + if _, err := os.Stat(rootfsDiffPath); err == nil { + // Only do this if a rootfs-diff.tar actually exists + rootfsDiffFile, err := os.Open(rootfsDiffPath) + if err != nil { + return errors.Wrapf(err, "Failed to open root file-system diff file %s", rootfsDiffPath) + } + if err := c.runtime.ApplyDiffTarStream(c.ID(), rootfsDiffFile); err != nil { + return errors.Wrapf(err, "Failed to apply root file-system diff file %s", rootfsDiffPath) + } + rootfsDiffFile.Close() } - rootfsDiffFile.Close() } if err := c.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil { diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 1cf9d686a..e0245d2ac 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -524,6 +524,10 @@ func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues) error { KeepRunning: c.LeaveRunning, TCPEstablished: c.TcpEstablished, TargetFile: c.Export, + IgnoreRootfs: c.IgnoreRootfs, + } + if c.Export == "" && c.IgnoreRootfs { + return errors.Errorf("--ignore-rootfs can only be used with --export") } if c.All { containers, err = r.Runtime.GetRunningContainers() @@ -560,6 +564,7 @@ func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues) TCPEstablished: c.TcpEstablished, TargetFile: c.Import, Name: c.Name, + IgnoreRootfs: c.IgnoreRootfs, } filterFuncs = append(filterFuncs, func(c *libpod.Container) bool { diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index 5836d0788..5e8df1b62 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -668,6 +668,9 @@ func (r *LocalRuntime) Checkpoint(c *cliconfig.CheckpointValues) error { if c.Export != "" { return errors.New("the remote client does not support exporting checkpoints") } + if c.IgnoreRootfs { + return errors.New("the remote client does not support --ignore-rootfs") + } var lastError error ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs) @@ -708,6 +711,9 @@ func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues) if c.Import != "" { return errors.New("the remote client does not support importing checkpoints") } + if c.IgnoreRootfs { + return errors.New("the remote client does not support --ignore-rootfs") + } var lastError error ids, err := iopodman.GetContainersByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs) -- cgit v1.2.3-54-g00ecf