-
Notifications
You must be signed in to change notification settings - Fork 17
Display network information of checkpoints created with Podman #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -19,7 +19,7 @@ import ( | |||||||||||||||||
"github.com/checkpoint-restore/go-criu/v7/crit" | ||||||||||||||||||
"github.com/containers/storage/pkg/archive" | ||||||||||||||||||
"github.com/olekukonko/tablewriter" | ||||||||||||||||||
spec "github.com/opencontainers/runtime-spec/specs-go" | ||||||||||||||||||
specs "github.com/opencontainers/runtime-spec/specs-go" | ||||||||||||||||||
) | ||||||||||||||||||
|
||||||||||||||||||
var pageSize = os.Getpagesize() | ||||||||||||||||||
|
@@ -41,20 +41,42 @@ type containerInfo struct { | |||||||||||||||||
|
||||||||||||||||||
type checkpointInfo struct { | ||||||||||||||||||
containerInfo *containerInfo | ||||||||||||||||||
specDump *spec.Spec | ||||||||||||||||||
specDump *specs.Spec | ||||||||||||||||||
configDump *metadata.ContainerConfig | ||||||||||||||||||
archiveSizes *archiveSizes | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func getPodmanInfo(containerConfig *metadata.ContainerConfig, _ *spec.Spec) *containerInfo { | ||||||||||||||||||
return &containerInfo{ | ||||||||||||||||||
func getPodmanInfo(containerConfig *metadata.ContainerConfig, specDump *specs.Spec, task Task) *containerInfo { | ||||||||||||||||||
info := &containerInfo{ | ||||||||||||||||||
Name: containerConfig.Name, | ||||||||||||||||||
Created: containerConfig.CreatedTime.Format(time.RFC3339), | ||||||||||||||||||
Engine: "Podman", | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Try to get network information from network.status file | ||||||||||||||||||
if specDump.Annotations["io.container.manager"] == "libpod" { | ||||||||||||||||||
// Create temp dir for network status file | ||||||||||||||||||
tmpDir, err := os.MkdirTemp("", "network-status") | ||||||||||||||||||
if err == nil { | ||||||||||||||||||
defer os.RemoveAll(tmpDir) | ||||||||||||||||||
|
||||||||||||||||||
// Extract network.status file | ||||||||||||||||||
err = UntarFiles(task.CheckpointFilePath, tmpDir, []string{metadata.NetworkStatusFile}) | ||||||||||||||||||
if err == nil { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||||||||||||||||||
networkStatusFile := filepath.Join(tmpDir, metadata.NetworkStatusFile) | ||||||||||||||||||
ip, mac, err := getPodmanNetworkInfo(networkStatusFile) | ||||||||||||||||||
if err == nil { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||||||||||||||||||
info.IP = ip | ||||||||||||||||||
info.MAC = mac | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return info | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *spec.Spec) *containerInfo { | ||||||||||||||||||
func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *specs.Spec) *containerInfo { | ||||||||||||||||||
return &containerInfo{ | ||||||||||||||||||
Name: specDump.Annotations["io.kubernetes.cri.container-name"], | ||||||||||||||||||
Created: containerConfig.CreatedTime.Format(time.RFC3339), | ||||||||||||||||||
|
@@ -64,7 +86,7 @@ func getContainerdInfo(containerConfig *metadata.ContainerConfig, specDump *spec | |||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func getCRIOInfo(_ *metadata.ContainerConfig, specDump *spec.Spec) (*containerInfo, error) { | ||||||||||||||||||
func getCRIOInfo(_ *metadata.ContainerConfig, specDump *specs.Spec) (*containerInfo, error) { | ||||||||||||||||||
cm := containerMetadata{} | ||||||||||||||||||
if err := json.Unmarshal([]byte(specDump.Annotations["io.kubernetes.cri-o.Metadata"]), &cm); err != nil { | ||||||||||||||||||
return nil, fmt.Errorf("failed to read io.kubernetes.cri-o.Metadata: %w", err) | ||||||||||||||||||
|
@@ -86,14 +108,23 @@ func getCheckpointInfo(task Task) (*checkpointInfo, error) { | |||||||||||||||||
|
||||||||||||||||||
info.configDump, _, err = metadata.ReadContainerCheckpointConfigDump(task.OutputDir) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return nil, err | ||||||||||||||||||
if strings.Contains(err.Error(), "unexpected end of JSON input") { | ||||||||||||||||||
return nil, fmt.Errorf("config.dump: unexpected end of JSON input") | ||||||||||||||||||
} | ||||||||||||||||||
return nil, fmt.Errorf("config.dump: %w", err) | ||||||||||||||||||
Comment on lines
+111
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't think we need the special case since we are wrapping with context. |
||||||||||||||||||
} | ||||||||||||||||||
info.specDump, _, err = metadata.ReadContainerCheckpointSpecDump(task.OutputDir) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return nil, err | ||||||||||||||||||
if os.IsNotExist(err) { | ||||||||||||||||||
return nil, fmt.Errorf("spec.dump: no such file or directory") | ||||||||||||||||||
} | ||||||||||||||||||
if strings.Contains(err.Error(), "unexpected end of JSON input") { | ||||||||||||||||||
return nil, fmt.Errorf("spec.dump: unexpected end of JSON input") | ||||||||||||||||||
} | ||||||||||||||||||
return nil, fmt.Errorf("spec.dump: %w", err) | ||||||||||||||||||
Comment on lines
+118
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't think we need the special case since we are wrapping with context. |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump) | ||||||||||||||||||
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump, task) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return nil, err | ||||||||||||||||||
} | ||||||||||||||||||
|
@@ -115,18 +146,25 @@ func ShowContainerCheckpoints(tasks []Task) error { | |||||||||||||||||
"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") | ||||||||||||||||||
"IP", | ||||||||||||||||||
"MAC", | ||||||||||||||||||
"CHKPT Size", | ||||||||||||||||||
"Root FS Diff Size", | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Would it make sense to drop the "size" suffix, since the field ought to make it clear that we are talking about the size? |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
for _, task := range tasks { | ||||||||||||||||||
info, err := getCheckpointInfo(task) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
if strings.Contains(err.Error(), "Error: ") { | ||||||||||||||||||
return fmt.Errorf("%s", strings.TrimPrefix(err.Error(), "Error: ")) | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+158
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems unnecessary |
||||||||||||||||||
return err | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if len(tasks) == 1 { | ||||||||||||||||||
fmt.Printf("Displaying container checkpoint data from %s\n", task.CheckpointFilePath) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
var row []string | ||||||||||||||||||
row = append(row, info.containerInfo.Name) | ||||||||||||||||||
row = append(row, info.configDump.RootfsImageName) | ||||||||||||||||||
|
@@ -135,37 +173,13 @@ func ShowContainerCheckpoints(tasks []Task) error { | |||||||||||||||||
} else { | ||||||||||||||||||
row = append(row, info.configDump.ID) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
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 info.containerInfo.IP != "" { | ||||||||||||||||||
header = append(header, "IP") | ||||||||||||||||||
row = append(row, info.containerInfo.IP) | ||||||||||||||||||
} | ||||||||||||||||||
if info.containerInfo.MAC != "" { | ||||||||||||||||||
header = append(header, "MAC") | ||||||||||||||||||
row = append(row, info.containerInfo.MAC) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
header = append(header, "CHKPT Size") | ||||||||||||||||||
row = append(row, metadata.ByteToString(info.archiveSizes.checkpointSize)) | ||||||||||||||||||
|
||||||||||||||||||
// Display root fs diff size if available | ||||||||||||||||||
if info.archiveSizes.rootFsDiffTarSize != 0 { | ||||||||||||||||||
header = append(header, "Root Fs Diff Size") | ||||||||||||||||||
row = append(row, metadata.ByteToString(info.archiveSizes.rootFsDiffTarSize)) | ||||||||||||||||||
} | ||||||||||||||||||
} else { | ||||||||||||||||||
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)) | ||||||||||||||||||
} | ||||||||||||||||||
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) | ||||||||||||||||||
} | ||||||||||||||||||
|
@@ -178,11 +192,11 @@ func ShowContainerCheckpoints(tasks []Task) error { | |||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func getContainerInfo(specDump *spec.Spec, containerConfig *metadata.ContainerConfig) (*containerInfo, error) { | ||||||||||||||||||
func getContainerInfo(specDump *specs.Spec, containerConfig *metadata.ContainerConfig, task Task) (*containerInfo, error) { | ||||||||||||||||||
var ci *containerInfo | ||||||||||||||||||
switch m := specDump.Annotations["io.container.manager"]; m { | ||||||||||||||||||
case "libpod": | ||||||||||||||||||
ci = getPodmanInfo(containerConfig, specDump) | ||||||||||||||||||
ci = getPodmanInfo(containerConfig, specDump, task) | ||||||||||||||||||
case "cri-o": | ||||||||||||||||||
var err error | ||||||||||||||||||
ci, err = getCRIOInfo(containerConfig, specDump) | ||||||||||||||||||
|
@@ -266,7 +280,7 @@ func UntarFiles(src, dest string, files []string) error { | |||||||||||||||||
} | ||||||||||||||||||
return nil | ||||||||||||||||||
}); err != nil { | ||||||||||||||||||
return fmt.Errorf("unpacking of checkpoint archive failed: %w", err) | ||||||||||||||||||
return err | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return nil | ||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package internal | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
) | ||
|
||
// PodmanNetworkStatus represents the network status structure for Podman | ||
type PodmanNetworkStatus struct { | ||
Podman struct { | ||
Interfaces map[string]struct { | ||
Subnets []struct { | ||
IPNet string `json:"ipnet"` | ||
Gateway string `json:"gateway"` | ||
} `json:"subnets"` | ||
MacAddress string `json:"mac_address"` | ||
} `json:"interfaces"` | ||
} `json:"podman"` | ||
} | ||
|
||
// getPodmanNetworkInfo reads and parses the network.status file from a Podman checkpoint | ||
func getPodmanNetworkInfo(networkStatusFile string) (string, string, error) { | ||
data, err := os.ReadFile(networkStatusFile) | ||
if err != nil { | ||
// Return empty strings if file doesn't exist or can't be read | ||
// This maintains compatibility with containers that don't have network info | ||
return "", "", nil | ||
} | ||
|
||
var status PodmanNetworkStatus | ||
if err := json.Unmarshal(data, &status); err != nil { | ||
return "", "", fmt.Errorf("failed to parse network status: %w", err) | ||
} | ||
|
||
// Get the first interface's information | ||
// Most containers will have a single interface (eth0) | ||
for _, info := range status.Podman.Interfaces { | ||
if len(info.Subnets) > 0 { | ||
return info.Subnets[0].IPNet, info.MacAddress, nil | ||
} | ||
} | ||
|
||
return "", "", nil | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a newline at the end of the file :) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package internal | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
metadata "github.com/checkpoint-restore/checkpointctl/lib" | ||
) | ||
|
||
func TestGetPodmanNetworkInfo(t *testing.T) { | ||
// Test case 1: Valid network status file | ||
networkStatus := `{ | ||
"podman": { | ||
"interfaces": { | ||
"eth0": { | ||
"subnets": [ | ||
{ | ||
"ipnet": "10.88.0.9/16", | ||
"gateway": "10.88.0.1" | ||
} | ||
], | ||
"mac_address": "f2:99:8d:fb:5a:57" | ||
} | ||
} | ||
} | ||
}` | ||
|
||
networkStatusFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile) | ||
if err := os.WriteFile(networkStatusFile, []byte(networkStatus), 0644); err != nil { | ||
t.Fatalf("Failed to write test file: %v", err) | ||
} | ||
|
||
ip, mac, err := getPodmanNetworkInfo(networkStatusFile) | ||
if err != nil { | ||
t.Errorf("getPodmanNetworkInfo failed: %v", err) | ||
} | ||
|
||
expectedIP := "10.88.0.9/16" | ||
expectedMAC := "f2:99:8d:fb:5a:57" | ||
|
||
if ip != expectedIP { | ||
t.Errorf("Expected IP %s, got %s", expectedIP, ip) | ||
} | ||
if mac != expectedMAC { | ||
t.Errorf("Expected MAC %s, got %s", expectedMAC, mac) | ||
} | ||
|
||
// Test case 2: Missing network status file | ||
nonExistentFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile) | ||
ip, mac, err = getPodmanNetworkInfo(nonExistentFile) | ||
if err != nil { | ||
t.Errorf("getPodmanNetworkInfo with missing file should not return error, got: %v", err) | ||
} | ||
if ip != "" || mac != "" { | ||
t.Errorf("Expected empty IP and MAC for missing file, got IP=%s, MAC=%s", ip, mac) | ||
} | ||
|
||
// Test case 3: Invalid JSON | ||
invalidJSONFile := filepath.Join(t.TempDir(), metadata.NetworkStatusFile) | ||
if err := os.WriteFile(invalidJSONFile, []byte("invalid json"), 0644); err != nil { | ||
t.Fatalf("Failed to write test file: %v", err) | ||
} | ||
|
||
ip, mac, err = getPodmanNetworkInfo(invalidJSONFile) | ||
if err == nil { | ||
t.Error("getPodmanNetworkInfo should fail with invalid JSON") | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a newline at the end of the file :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would like to return the error here (possible wrapped with some context)