From b7f33f8d7bb179779e8d4771a56f7bad84779144 Mon Sep 17 00:00:00 2001 From: Kouame Behouba Manasse Date: Wed, 28 Jun 2023 03:33:14 +0300 Subject: [PATCH 1/4] show: update `show` sub-command This commit updates the show sub-command. The `show` sub-command is now used only for a quick overview of checkpoints in table format and doesn't support any flags. The test suite is also updated according to the new expected behavior of the `show` sub-command. Signed-off-by: Kouame Behouba Manasse --- checkpointctl.go | 92 ++++---------------------------- container.go | 114 ---------------------------------------- test/checkpointctl.bats | 110 -------------------------------------- 3 files changed, 11 insertions(+), 305 deletions(-) diff --git a/checkpointctl.go b/checkpointctl.go index b10b3b54..ac8558aa 100644 --- a/checkpointctl.go +++ b/checkpointctl.go @@ -4,22 +4,15 @@ package main import ( "fmt" - "log" "os" - "path/filepath" metadata "github.com/checkpoint-restore/checkpointctl/lib" "github.com/spf13/cobra" ) var ( - name string - version string - stats bool - mounts bool - fullPaths bool - psTree bool - showAll bool + name string + version string ) func main() { @@ -42,66 +35,17 @@ func main() { func setupShow() *cobra.Command { cmd := &cobra.Command{ - Use: "show", - Short: "Show information about available checkpoints", - RunE: show, - Args: cobra.MinimumNArgs(1), - } - flags := cmd.Flags() - flags.BoolVar( - &stats, - "print-stats", - false, - "Print checkpointing statistics if available", - ) - flags.BoolVar( - &stats, - "stats", - false, - "Print checkpointing statistics if available", - ) - flags.BoolVar( - &mounts, - "mounts", - false, - "Print overview about mounts used in the checkpoints", - ) - flags.BoolVar( - &psTree, - "ps-tree", - false, - "Print overview about the process tree in the checkpoints", - ) - flags.BoolVar( - &fullPaths, - "full-paths", - false, - "Display mounts with full paths", - ) - flags.BoolVar( - &showAll, - "all", - false, - "Display all additional information about the checkpoints", - ) - - err := flags.MarkHidden("print-stats") - if err != nil { - log.Fatal(err) + Use: "show", + Short: "Show information about available checkpoints", + RunE: show, + Args: cobra.MinimumNArgs(1), + DisableFlagsInUseLine: true, } + return cmd } func show(cmd *cobra.Command, args []string) error { - if showAll { - stats = true - mounts = true - psTree = true - } - if fullPaths && !mounts { - return fmt.Errorf("Cannot use --full-paths without --mounts/--all option") - } - tasks := make([]task, 0, len(args)) for _, input := range args { @@ -113,23 +57,6 @@ func show(cmd *cobra.Command, args []string) error { return fmt.Errorf("input %s not a regular file", input) } - // A list of files that need to be unarchived. The files need not be - // full paths. Even a substring of the file name is valid. - files := []string{metadata.SpecDumpFile, metadata.ConfigDumpFile} - - if stats { - files = append(files, "stats-dump") - } - - if psTree { - files = append( - files, - filepath.Join(metadata.CheckpointDirectory, "pstree.img"), - // All core-*.img files - filepath.Join(metadata.CheckpointDirectory, "core-"), - ) - } - // Check if there is a checkpoint directory in the archive file checkpointDirExists, err := isFileInArchive(input, metadata.CheckpointDirectory, true) if err != nil { @@ -150,6 +77,9 @@ func show(cmd *cobra.Command, args []string) error { } }() + // A list of files that need to be unarchived. The files need not be + // full paths. Even a substring of the file name is valid. + files := []string{metadata.SpecDumpFile, metadata.ConfigDumpFile} if err := untarFiles(input, dir, files); err != nil { return err } diff --git a/container.go b/container.go index 196d6db1..86b3918f 100644 --- a/container.go +++ b/container.go @@ -16,12 +16,9 @@ import ( "time" metadata "github.com/checkpoint-restore/checkpointctl/lib" - "github.com/checkpoint-restore/go-criu/v6/crit" - "github.com/checkpoint-restore/go-criu/v6/crit/images" "github.com/containers/storage/pkg/archive" "github.com/olekukonko/tablewriter" spec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/xlab/treeprint" ) type containerMetadata struct { @@ -68,11 +65,6 @@ func getCRIOInfo(_ *metadata.ContainerConfig, specDump *spec.Spec) (*containerIn } func showContainerCheckpoints(tasks []task) error { - // Return an error early when attempting to display multiple checkpoints with additional flags - if (len(tasks) > 1) && (mounts || stats || psTree) { - return fmt.Errorf("displaying multiple checkpoints with additional flags is not supported") - } - table := tablewriter.NewWriter(os.Stdout) header := []string{ "Container", @@ -158,35 +150,6 @@ func showContainerCheckpoints(tasks []task) error { table.SetRowLine(true) table.Render() - // If there is only one checkpoint to show, check the mounts and stats flags - if len(tasks) == 1 { - if mounts { - renderMounts(specDump) - } - - if stats { - // Get dump statistics with crit - dumpStats, err := crit.GetDumpStats(tasks[0].outputDir) - if err != nil { - return fmt.Errorf("failed to get dump statistics: %w", err) - } - - renderDumpStats(dumpStats) - } - - if psTree { - // The image files reside in a subdirectory called "checkpoint" - c := crit.New("", "", filepath.Join(tasks[0].outputDir, "checkpoint"), false, false) - // Get process tree with CRIT - psTree, err := c.ExplorePs() - if err != nil { - return fmt.Errorf("failed to get process tree: %w", err) - } - - renderPsTree(psTree, ci.Name) - } - } - return nil } @@ -212,75 +175,6 @@ func getContainerInfo(checkpointDir string, specDump *spec.Spec, containerConfig return ci, nil } -func renderMounts(specDump *spec.Spec) { - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{ - "Destination", - "Type", - "Source", - }) - // Get overview of mounts from spec.dump - for _, data := range specDump.Mounts { - table.Append([]string{ - data.Destination, - data.Type, - func() string { - if fullPaths { - return data.Source - } - return shortenPath(data.Source) - }(), - }) - } - fmt.Println("\nOverview of Mounts") - table.Render() -} - -func renderDumpStats(dumpStats *images.DumpStatsEntry) { - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{ - "Freezing Time", - "Frozen Time", - "Memdump Time", - "Memwrite Time", - "Pages Scanned", - "Pages Written", - }) - table.Append([]string{ - fmt.Sprintf("%d us", dumpStats.GetFreezingTime()), - fmt.Sprintf("%d us", dumpStats.GetFrozenTime()), - fmt.Sprintf("%d us", dumpStats.GetMemdumpTime()), - fmt.Sprintf("%d us", dumpStats.GetMemwriteTime()), - fmt.Sprintf("%d", dumpStats.GetPagesScanned()), - fmt.Sprintf("%d", dumpStats.GetPagesWritten()), - }) - fmt.Println("\nCRIU dump statistics") - table.Render() -} - -func renderPsTree(psTree *crit.PsTree, containerName string) { - var tree treeprint.Tree - if containerName == "" { - containerName = "Container" - } - tree = treeprint.NewWithRoot(containerName) - // processNodes is a recursive function to create - // a new branch for each process and add its child - // processes as child nodes of the branch. - var processNodes func(treeprint.Tree, *crit.PsTree) - processNodes = func(tree treeprint.Tree, root *crit.PsTree) { - node := tree.AddMetaBranch(root.PId, root.Comm) - for _, child := range root.Children { - processNodes(node, child) - } - } - - processNodes(tree, psTree) - - fmt.Print("\nProcess tree\n\n") - fmt.Println(tree.String()) -} - func hasPrefix(path, prefix string) bool { return strings.HasPrefix(strings.TrimPrefix(path, "./"), prefix) } @@ -309,14 +203,6 @@ func getArchiveSizes(archiveInput string) (*archiveSizes, error) { return result, err } -func shortenPath(path string) string { - parts := strings.Split(path, string(filepath.Separator)) - if len(parts) <= 2 { - return path - } - return filepath.Join("..", filepath.Join(parts[len(parts)-2:]...)) -} - // untarFiles unpack only specified files from an archive to the destination directory. func untarFiles(src, dest string, files []string) error { archiveFile, err := os.Open(src) diff --git a/test/checkpointctl.bats b/test/checkpointctl.bats index bb0d71b4..fafba9c8 100644 --- a/test/checkpointctl.bats +++ b/test/checkpointctl.bats @@ -106,95 +106,6 @@ function teardown() { [[ ${lines[4]} == *"containerd"* ]] } -@test "Run checkpointctl show with tar file and --stats and missing stats-dump" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --stats - [ "$status" -eq 1 ] - [[ ${lines[6]} == *"failed to get dump statistics"* ]] -} - -@test "Run checkpointctl show with tar file and --stats and invalid stats-dump" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1"/stats-dump - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --stats - [ "$status" -eq 1 ] - [[ ${lines[6]} == *"Unknown magic"* ]] -} - -@test "Run checkpointctl show with tar file and --stats and valid stats-dump" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - cp test-imgs/stats-dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --stats - [ "$status" -eq 0 ] - [[ ${lines[6]} == *"CRIU dump statistics"* ]] - [[ ${lines[8]} == *"MEMWRITE TIME"* ]] - [[ ${lines[10]} =~ [1-9]+" us" ]] -} - -@test "Run checkpointctl show with tar file and --mounts and valid spec.dump" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --mounts - [ "$status" -eq 0 ] - [[ ${lines[6]} == *"Overview of Mounts"* ]] - [[ ${lines[8]} == *"DESTINATION"* ]] - [[ ${lines[10]} == *"/proc"* ]] -} - -@test "Run checkpointctl show with tar file and --mounts and --full-paths and valid spec.dump" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --mounts --full-paths - [ "$status" -eq 0 ] - [[ ${lines[6]} == *"Overview of Mounts"* ]] - [[ ${lines[8]} == *"DESTINATION"* ]] - [[ ${lines[10]} == *"/proc"* ]] -} - -@test "Run checkpointctl show with tar file and --all and valid spec.dump and valid stats-dump" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - cp test-imgs/stats-dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - cp test-imgs/pstree.img \ - test-imgs/core-*.img "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --all - [ "$status" -eq 0 ] - [[ ${lines[6]} == *"Overview of Mounts"* ]] - [[ ${lines[8]} == *"DESTINATION"* ]] - [[ ${lines[10]} == *"/proc"* ]] - [[ ${lines[11]} == *"/etc/hostname"* ]] - [[ ${lines[13]} == *"CRIU dump statistics"* ]] - [[ ${lines[15]} == *"MEMWRITE TIME"* ]] - [[ ${lines[17]} =~ [1-9]+" us" ]] - [[ ${lines[19]} == *"Process tree"* ]] - [[ ${lines[21]} == *"piggie"* ]] -} - -@test "Run checkpointctl show with tar file and missing --mounts/--all and --full-paths" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --full-paths - [ "$status" -eq 1 ] - [[ ${lines[0]} == *"Error: Cannot use --full-paths without --mounts/--all option"* ]] -} - @test "Run checkpointctl show with tar file with valid config.dump and valid spec.dump (CRI-O) and no checkpoint directory" { cp data/config.dump "$TEST_TMP_DIR1" cp data/spec.dump.cri-o "$TEST_TMP_DIR1"/spec.dump @@ -258,18 +169,6 @@ function teardown() { [[ ${lines[2]} == *"ROOT FS DIFF SIZE"* ]] } -@test "Run checkpointctl show with tar file and --ps-tree" { - cp data/config.dump \ - data/spec.dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - cp test-imgs/pstree.img \ - test-imgs/core-*.img "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/test.tar --ps-tree - [ "$status" -eq 0 ] - [[ ${lines[8]} == *"piggie"* ]] -} - @test "Run checkpointctl show with multiple tar files" { cp data/config.dump "$TEST_TMP_DIR1" cp data/spec.dump "$TEST_TMP_DIR1" @@ -281,12 +180,3 @@ function teardown() { [[ ${lines[5]} == *"Podman"* ]] } -@test "Run checkpointctl show with multiple tar files with additional flags" { - cp data/config.dump "$TEST_TMP_DIR1" - cp data/spec.dump "$TEST_TMP_DIR1" - mkdir "$TEST_TMP_DIR1"/checkpoint - ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test1.tar . && tar cf "$TEST_TMP_DIR2"/test2.tar . ) - checkpointctl show "$TEST_TMP_DIR2"/*.tar --all - [ "$status" -eq 1 ] - [[ ${lines[0]} == *"displaying multiple checkpoints with additional flags is not supported"* ]] -} From 489ddf8c135833eb2e416e9b98f7fe40f8da58b7 Mon Sep 17 00:00:00 2001 From: Kouame Behouba Manasse Date: Wed, 28 Jun 2023 05:02:24 +0300 Subject: [PATCH 2/4] inspect: add new sub-command `inspect` This commit introduces a new sub-command `inspect`. This new sub-command provides more detailed information about checkpoints using a default tree output format. The `inspect` sub-command inherits all the flags previously used for the show sub-command, with the exception of --full-paths which has been removed. This change was made because the tree and json formats now provide more compact representations, allowing us to display the full paths by default. Signed-off-by: Kouame Behouba Manasse --- checkpointctl.go | 158 ++++++++++++++++++++++++++++++++++++++++------- tree.go | 130 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 tree.go diff --git a/checkpointctl.go b/checkpointctl.go index ac8558aa..627a7371 100644 --- a/checkpointctl.go +++ b/checkpointctl.go @@ -4,7 +4,9 @@ package main import ( "fmt" + "log" "os" + "path/filepath" metadata "github.com/checkpoint-restore/checkpointctl/lib" "github.com/spf13/cobra" @@ -13,6 +15,11 @@ import ( var ( name string version string + format string + stats bool + mounts bool + psTree bool + showAll bool ) func main() { @@ -26,6 +33,10 @@ func main() { showCommand := setupShow() rootCommand.AddCommand(showCommand) + + inspectCommand := setupInspect() + rootCommand.AddCommand(inspectCommand) + rootCommand.Version = version if err := rootCommand.Execute(); err != nil { @@ -36,7 +47,7 @@ func main() { func setupShow() *cobra.Command { cmd := &cobra.Command{ Use: "show", - Short: "Show information about available checkpoints", + Short: "Show an overview of container checkpoints", RunE: show, Args: cobra.MinimumNArgs(1), DisableFlagsInUseLine: true, @@ -46,51 +57,156 @@ func setupShow() *cobra.Command { } func show(cmd *cobra.Command, args []string) error { + // Only "spec.dump" and "config.dump" are need when for the show sub-command + requiredFiles := []string{metadata.SpecDumpFile, metadata.ConfigDumpFile} + tasks, err := createTasks(args, requiredFiles) + if err != nil { + return err + } + defer cleanupTasks(tasks) + + return showContainerCheckpoints(tasks) +} + +func setupInspect() *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect", + Short: "Display low-level information about a container checkpoint", + RunE: inspect, + Args: cobra.MinimumNArgs(1), + } + flags := cmd.Flags() + + flags.BoolVar( + &stats, + "print-stats", + false, + "Display checkpoint statistics", + ) + flags.BoolVar( + &stats, + "stats", + false, + "Display checkpoint statistics", + ) + flags.BoolVar( + &mounts, + "mounts", + false, + "Display an overview of mounts used in the checkpoint", + ) + flags.BoolVar( + &psTree, + "ps-tree", + false, + "Display an overview of processes in the container checkpoint", + ) + flags.BoolVar( + &showAll, + "all", + false, + "Show all information about container checkpoint", + ) + flags.StringVar( + &format, + "format", + "tree", + "Specify the output format: tree or json", + ) + + err := flags.MarkHidden("print-stats") + if err != nil { + log.Fatal(err) + } + + return cmd +} + +func inspect(cmd *cobra.Command, args []string) error { + if showAll { + stats = true + mounts = true + psTree = true + } + + requiredFiles := []string{metadata.SpecDumpFile, metadata.ConfigDumpFile} + + if stats { + requiredFiles = append(requiredFiles, "stats-dump") + } + + if psTree { + requiredFiles = append( + requiredFiles, + filepath.Join(metadata.CheckpointDirectory, "pstree.img"), + // All core-*.img files + filepath.Join(metadata.CheckpointDirectory, "core-"), + ) + } + + tasks, err := createTasks(args, requiredFiles) + if err != nil { + return err + } + defer cleanupTasks(tasks) + + switch format { + case "tree": + return renderTreeView(tasks) + case "json": + return fmt.Errorf("json format is not supported yet") + default: + return fmt.Errorf("invalid output format: %s", format) + } +} + +type task struct { + checkpointFilePath string + outputDir string +} + +func createTasks(args []string, requiredFiles []string) ([]task, error) { tasks := make([]task, 0, len(args)) for _, input := range args { tar, err := os.Stat(input) if err != nil { - return err + return nil, err } if !tar.Mode().IsRegular() { - return fmt.Errorf("input %s not a regular file", input) + return nil, fmt.Errorf("input %s not a regular file", input) } // Check if there is a checkpoint directory in the archive file checkpointDirExists, err := isFileInArchive(input, metadata.CheckpointDirectory, true) if err != nil { - return err + return nil, err } if !checkpointDirExists { - return fmt.Errorf("checkpoint directory is missing in the archive file: %s", input) + return nil, fmt.Errorf("checkpoint directory is missing in the archive file: %s", input) } dir, err := os.MkdirTemp("", "checkpointctl") if err != nil { - return err + return nil, err } - defer func() { - if err := os.RemoveAll(dir); err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - - // A list of files that need to be unarchived. The files need not be - // full paths. Even a substring of the file name is valid. - files := []string{metadata.SpecDumpFile, metadata.ConfigDumpFile} - if err := untarFiles(input, dir, files); err != nil { - return err + + if err := untarFiles(input, dir, requiredFiles); err != nil { + return nil, err } tasks = append(tasks, task{checkpointFilePath: input, outputDir: dir}) } - return showContainerCheckpoints(tasks) + return tasks, nil } -type task struct { - checkpointFilePath string - outputDir string +// cleanupTasks removes all output directories of given tasks +func cleanupTasks(tasks []task) { + for _, task := range tasks { + if err := os.RemoveAll(task.outputDir); err != nil { + fmt.Fprintln(os.Stderr, err) + } + } } diff --git a/tree.go b/tree.go new file mode 100644 index 00000000..1a74190a --- /dev/null +++ b/tree.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "path/filepath" + + metadata "github.com/checkpoint-restore/checkpointctl/lib" + "github.com/checkpoint-restore/go-criu/v6/crit" + "github.com/checkpoint-restore/go-criu/v6/crit/images" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/xlab/treeprint" +) + +func renderTreeView(tasks []task) error { + for _, task := range tasks { + containerConfig, _, err := metadata.ReadContainerCheckpointConfigDump(task.outputDir) + if err != nil { + return err + } + + specDump, _, err := metadata.ReadContainerCheckpointSpecDump(task.outputDir) + if err != nil { + return err + } + + ci, err := getContainerInfo(task.outputDir, specDump, containerConfig) + if err != nil { + return err + } + + archiveSizes, err := getArchiveSizes(task.checkpointFilePath) + if err != nil { + return err + } + + tree := buildTree(ci, containerConfig, archiveSizes) + + if mounts { + addMountsToTree(tree, specDump) + } + + if stats { + dumpStats, err := crit.GetDumpStats(task.outputDir) + if err != nil { + return fmt.Errorf("failed to get dump statistics: %w", err) + } + + addDumpStatsToTree(tree, dumpStats) + } + + if psTree { + c := crit.New("", "", filepath.Join(task.outputDir, "checkpoint"), false, false) + psTree, err := c.ExplorePs() + if err != nil { + return fmt.Errorf("failed to get process tree: %w", err) + } + + addPsTreeToTree(tree, psTree) + } + + fmt.Printf("\nDisplaying container checkpoint tree view from %s\n\n", task.checkpointFilePath) + fmt.Println(tree.String()) + } + + return nil +} + +func buildTree(ci *containerInfo, containerConfig *metadata.ContainerConfig, archiveSizes *archiveSizes) treeprint.Tree { + if ci.Name == "" { + ci.Name = "Container" + } + tree := treeprint.NewWithRoot(ci.Name) + + tree.AddBranch(fmt.Sprintf("Image: %s", containerConfig.RootfsImageName)) + tree.AddBranch(fmt.Sprintf("ID: %s", containerConfig.ID)) + tree.AddBranch(fmt.Sprintf("Runtime: %s", containerConfig.OCIRuntime)) + tree.AddBranch(fmt.Sprintf("Created: %s", ci.Created)) + tree.AddBranch(fmt.Sprintf("Engine: %s", ci.Engine)) + + if ci.IP != "" { + tree.AddBranch(fmt.Sprintf("IP: %s", ci.IP)) + } + if ci.MAC != "" { + tree.AddBranch(fmt.Sprintf("MAC: %s", ci.MAC)) + } + + tree.AddBranch(fmt.Sprintf("Checkpoint Size: %s", metadata.ByteToString(archiveSizes.checkpointSize))) + + if archiveSizes.rootFsDiffTarSize != 0 { + tree.AddBranch(fmt.Sprintf("Root Fs Diff Size: %s", metadata.ByteToString(archiveSizes.rootFsDiffTarSize))) + } + + return tree +} + +func addMountsToTree(tree treeprint.Tree, specDump *spec.Spec) { + mountsTree := tree.AddBranch("Overview of Mounts") + for _, data := range specDump.Mounts { + mountTree := mountsTree.AddBranch(fmt.Sprintf("Destination: %s", data.Destination)) + mountTree.AddBranch(fmt.Sprintf("Type: %s", data.Type)) + mountTree.AddBranch(fmt.Sprintf("Source: %s", func() string { + return data.Source + }())) + } +} + +func addDumpStatsToTree(tree treeprint.Tree, dumpStats *images.DumpStatsEntry) { + statsTree := tree.AddBranch("CRIU dump statistics") + statsTree.AddBranch(fmt.Sprintf("Freezing Time: %d us", dumpStats.GetFreezingTime())) + statsTree.AddBranch(fmt.Sprintf("Frozen Time: %d us", dumpStats.GetFrozenTime())) + statsTree.AddBranch(fmt.Sprintf("Memdump Time: %d us", dumpStats.GetMemdumpTime())) + statsTree.AddBranch(fmt.Sprintf("Memwrite Time: %d us", dumpStats.GetMemwriteTime())) + statsTree.AddBranch(fmt.Sprintf("Pages Scanned: %d us", dumpStats.GetPagesScanned())) + statsTree.AddBranch(fmt.Sprintf("Pages Written: %d us", dumpStats.GetPagesWritten())) +} + +func addPsTreeToTree(tree treeprint.Tree, psTree *crit.PsTree) { + // processNodes is a recursive function to create + // a new branch for each process and add its child + // processes as child nodes of the branch. + var processNodes func(treeprint.Tree, *crit.PsTree) + processNodes = func(tree treeprint.Tree, root *crit.PsTree) { + node := tree.AddMetaBranch(root.PId, root.Comm) + for _, child := range root.Children { + processNodes(node, child) + } + } + psTreeNode := tree.AddBranch("Process tree") + processNodes(psTreeNode, psTree) +} From a8a7abd6f261ca5a1261622000cfd35c3e8f612c Mon Sep 17 00:00:00 2001 From: Kouame Behouba Manasse Date: Wed, 28 Jun 2023 05:34:04 +0300 Subject: [PATCH 3/4] test: add tests for inspect sub-command This commit adds tests for the default output format (tree) of the `inspect` sub-command. Signed-off-by: Kouame Behouba Manasse --- test/checkpointctl.bats | 176 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/test/checkpointctl.bats b/test/checkpointctl.bats index fafba9c8..4564baeb 100644 --- a/test/checkpointctl.bats +++ b/test/checkpointctl.bats @@ -180,3 +180,179 @@ function teardown() { [[ ${lines[5]} == *"Podman"* ]] } +@test "Run checkpointctl inspect with invalid format" { + touch "$TEST_TMP_DIR1"/config.dump + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --format=xml + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"invalid output format"* ]] +} + +@test "Run checkpointctl inspect with tar file with empty config.dump" { + touch "$TEST_TMP_DIR1"/config.dump + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"config.dump: unexpected end of JSON input" ]] +} + +@test "Run checkpointctl inspect with tar file with valid config.dump and no spec.dump" { + cp data/config.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"spec.dump: no such file or directory" ]] +} + +@test "Run checkpointctl inspect with tar file with valid config.dump and empty spec.dump" { + cp data/config.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + touch "$TEST_TMP_DIR1"/spec.dump + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"spec.dump: unexpected end of JSON input" ]] +} + +@test "Run checkpointctl inspect with tar file and --stats and missing stats-dump" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --stats + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"failed to get dump statistics"* ]] +} + +@test "Run checkpointctl inspect with tar file and --stats and invalid stats-dump" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1"/stats-dump + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --stats + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"Unknown magic"* ]] +} + +@test "Run checkpointctl inspect with tar file and --stats and valid stats-dump" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + cp test-imgs/stats-dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --stats + [ "$status" -eq 0 ] + [[ ${lines[8]} == *"CRIU dump statistics"* ]] + [[ ${lines[12]} == *"Memwrite Time"* ]] + [[ ${lines[13]} =~ [1-9]+" us" ]] +} + +@test "Run checkpointctl inspect with tar file and --mounts and valid spec.dump" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --mounts + [ "$status" -eq 0 ] + [[ ${lines[8]} == *"Overview of Mounts"* ]] + [[ ${lines[9]} == *"Destination"* ]] + [[ ${lines[10]} == *"proc"* ]] +} + +@test "Run checkpointctl inspect with tar file and --ps-tree and missing pstree.img" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --ps-tree + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"failed to get process tree"* ]] +} + +@test "Run checkpointctl inspect with tar file and --all and valid spec.dump and valid stats-dump" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + cp test-imgs/stats-dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + cp test-imgs/pstree.img \ + test-imgs/core-*.img "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --all + [ "$status" -eq 0 ] + [[ ${lines[8]} == *"Overview of Mounts"* ]] + [[ ${lines[9]} == *"Destination"* ]] + [[ ${lines[10]} == *"proc"* ]] + [[ ${lines[12]} == *"/etc/hostname"* ]] + [[ ${lines[15]} == *"CRIU dump statistics"* ]] + [[ ${lines[19]} == *"Memwrite Time"* ]] + [[ ${lines[20]} =~ [1-9]+" us" ]] + [[ ${lines[22]} == *"Process tree"* ]] + [[ ${lines[23]} == *"piggie"* ]] +} + +@test "Run checkpointctl inspect with tar file with valid config.dump and valid spec.dump and no checkpoint directory" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 1 ] + [[ ${lines[0]} == *"Error: checkpoint directory is missing in the archive file"* ]] +} + +@test "Run checkpointctl inspect with tar file with valid config.dump and valid spec.dump and checkpoint directory" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 0 ] + [[ ${lines[6]} == *"Podman"* ]] +} + +@test "Run checkpointctl inspect with tar file from containerd with valid config.dump and valid spec.dump and checkpoint directory" { + cp data/config.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + echo "{}" > "$TEST_TMP_DIR1"/status + echo "{}" > "$TEST_TMP_DIR1"/spec.dump + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 0 ] + [[ ${lines[6]} == *"containerd"* ]] +} + +@test "Run checkpointctl inspect with tar file with valid config.dump and valid spec.dump (CRI-O) and checkpoint directory" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump.cri-o "$TEST_TMP_DIR1"/spec.dump + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 0 ] + [[ ${lines[6]} == *"CRI-O"* ]] +} + +@test "Run checkpointctl inspect with tar file and rootfs-diff tar file" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + echo 1 > "$TEST_TMP_DIR1"/test.pid + tar -cf "$TEST_TMP_DIR1"/rootfs-diff.tar -C "$TEST_TMP_DIR1" test.pid + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar + [ "$status" -eq 0 ] + [[ ${lines[8]} == *"Root Fs Diff Size"* ]] +} + +@test "Run checkpointctl inspect with multiple tar files" { + cp data/config.dump "$TEST_TMP_DIR1" + cp data/spec.dump "$TEST_TMP_DIR1" + mkdir "$TEST_TMP_DIR1"/checkpoint + ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test1.tar . && tar cf "$TEST_TMP_DIR2"/test2.tar . ) + checkpointctl inspect "$TEST_TMP_DIR2"/*.tar + [ "$status" -eq 0 ] + [[ ${lines[6]} == *"Podman"* ]] + [[ ${lines[14]} == *"Podman"* ]] +} From 4c9386e661713d2455e717f1c41ae51626fa6d3c Mon Sep 17 00:00:00 2001 From: Kouame Behouba Manasse Date: Wed, 28 Jun 2023 13:21:46 +0300 Subject: [PATCH 4/4] reduce code duplication between table and tree rendering functions This commit reduces code duplication between the table and the tree rendering logics. The duplicated logic for retrieving checkpoint information has been extracted into a reusable function: `getCheckpointInfo`. Signed-off-by: Kouame Behouba Manasse --- container.go | 90 ++++++++++++++++++++++++----------------- test/checkpointctl.bats | 3 +- tree.go | 21 ++-------- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/container.go b/container.go index 86b3918f..6da20069 100644 --- a/container.go +++ b/container.go @@ -34,6 +34,13 @@ type containerInfo struct { Engine string } +type checkpointInfo struct { + containerInfo *containerInfo + specDump *spec.Spec + configDump *metadata.ContainerConfig + archiveSizes *archiveSizes +} + func getPodmanInfo(containerConfig *metadata.ContainerConfig, _ *spec.Spec) *containerInfo { return &containerInfo{ Name: containerConfig.Name, @@ -64,6 +71,32 @@ func getCRIOInfo(_ *metadata.ContainerConfig, specDump *spec.Spec) (*containerIn }, nil } +func getCheckpointInfo(task task) (*checkpointInfo, error) { + info := &checkpointInfo{} + var err error + + info.configDump, _, err = metadata.ReadContainerCheckpointConfigDump(task.outputDir) + if err != nil { + return nil, err + } + info.specDump, _, err = metadata.ReadContainerCheckpointSpecDump(task.outputDir) + if err != nil { + return nil, err + } + + info.containerInfo, err = getContainerInfo(task.outputDir, info.specDump, info.configDump) + if err != nil { + return nil, err + } + + info.archiveSizes, err = getArchiveSizes(task.checkpointFilePath) + if err != nil { + return nil, err + } + + return info, nil +} + func showContainerCheckpoints(tasks []task) error { table := tablewriter.NewWriter(os.Stdout) header := []string{ @@ -79,67 +112,50 @@ func showContainerCheckpoints(tasks []task) error { header = append(header, "IP", "MAC", "CHKPT Size", "Root Fs Diff Size") } - var specDump *spec.Spec - var ci *containerInfo - for _, task := range tasks { - containerConfig, _, err := metadata.ReadContainerCheckpointConfigDump(task.outputDir) - if err != nil { - return err - } - specDump, _, err = metadata.ReadContainerCheckpointSpecDump(task.outputDir) - if err != nil { - return err - } - - ci, err = getContainerInfo(task.outputDir, specDump, containerConfig) - if err != nil { - return err - } - - archiveSizes, err := getArchiveSizes(task.checkpointFilePath) + info, err := getCheckpointInfo(task) if err != nil { return err } var row []string - row = append(row, ci.Name) - row = append(row, containerConfig.RootfsImageName) - if len(containerConfig.ID) > 12 { - row = append(row, containerConfig.ID[:12]) + row = append(row, info.containerInfo.Name) + row = append(row, info.configDump.RootfsImageName) + if len(info.configDump.ID) > 12 { + row = append(row, info.configDump.ID[:12]) } else { - row = append(row, containerConfig.ID) + row = append(row, info.configDump.ID) } - row = append(row, containerConfig.OCIRuntime) - row = append(row, ci.Created) - row = append(row, ci.Engine) + row = append(row, info.configDump.OCIRuntime) + row = append(row, info.containerInfo.Created) + row = append(row, info.containerInfo.Engine) if len(tasks) == 1 { fmt.Printf("\nDisplaying container checkpoint data from %s\n\n", task.checkpointFilePath) - if ci.IP != "" { + if info.containerInfo.IP != "" { header = append(header, "IP") - row = append(row, ci.IP) + row = append(row, info.containerInfo.IP) } - if ci.MAC != "" { + if info.containerInfo.MAC != "" { header = append(header, "MAC") - row = append(row, ci.MAC) + row = append(row, info.containerInfo.MAC) } header = append(header, "CHKPT Size") - row = append(row, metadata.ByteToString(archiveSizes.checkpointSize)) + row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize)) // Display root fs diff size if available - if archiveSizes.rootFsDiffTarSize != 0 { + if info.archiveSizes.rootFsDiffTarSize != 0 { header = append(header, "Root Fs Diff Size") - row = append(row, metadata.ByteToString(archiveSizes.rootFsDiffTarSize)) + row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize)) } } else { - row = append(row, ci.IP) - row = append(row, ci.MAC) - row = append(row, metadata.ByteToString(archiveSizes.checkpointSize)) - row = append(row, metadata.ByteToString(archiveSizes.rootFsDiffTarSize)) + row = append(row, info.containerInfo.IP) + row = append(row, info.containerInfo.MAC) + row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize)) + row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize)) } table.Append(row) diff --git a/test/checkpointctl.bats b/test/checkpointctl.bats index 4564baeb..381e1379 100644 --- a/test/checkpointctl.bats +++ b/test/checkpointctl.bats @@ -98,7 +98,6 @@ function teardown() { @test "Run checkpointctl show with tar file from containerd with valid config.dump and valid spec.dump and checkpoint directory" { cp data/config.dump "$TEST_TMP_DIR1" mkdir "$TEST_TMP_DIR1"/checkpoint - echo "{}" > "$TEST_TMP_DIR1"/status echo "{}" > "$TEST_TMP_DIR1"/spec.dump ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) checkpointctl show "$TEST_TMP_DIR2"/test.tar @@ -184,7 +183,7 @@ function teardown() { touch "$TEST_TMP_DIR1"/config.dump mkdir "$TEST_TMP_DIR1"/checkpoint ( cd "$TEST_TMP_DIR1" && tar cf "$TEST_TMP_DIR2"/test.tar . ) - checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --format=xml + checkpointctl inspect "$TEST_TMP_DIR2"/test.tar --format=invalid [ "$status" -eq 1 ] [[ ${lines[0]} == *"invalid output format"* ]] } diff --git a/tree.go b/tree.go index 1a74190a..4b064a4b 100644 --- a/tree.go +++ b/tree.go @@ -13,30 +13,15 @@ import ( func renderTreeView(tasks []task) error { for _, task := range tasks { - containerConfig, _, err := metadata.ReadContainerCheckpointConfigDump(task.outputDir) + info, err := getCheckpointInfo(task) if err != nil { return err } - specDump, _, err := metadata.ReadContainerCheckpointSpecDump(task.outputDir) - if err != nil { - return err - } - - ci, err := getContainerInfo(task.outputDir, specDump, containerConfig) - if err != nil { - return err - } - - archiveSizes, err := getArchiveSizes(task.checkpointFilePath) - if err != nil { - return err - } - - tree := buildTree(ci, containerConfig, archiveSizes) + tree := buildTree(info.containerInfo, info.configDump, info.archiveSizes) if mounts { - addMountsToTree(tree, specDump) + addMountsToTree(tree, info.specDump) } if stats {