Skip to content

Commit

Permalink
show: extend the output format with a tree view
Browse files Browse the repository at this point in the history
This commit extends the output format of `show` command by adding a
tree view output format. This provides additional output format
options and brings all the benefits associated with the tree view,
including the ability to utilize unlimited vertical space.

Signed-off-by: Kouame Behouba Manasse <[email protected]>
  • Loading branch information
behouba committed Jun 20, 2023
1 parent ba8d41b commit 2ae44f9
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 192 deletions.
7 changes: 7 additions & 0 deletions checkpointctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
var (
name string
version string
format string
stats bool
mounts bool
fullPaths bool
Expand Down Expand Up @@ -84,6 +85,12 @@ func setupShow() *cobra.Command {
false,
"Display all additional information about the checkpoints",
)
flags.StringVar(
&format,
"format",
"table",
"Format the output using one of the following views: table, tree, or JSON (default: \"table\").",
)

err := flags.MarkHidden("print-stats")
if err != nil {
Expand Down
195 changes: 3 additions & 192 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ 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 {
Expand Down Expand Up @@ -68,126 +64,10 @@ 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")
if format == "tree" {
return renderTreeView(tasks)
}

table := tablewriter.NewWriter(os.Stdout)
header := []string{
"Container",
"Image",
"ID",
"Runtime",
"Created",
"Engine",
}
// Set all columns in the table header upfront when displaying more than one checkpoint
if len(tasks) > 1 {
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)
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])
} else {
row = append(row, containerConfig.ID)
}

row = append(row, containerConfig.OCIRuntime)
row = append(row, ci.Created)
row = append(row, ci.Engine)

if len(tasks) == 1 {
fmt.Printf("\nDisplaying container checkpoint data from %s\n\n", task.checkpointFilePath)

if ci.IP != "" {
header = append(header, "IP")
row = append(row, ci.IP)
}
if ci.MAC != "" {
header = append(header, "MAC")
row = append(row, ci.MAC)
}

header = append(header, "CHKPT Size")
row = append(row, metadata.ByteToString(archiveSizes.checkpointSize))

// Display root fs diff size if available
if archiveSizes.rootFsDiffTarSize != 0 {
header = append(header, "Root Fs Diff Size")
row = append(row, metadata.ByteToString(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))
}

table.Append(row)
}

table.SetHeader(header)
table.SetAutoMergeCells(false)
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
return renderTableView(tasks)
}

func getContainerInfo(checkpointDir string, specDump *spec.Spec, containerConfig *metadata.ContainerConfig) (*containerInfo, error) {
Expand All @@ -212,75 +92,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)
}
Expand Down
Loading

0 comments on commit 2ae44f9

Please sign in to comment.