Skip to content

Commit 5cdf9c8

Browse files
container: Add Podman network information display support
Currently, checkpointctl shows IP address information for checkpoints created with CRI-O but not with Podman. For Podman checkpoints, this information is stored in network.status in JSON format. This commit adds support for extracting and displaying network information from Podman container checkpoints by: - Adding network.status file parsing functionality - Displaying IP and MAC address information in checkpoint show output - Adding test coverage for network information parsing - Maintaining compatibility with existing checkpoint formats - Updated import references from lib to metadata for consistency Testing Results: 1. Test Environment: - Container: nginx (docker.io/library/nginx:latest) - Container ID: 4cf1a79a043a - Runtime: crun 2. Verification: - Successfully extracts network.status from checkpoint - Correctly displays IP address (10.88.0.2/16) - Correctly displays MAC address (c2:10:ff:79:ea:72) - Maintains all existing checkpoint information Fixes: #132 Signed-off-by: deveshgoyal1000 <[email protected]>
1 parent 2eec433 commit 5cdf9c8

File tree

5 files changed

+179
-48
lines changed

5 files changed

+179
-48
lines changed

internal/config_extractor.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ func ExtractConfigDump(checkpointPath string) (*ChkptConfig, error) {
3939
return nil, err
4040
}
4141

42-
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump)
42+
task := Task{
43+
OutputDir: tempDir,
44+
CheckpointFilePath: checkpointPath,
45+
}
46+
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task)
4347
if err != nil {
4448
return nil, err
4549
}

internal/container.go

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import (
1515
"strings"
1616
"time"
1717

18-
metadata "github.com/checkpoint-restore/checkpointctl/lib"
18+
lib "github.com/checkpoint-restore/checkpointctl/lib"
1919
"github.com/checkpoint-restore/go-criu/v7/crit"
2020
"github.com/containers/storage/pkg/archive"
2121
"github.com/olekukonko/tablewriter"
22-
spec "github.com/opencontainers/runtime-spec/specs-go"
22+
specs "github.com/opencontainers/runtime-spec/specs-go"
2323
)
2424

