diff --git a/docs/source/markdown/podman-info.1.md b/docs/source/markdown/podman-info.1.md index cb7151d9403..d68d188194e 100644 --- a/docs/source/markdown/podman-info.1.md +++ b/docs/source/markdown/podman-info.1.md @@ -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 diff --git a/libpod/define/info.go b/libpod/define/info.go index 2dacf09c928..84995629d4d 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -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"` diff --git a/libpod/info.go b/libpod/info.go index 5aab80902d6..9c163064788 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -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{} diff --git a/pkg/api/handlers/compat/info.go b/pkg/api/handlers/compat/info.go index 5ef43badd5e..44bf6a1341b 100644 --- a/pkg/api/handlers/compat/info.go +++ b/pkg/api/handlers/compat/info.go @@ -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 diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go index f0f42555089..46358ba5f06 100644 --- a/test/e2e/info_test.go +++ b/test/e2e/info_test.go @@ -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,44 @@ 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") + err := os.MkdirAll(cdiDir, os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + cdiSpec := []byte(`{ + "cdiVersion": "0.3.0", + "kind": "vendor.com/device", + "devices": [ + { + "name": "myKmsg", + "containerEdits": { + "env": ["PODMAN_CDI_INFO_TEST=1"] + } + } + ] +}`) + err = os.WriteFile(filepath.Join(cdiDir, "device.json"), cdiSpec, os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + 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 desired storage driver", func() { // defined in .cirrus.yml want := os.Getenv("CI_DESIRED_STORAGE") @@ -194,9 +203,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 +212,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 +221,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 +250,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("")) diff --git a/test/system/005-info.bats b/test/system/005-info.bats index e56dcfce2c3..7a8a37e3148 100644 --- a/test/system/005-info.bats +++ b/test/system/005-info.bats @@ -62,6 +62,37 @@ store.imageStore.number | 1 done < <(parse_table "$tests") } +@test "podman info - CDI spec dirs and devices" { + skip_if_remote "--cdi-spec-dir flag is not supported for remote" + + cdi_dir=$PODMAN_TMPDIR/cdi + mkdir -p "$cdi_dir" + cat >"$cdi_dir/device.json" <