Merge pull request #28712 from elezar/info-cdi-devices

info: report configured and discovered CDI devices
This commit is contained in:
Paul Holzinger 2026-06-10 14:34:35 +02:00 committed by GitHub
commit cefaa0ee44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 131 additions and 56 deletions

View file

@ -11,6 +11,7 @@ podman\-info - Display Podman related system information
## DESCRIPTION
Displays information pertinent to the host, current storage stats, configured container registries, and build of podman.
Host information includes configured CDI spec directories and resolved CDI devices when present.
## OPTIONS

View file

@ -33,6 +33,8 @@ type HostInfo struct {
CgroupManager string `json:"cgroupManager"`
CgroupsVersion string `json:"cgroupVersion"`
CgroupControllers []string `json:"cgroupControllers"`
CDISpecDirs []string `json:"cdiSpecDirs"`
DiscoveredDevices []DeviceInfo `json:"discoveredDevices,omitempty"`
Conmon *ConmonInfo `json:"conmon"`
CPUs int `json:"cpus"`
CPUUtilization *CPUUsage `json:"cpuUtilization"`
@ -72,6 +74,12 @@ type HostInfo struct {
EmulatedArchitectures []string `json:"emulatedArchitectures,omitempty"`
}
// DeviceInfo describes a device discovered by a device source.
type DeviceInfo struct {
Source string `json:"source"`
ID string `json:"id"`
}
// RemoteSocket describes information about the API socket
type RemoteSocket struct {
Path string `json:"path,omitempty"`

View file

@ -23,6 +23,7 @@ import (
"go.podman.io/podman/v6/libpod/define"
"go.podman.io/podman/v6/libpod/linkmode"
"go.podman.io/storage/pkg/system"
"tags.cncf.io/container-device-interface/pkg/cdi"
)
// Info returns the store and host information
@ -130,6 +131,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) {
SwapFree: mi.SwapFree,
SwapTotal: mi.SwapTotal,
}
info.CDISpecDirs, info.DiscoveredDevices = r.cdiInfo()
platform := parse.DefaultPlatform()
pArr := strings.Split(platform, "/")
if len(pArr) == 3 {
@ -177,6 +179,33 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) {
return &info, nil
}
func (r *Runtime) cdiInfo() ([]string, []define.DeviceInfo) {
registry, err := cdi.NewCache(
cdi.WithSpecDirs(r.config.Engine.CdiSpecDirs.Get()...),
cdi.WithAutoRefresh(false),
)
if err != nil {
logrus.Debugf("Creating CDI registry for info: %v", err)
return r.config.Engine.CdiSpecDirs.Get(), nil
}
if err := registry.Refresh(); err != nil {
logrus.Debugf("The following error was triggered when refreshing the CDI registry for info: %v", err)
}
return registry.GetSpecDirectories(), cdiDeviceInfo(registry.ListDevices())
}
func cdiDeviceInfo(deviceNames []string) []define.DeviceInfo {
devices := make([]define.DeviceInfo, 0, len(deviceNames))
for _, device := range deviceNames {
devices = append(devices, define.DeviceInfo{
Source: "cdi",
ID: device,
})
}
return devices
}
func (r *Runtime) getContainerStoreInfo() (define.ContainerStore, error) {
var paused, running, stopped int
cs := define.ContainerStore{}

View file

@ -62,6 +62,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) {
CPUSet: sysInfo.Cpuset,
CPUShares: sysInfo.CPUShares,
CgroupDriver: configInfo.Engine.CgroupManager,
CDISpecDirs: infoData.Host.CDISpecDirs,
ContainerdCommit: dockerSystem.Commit{},
Containers: infoData.Store.ContainerStore.Number,
ContainersPaused: stateInfo[define.ContainerStatePaused],
@ -70,6 +71,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) {
Debug: log.IsLevelEnabled(log.DebugLevel),
DefaultAddressPools: getDefaultAddressPools(configInfo),
DefaultRuntime: configInfo.Engine.OCIRuntime,
DiscoveredDevices: getDiscoveredDevices(infoData.Host.DiscoveredDevices),
DockerRootDir: infoData.Store.GraphRoot,
Driver: infoData.Store.GraphDriverName,
DriverStatus: getGraphStatus(infoData.Store.GraphStatus),
@ -132,6 +134,17 @@ func GetInfo(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusOK, info)
}
func getDiscoveredDevices(discoveredDevices []define.DeviceInfo) []dockerSystem.DeviceInfo {
devices := make([]dockerSystem.DeviceInfo, 0, len(discoveredDevices))
for _, device := range discoveredDevices {
devices = append(devices, dockerSystem.DeviceInfo{
Source: device.Source,
ID: device.ID,
})
}
return devices
}
func getServiceConfig(runtime *libpod.Runtime) *registry.ServiceConfig {
var indexConfs map[string]*registry.IndexInfo

View file

@ -47,22 +47,16 @@ var _ = Describe("Podman Info", func() {
})
It("podman info --format GO template", func() {
session := podmanTest.Podman([]string{"info", "--format", "{{.Store.GraphRoot}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
podmanTest.PodmanExitCleanly("info", "--format", "{{.Store.GraphRoot}}")
})
It("podman info --format GO template", func() {
session := podmanTest.Podman([]string{"info", "--format", "{{.Registries}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Registries}}")
Expect(session.OutputToString()).To(ContainSubstring("registry"))
})
It("podman info --format GO template plugins", func() {
session := podmanTest.Podman([]string{"info", "--format", "{{.Plugins}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Plugins}}")
Expect(session.OutputToString()).To(ContainSubstring("local"))
Expect(session.OutputToString()).To(ContainSubstring("journald"))
Expect(session.OutputToString()).To(ContainSubstring("bridge"))
@ -72,10 +66,7 @@ var _ = Describe("Podman Info", func() {
SkipIfNotRootless("test of rootless_storage_path is only meaningful as rootless")
SkipIfRemote("Only tests storage on local client")
configPath := filepath.Join(podmanTest.TempDir, ".config", "containers", "storage.conf")
os.Setenv("CONTAINERS_STORAGE_CONF", configPath)
defer func() {
os.Unsetenv("CONTAINERS_STORAGE_CONF")
}()
GinkgoT().Setenv("CONTAINERS_STORAGE_CONF", configPath)
err := os.RemoveAll(filepath.Dir(configPath))
Expect(err).ToNot(HaveOccurred())
@ -104,9 +95,7 @@ var _ = Describe("Podman Info", func() {
})
It("check RemoteSocket ", func() {
session := podmanTest.Podman([]string{"info", "--format", "{{.Host.RemoteSocket.Path}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.RemoteSocket.Path}}")
switch podmanTest.RemoteSocketScheme {
case "unix":
Expect(session.OutputToString()).To(MatchRegexp("/run/.*podman.*sock"))
@ -114,9 +103,7 @@ var _ = Describe("Podman Info", func() {
Expect(session.OutputToString()).To(MatchRegexp("tcp://127.0.0.1:.*"))
}
session = podmanTest.Podman([]string{"info", "--format", "{{.Host.ServiceIsRemote}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
session = podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.ServiceIsRemote}}")
if podmanTest.RemoteTest {
Expect(session.OutputToString()).To(Equal("true"))
} else {
@ -124,18 +111,14 @@ var _ = Describe("Podman Info", func() {
}
if IsRemote() {
session = podmanTest.Podman([]string{"info", "--format", "{{.Host.RemoteSocket.Exists}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
session = podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.RemoteSocket.Exists}}")
Expect(session.OutputToString()).To(Equal("true"))
}
})
It("Podman info must contain cgroupControllers with RelevantControllers", func() {
SkipIfRootless("Hard to tell which controllers are going to be enabled for rootless")
session := podmanTest.Podman([]string{"info", "--format", "{{.Host.CgroupControllers}}"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.CgroupControllers}}")
Expect(session.OutputToString()).To(ContainSubstring("memory"))
Expect(session.OutputToString()).To(ContainSubstring("pids"))
})
@ -149,28 +132,22 @@ var _ = Describe("Podman Info", func() {
}
Fail("CIRRUS_CI is set, but CI_DESIRED_RUNTIME is not! See #14912")
}
session := podmanTest.Podman([]string{"info", "--format", "{{.Host.OCIRuntime.Name}}"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.OCIRuntime.Name}}")
Expect(session.OutputToString()).To(Equal(want))
})
It("Podman info: check desired network backend", func() {
session := podmanTest.Podman([]string{"info", "--format", "{{.Host.NetworkBackend}}"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.NetworkBackend}}")
Expect(session.OutputToString()).To(Equal("netavark"))
session = podmanTest.Podman([]string{"info", "--format", "{{.Host.NetworkBackendInfo.Backend}}"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session = podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.NetworkBackendInfo.Backend}}")
Expect(session.OutputToString()).To(Equal("netavark"))
})
It("Podman info: check default network from configuration", func() {
configPath := filepath.Join(podmanTest.TempDir, "containers.conf")
os.Setenv("CONTAINERS_CONF_OVERRIDE", configPath)
GinkgoT().Setenv("CONTAINERS_CONF_OVERRIDE", configPath)
customNetName := "my-custom-test-network"
configContent := fmt.Sprintf("[network]\ndefault_network=%q\n", customNetName)
@ -179,12 +156,71 @@ var _ = Describe("Podman Info", func() {
Expect(err).ToNot(HaveOccurred())
podmanTest.RestartRemoteService()
session := podmanTest.Podman([]string{"info", "--format", "{{.Host.NetworkBackendInfo.DefaultNetwork}}"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.NetworkBackendInfo.DefaultNetwork}}")
Expect(session.OutputToString()).To(Equal(customNetName))
})
It("Podman info: check CDI spec dirs and devices from configuration", func() {
cdiDir := filepath.Join(podmanTest.TempDir, "cdi")
Expect(os.MkdirAll(cdiDir, os.ModePerm)).To(Succeed())
cdiSpec := []byte(`{
"cdiVersion": "0.3.0",
"kind": "vendor.com/device",
"devices": [
{
"name": "myKmsg",
"containerEdits": {
"env": ["PODMAN_CDI_INFO_TEST=1"]
}
}
]
}`)
Expect(os.WriteFile(filepath.Join(cdiDir, "device.json"), cdiSpec, os.ModePerm)).To(Succeed())
configPath := filepath.Join(podmanTest.TempDir, "containers.conf")
configContent := fmt.Sprintf("[engine]\ncdi_spec_dirs = [%q]\n", cdiDir)
err := os.WriteFile(configPath, []byte(configContent), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
GinkgoT().Setenv("CONTAINERS_CONF_OVERRIDE", configPath)
podmanTest.RestartRemoteService()
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.CDISpecDirs}} {{.Host.DiscoveredDevices}}")
Expect(session.OutputToString()).To(ContainSubstring(cdiDir))
Expect(session.OutputToString()).To(ContainSubstring("vendor.com/device=myKmsg"))
})
It("Podman info: check CDI spec dirs and devices with --cdi-spec-dir", func() {
SkipIfRemote("The --cdi-spec-dir flag is not supported for remote")
cdiDir := filepath.Join(podmanTest.TempDir, "cdi")
Expect(os.MkdirAll(cdiDir, os.ModePerm)).To(Succeed())
cdiSpec := []byte(`{
"cdiVersion": "0.3.0",
"kind": "vendor.com/device",
"devices": [
{
"name": "myKmsg",
"containerEdits": {
"env": ["PODMAN_CDI_INFO_TEST=1"]
}
}
]
}`)
Expect(os.WriteFile(filepath.Join(cdiDir, "device.json"), cdiSpec, os.ModePerm)).To(Succeed())
session := podmanTest.PodmanExitCleanly("--cdi-spec-dir", cdiDir, "info", "--format", "{{.Host.CDISpecDirs}} {{.Host.DiscoveredDevices}}")
Expect(session.OutputToString()).To(ContainSubstring(cdiDir))
Expect(session.OutputToString()).To(ContainSubstring("vendor.com/device=myKmsg"))
session = podmanTest.PodmanExitCleanly("--cdi-spec-dir", cdiDir, "system", "info", "--format", "{{.Host.CDISpecDirs}} {{.Host.DiscoveredDevices}}")
Expect(session.OutputToString()).To(ContainSubstring(cdiDir))
Expect(session.OutputToString()).To(ContainSubstring("vendor.com/device=myKmsg"))
})
It("Podman info: check desired storage driver", func() {
// defined in .cirrus.yml
want := os.Getenv("CI_DESIRED_STORAGE")
@ -194,9 +230,7 @@ var _ = Describe("Podman Info", func() {
}
Fail("CIRRUS_CI is set, but CI_DESIRED_STORAGE is not! See #20161")
}
session := podmanTest.Podman([]string{"info", "--format", "{{.Store.GraphDriverName}}"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Store.GraphDriverName}}")
Expect(session.OutputToString()).To(Equal(want), ".Store.GraphDriverName from podman info")
// Confirm desired setting of composefs
@ -205,9 +239,7 @@ var _ = Describe("Podman Info", func() {
if os.Getenv("CI_DESIRED_COMPOSEFS") != "" {
expect = "true"
}
session = podmanTest.Podman([]string{"info", "--format", `{{index .Store.GraphOptions "overlay.use_composefs"}}`})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitCleanly())
session = podmanTest.PodmanExitCleanly("info", "--format", `{{index .Store.GraphOptions "overlay.use_composefs"}}`)
Expect(session.OutputToString()).To(Equal(expect), ".Store.GraphOptions -> overlay.use_composefs")
}
})
@ -216,19 +248,13 @@ var _ = Describe("Podman Info", func() {
// This should not run on architectures and OSes that use the file locks backend.
// Which, for now, is Linux + RISCV and FreeBSD, neither of which are in CI - so
// no skips.
info1 := podmanTest.Podman([]string{"info", "--format", "{{ .Host.FreeLocks }}"})
info1.WaitWithDefaultTimeout()
Expect(info1).To(ExitCleanly())
info1 := podmanTest.PodmanExitCleanly("info", "--format", "{{ .Host.FreeLocks }}")
free1, err := strconv.Atoi(info1.OutputToString())
Expect(err).To(Not(HaveOccurred()))
ctr := podmanTest.Podman([]string{"create", ALPINE, "top"})
ctr.WaitWithDefaultTimeout()
Expect(ctr).To(ExitCleanly())
podmanTest.PodmanExitCleanly("create", ALPINE, "top")
info2 := podmanTest.Podman([]string{"info", "--format", "{{ .Host.FreeLocks }}"})
info2.WaitWithDefaultTimeout()
Expect(info2).To(ExitCleanly())
info2 := podmanTest.PodmanExitCleanly("info", "--format", "{{ .Host.FreeLocks }}")
free2, err := strconv.Atoi(info2.OutputToString())
Expect(err).To(Not(HaveOccurred()))
@ -251,9 +277,7 @@ var _ = Describe("Podman Info", func() {
})
It("Podman info: check client information", func() {
info := podmanTest.Podman([]string{"info", "--format", "{{ .Client }}"})
info.WaitWithDefaultTimeout()
Expect(info).To(ExitCleanly())
info := podmanTest.PodmanExitCleanly("info", "--format", "{{ .Client }}")
// client info should only appear when using the remote client
if IsRemote() {
Expect(info.OutputToString()).ToNot(Equal("<nil>"))