2525
var pageSize = os.Getpagesize()
@@ -41,20 +41,48 @@ type containerInfo struct {
4141

4242
type checkpointInfo struct {
4343
containerInfo *containerInfo
44-
specDump *spec.Spec
45-
configDump *metadata.ContainerConfig
44+
specDump *specs.Spec
45+
configDump *lib.ContainerConfig
4646
archiveSizes *archiveSizes
4747
}
4848

49-
func getPodmanInfo(containerConfig *metadata.ContainerConfig, _ *spec.Spec) *containerInfo {
50-
return &containerInfo{
49+
func getPodmanInfo(containerConfig *lib.ContainerConfig, specDump *specs.Spec, task Task) *containerInfo {
50+
info := &containerInfo{
5151
Name: containerConfig.Name,
5252
Created: containerConfig.CreatedTime.Format(time.RFC3339),
5353
Engine: "Podman",
5454
}
55+
56+
// Try to get network information from network.status file
57+
if specDump.Annotations["io.container.manager"] == "libpod" {
58+
// Create temp dir for network status file
59+
tmpDir, err := os.MkdirTemp("", "network-status")
60+
if err == nil {
61+
defer os.RemoveAll(tmpDir)
62+
63+
// Extract network.status file
64+
fmt.Printf("Extracting network.status from: %s\n", task.CheckpointFilePath)
65+
err = UntarFiles(task.CheckpointFilePath, tmpDir, []string{lib.NetworkStatusFile})
66+
if err != nil {
67+
fmt.Printf("Error extracting network.status: %v\n", err)
68+
} else {
69+
networkStatusFile := filepath.Join(tmpDir, lib.NetworkStatusFile)
70+
ip, mac, err := getPodmanNetworkInfo(networkStatusFile)
71+
if err != nil {
72+
fmt.Printf("Error reading network info: %v\n", err)
73+
} else {
74+
info.IP = ip
75+
info.MAC = mac
76+
fmt.Printf("Found network info - IP: %s, MAC: %s\n", ip, mac)
77+
}
78+
}
79+
}
80+
}
81+
82+
return info
5583
}
5684

57-
func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *spec.Spec) *containerInfo {
85+
func getContainerdInfo(containerConfig *lib.ContainerConfig, specDump *specs.Spec) *containerInfo {
5886
return &containerInfo{
5987
Name: specDump.Annotations["io.kubernetes.cri.container-name"],
6088
Created: containerConfig.CreatedTime.Format(time.RFC3339),
@@ -64,7 +92,7 @@ func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *spec
6492
}
6593
}
6694

67-
func getCRIOInfo(_ *metadata.ContainerConfig, specDump *spec.Spec) (*containerInfo, error) {
95+
func getCRIOInfo(_ *lib.ContainerConfig, specDump *specs.Spec) (*containerInfo, error) {
6896
cm := containerMetadata{}
6997
if err := json.Unmarshal([]byte(specDump.Annotations["io.kubernetes.cri-o.Metadata"]), &cm); err != nil {
7098
return nil, fmt.Errorf("failed to read io.kubernetes.cri-o.Metadata: %w", err)
@@ -84,16 +112,16 @@ func getCheckpointInfo(task Task) (*checkpointInfo, error) {
84112
info := &checkpointInfo{}
85113
var err error
86114

87-
info.configDump, _, err = metadata.ReadContainerCheckpointConfigDump(task.OutputDir)
115+
info.configDump, _, err = lib.ReadContainerCheckpointConfigDump(task.OutputDir)
88116
if err != nil {
89117
return nil, err
90118
}
91-
info.specDump, _, err = metadata.ReadContainerCheckpointSpecDump(task.OutputDir)
119+
info.specDump, _, err = lib.ReadContainerCheckpointSpecDump(task.OutputDir)
92120
if err != nil {
93121
return nil, err
94122
}
95123

96-
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump)
124+
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task)
97125
if err != nil {
98126
return nil, err
99127
}
@@ -116,10 +144,8 @@ func ShowContainerCheckpoints(tasks []Task) error {
116144
"Created",
117145
"Engine",
118146
}
119-
// Set all columns in the table header upfront when displaying more than one checkpoint
120-
if len(tasks) > 1 {
121-
header = append(header, "IP", "MAC", "CHKPT Size", "Root Fs Diff Size")
122-
}
147+
// Always include network columns in header
148+
header = append(header, "IP", "MAC", "CHKPT Size", "Root Fs Diff Size")
123149

124150
for _, task := range tasks {
125151
info, err := getCheckpointInfo(task)
@@ -142,31 +168,14 @@ func ShowContainerCheckpoints(tasks []Task) error {
142168

143169
if len(tasks) == 1 {
144170
fmt.Printf("\nDisplaying container checkpoint data from %s\n\n", task.CheckpointFilePath)
145-
146-
if info.containerInfo.IP != "" {
147-
header = append(header, "IP")
148-
row = append(row, info.containerInfo.IP)
149-
}
150-
if info.containerInfo.MAC != "" {
151-
header = append(header, "MAC")
152-
row = append(row, info.containerInfo.MAC)
153-
}
154-
155-
header = append(header, "CHKPT Size")
156-
row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize))
157-
158-
// Display root fs diff size if available
159-
if info.archiveSizes.rootFsDiffTarSize != 0 {
160-
header = append(header, "Root Fs Diff Size")
161-
row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize))
162-
}
163-
} else {
164-
row = append(row, info.containerInfo.IP)
165-
row = append(row, info.containerInfo.MAC)
166-
row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize))
167-
row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize))
168171
}
169172

173+
// Always include network and size information
174+
row = append(row, info.containerInfo.IP)
175+
row = append(row, info.containerInfo.MAC)
176+
row = append(row, lib.ByteToString(info.archiveSizes.checkpointSize))
177+
row = append(row, lib.ByteToString(info.archiveSizes.rootFsDiffTarSize))
178+
170179
table.Append(row)
171180
}
172181

@@ -178,11 +187,11 @@ func ShowContainerCheckpoints(tasks []Task) error {
178187
return nil
179188
}
180189

181-
func getContainerInfo(specDump *spec.Spec, containerConfig *metadata.ContainerConfig) (*containerInfo, error) {
190+
func getContainerInfo(specDump *specs.Spec, containerConfig *lib.ContainerConfig, task Task) (*containerInfo, error) {
182191
var ci *containerInfo
183192
switch m := specDump.Annotations["io.container.manager"]; m {
184193
case "libpod":
185-
ci = getPodmanInfo(containerConfig, specDump)
194+
ci = getPodmanInfo(containerConfig, specDump, task)
186195
case "cri-o":
187196
var err error
188197
ci, err = getCRIOInfo(containerConfig, specDump)
@@ -213,15 +222,15 @@ func getArchiveSizes(archiveInput string) (*archiveSizes, error) {
213222

214223
err := iterateTarArchive(archiveInput, func(r *tar.Reader, header *tar.Header) error {
215224
if header.FileInfo().Mode().IsRegular() {
216-
if hasPrefix(header.Name, metadata.CheckpointDirectory) {
225+
if hasPrefix(header.Name, lib.CheckpointDirectory) {
217226
// Add the file size to the total checkpoint size
218227
result.checkpointSize += header.Size
219-
if hasPrefix(header.Name, filepath.Join(metadata.CheckpointDirectory, metadata.PagesPrefix)) {
228+
if hasPrefix(header.Name, filepath.Join(lib.CheckpointDirectory, lib.PagesPrefix)) {
220229
result.pagesSize += header.Size
221-
} else if hasPrefix(header.Name, filepath.Join(metadata.CheckpointDirectory, metadata.AmdgpuPagesPrefix)) {
230+
} else if hasPrefix(header.Name, filepath.Join(lib.CheckpointDirectory, lib.AmdgpuPagesPrefix)) {
222231
result.amdgpuPagesSize += header.Size
223232
}
224-
} else if hasPrefix(header.Name, metadata.RootFsDiffTar) {
233+
} else if hasPrefix(header.Name, lib.RootFsDiffTar) {
225234
// Read the size of rootfs diff
226235
result.rootFsDiffTarSize = header.Size
227236
}
@@ -324,7 +333,7 @@ func iterateTarArchive(archiveInput string, callback func(r *tar.Reader, header
324333
}
325334

326335
func getCmdline(checkpointOutputDir string, pid uint32) (cmdline string, err error) {
327-
mr, err := crit.NewMemoryReader(filepath.Join(checkpointOutputDir, metadata.CheckpointDirectory), pid, pageSize)
336+
mr, err := crit.NewMemoryReader(filepath.Join(checkpointOutputDir, lib.CheckpointDirectory), pid, pageSize)
328337
if err != nil {
329338
return
330339
}
@@ -339,7 +348,7 @@ func getCmdline(checkpointOutputDir string, pid uint32) (cmdline string, err err
339348
}
340349

341350
func getPsEnvVars(checkpointOutputDir string, pid uint32) (envVars []string, err error) {
342-
mr, err := crit.NewMemoryReader(filepath.Join(checkpointOutputDir, metadata.CheckpointDirectory), pid, pageSize)
351+
mr, err := crit.NewMemoryReader(filepath.Join(checkpointOutputDir, lib.CheckpointDirectory), pid, pageSize)
343352
if err != nil {
344353
return
345354
}

internal/oci_image_build.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ func (ic *ImageBuilder) getCheckpointAnnotations() (map[string]string, error) {
116116
return nil, err
117117
}
118118

119-
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump)
119+
task := Task{
120+
OutputDir: tempDir,
121+
CheckpointFilePath: ic.checkpointPath,
122+
}
123+
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task)
120124
if err != nil {
121125
return nil, err
122126
}

internal/podman_network.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package internal
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// PodmanNetworkStatus represents the network status structure for Podman
10+
type PodmanNetworkStatus struct {
11+
Podman struct {
12+
Interfaces map[string]struct {
13+
Subnets []struct {
14+
IPNet string `json:"ipnet"`
15+
Gateway string `json:"gateway"`
16+
} `json:"subnets"`
17+
MacAddress string `json:"mac_address"`
18+
} `json:"interfaces"`
19+
} `json:"podman"`
20+
}
21+
22+
// getPodmanNetworkInfo reads and parses the network.status file from a Podman checkpoint
23+
func getPodmanNetworkInfo(networkStatusFile string) (string, string, error) {
24+
data, err := os.ReadFile(networkStatusFile)
25+
if err != nil {
26+
// Return empty strings if file doesn't exist or can't be read
27+
// This maintains compatibility with containers that don't have network info
28+
return "", "", nil
29+
}
30+
31+
var status PodmanNetworkStatus
32+
if err := json.Unmarshal(data, &status); err != nil {
33+
return "", "", fmt.Errorf("failed to parse network status: %w", err)
34+
}
35+
36+
// Get the first interface's information
37+
// Most containers will have a single interface (eth0)
38+
for _, info := range status.Podman.Interfaces {
39+
if len(info.Subnets) > 0 {
40+
return info.Subnets[0].IPNet, info.MacAddress, nil
41+
}
42+
}
43+
44+
return "", "", nil
45+
}

internal/podman_network_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package internal
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
metadata "github.com/checkpoint-restore/checkpointctl/lib"
9+
)
10+
11+
func TestGetPodmanNetworkInfo(t *testing.T) {
12+
// Test case 1: Valid network status file
13+
networkStatus := `{
14+
"podman": {
15+
"interfaces": {
16+
"eth0": {
17+
"subnets": [
18+
{
19+
"ipnet": "10.88.0.9/16",
20+
"gateway": "10.88.0.1"
21+
}
22+
],
23+
"mac_address": "f2:99:8d:fb:5a:57"
24+
}
25+
}
26+
}
27+
}`
28+
29+
networkStatusFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile)
30+
if err := os.WriteFile(networkStatusFile, []byte(networkStatus), 0644); err != nil {
31+
t.Fatalf("Failed to write test file: %v", err)
32+
}
33+
34+
ip, mac, err := getPodmanNetworkInfo(networkStatusFile)
35+
if err != nil {
36+
t.Errorf("getPodmanNetworkInfo failed: %v", err)
37+
}
38+
39+
expectedIP := "10.88.0.9/16"
40+
expectedMAC := "f2:99:8d:fb:5a:57"
41+
42+
if ip != expectedIP {
43+
t.Errorf("Expected IP %s, got %s", expectedIP, ip)
44+
}
45+
if mac != expectedMAC {
46+
t.Errorf("Expected MAC %s, got %s", expectedMAC, mac)
47+
}
48+
49+
// Test case 2: Missing network status file
50+
nonExistentFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile)
51+
ip, mac, err = getPodmanNetworkInfo(nonExistentFile)
52+
if err != nil {
53+
t.Errorf("getPodmanNetworkInfo with missing file should not return error, got: %v", err)
54+
}
55+
if ip != "" || mac != "" {
56+
t.Errorf("Expected empty IP and MAC for missing file, got IP=%s, MAC=%s", ip, mac)
57+
}
58+
59+
// Test case 3: Invalid JSON
60+
invalidJSONFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile)
61+
if err := os.WriteFile(invalidJSONFile, []byte("invalid json"), 0644); err != nil {
62+
t.Fatalf("Failed to write test file: %v", err)
63+
}
64+
65+
ip, mac, err = getPodmanNetworkInfo(invalidJSONFile)
66+
if err == nil {
67+
t.Error("getPodmanNetworkInfo should fail with invalid JSON")
68+
}
69+
}

0 commit comments

Comments
 (0)