From f9fc9a7b7b0e5633cf279273b614a5d3e4c68906 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Thu, 20 Feb 2020 13:34:51 -0500 Subject: Add support for multiple CNI networks in podman inspect When inspecting containers, info on CNI networks added to the container by name (e.g. --net=name1) should be displayed separately from the configuration of the default network, in a separate map called Networks. This patch adds this separation, improving our Docker compatibility and also adding the ability to see if a container has more than one IPv4 and IPv6 address and more than one MAC address. Fixes #4907 Signed-off-by: Matthew Heon --- libpod/container_inspect.go | 100 +++++++++++++++++++++------------ libpod/networking_linux.go | 118 ++++++++++++++++++++++++++++++--------- libpod/networking_unsupported.go | 4 +- test/e2e/network_test.go | 80 +++++++++++++++++++++++++- 4 files changed, 235 insertions(+), 67 deletions(-) diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index 641bc8a91..a543a19c0 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -606,11 +606,45 @@ type InspectContainerState struct { Healthcheck HealthCheckResults `json:"Healthcheck,omitempty"` } +// InspectBasicNetworkConfig holds basic configuration information (e.g. IP +// addresses, MAC address, subnet masks, etc) that are common for all networks +// (both additional and main). +type InspectBasicNetworkConfig struct { + // EndpointID is unused, maintained exclusively for compatibility. + EndpointID string `json:"EndpointID"` + // Gateway is the IP address of the gateway this network will use. + Gateway string `json:"Gateway"` + // IPAddress is the IP address for this network. + IPAddress string `json:"IPAddress"` + // IPPrefixLen is the length of the subnet mask of this network. + IPPrefixLen int `json:"IPPrefixLen"` + // SecondaryIPAddresses is a list of extra IP Addresses that the + // container has been assigned in this network. + SecondaryIPAddresses []string `json:"SecondaryIPAddresses,omitempty"` + // IPv6Gateway is the IPv6 gateway this network will use. + IPv6Gateway string `json:"IPv6Gateway"` + // GlobalIPv6Address is the global-scope IPv6 Address for this network. + GlobalIPv6Address string `json:"GlobalIPv6Address"` + // GlobalIPv6PrefixLen is the length of the subnet mask of this network. + GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"` + // SecondaryIPv6Addresses is a list of extra IPv6 Addresses that the + // container has been assigned in this networ. + SecondaryIPv6Addresses []string `json:"SecondaryIPv6Addresses,omitempty"` + // MacAddress is the MAC address for the interface in this network. + MacAddress string `json:"MacAddress"` + // AdditionalMacAddresses is a set of additional MAC Addresses beyond + // the first. CNI may configure more than one interface for a single + // network, which can cause this. + AdditionalMacAddresses []string `json:"AdditionalMACAddresses,omitempty"` +} + // InspectNetworkSettings holds information about the network settings of the // container. // Many fields are maintained only for compatibility with `docker inspect` and // are unused within Libpod. type InspectNetworkSettings struct { + InspectBasicNetworkConfig + Bridge string `json:"Bridge"` SandboxID string `json:"SandboxID"` HairpinMode bool `json:"HairpinMode"` @@ -618,16 +652,30 @@ type InspectNetworkSettings struct { LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen"` Ports []ocicni.PortMapping `json:"Ports"` SandboxKey string `json:"SandboxKey"` - SecondaryIPAddresses []string `json:"SecondaryIPAddresses"` - SecondaryIPv6Addresses []string `json:"SecondaryIPv6Addresses"` - EndpointID string `json:"EndpointID"` - Gateway string `json:"Gateway"` - GlobalIPv6Address string `json:"GlobalIPv6Address"` - GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"` - IPAddress string `json:"IPAddress"` - IPPrefixLen int `json:"IPPrefixLen"` - IPv6Gateway string `json:"IPv6Gateway"` - MacAddress string `json:"MacAddress"` + // Networks contains information on non-default CNI networks this + // container has joined. + // It is a map of network name to network information. + Networks map[string]*InspectAdditionalNetwork `json:"Networks,omitempty"` +} + +// InspectAdditionalNetwork holds information about non-default CNI networks the +// container has been connected to. +// As with InspectNetworkSettings, many fields are unused and maintained only +// for compatibility with Docker. +type InspectAdditionalNetwork struct { + InspectBasicNetworkConfig + + // Name of the network we're connecting to. + NetworkID string `json:"NetworkID,omitempty"` + // DriverOpts is presently unused and maintained exclusively for + // compatibility. + DriverOpts map[string]string `json:"DriverOpts"` + // IPAMConfig is presently unused and maintained exlusively for + // compabitility. + IPAMConfig map[string]string `json:"IPAMConfig"` + // Links is presently unused and maintained exclusively for + // compatibility. + Links []string `json:"Links"` } // inspectLocked inspects a container for low-level information. @@ -754,27 +802,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) GraphDriver: driverData, Mounts: inspectMounts, Dependencies: c.Dependencies(), - NetworkSettings: &InspectNetworkSettings{ - Bridge: "", // TODO - SandboxID: "", // TODO - is this even relevant? - HairpinMode: false, // TODO - LinkLocalIPv6Address: "", // TODO - do we even support IPv6? - LinkLocalIPv6PrefixLen: 0, // TODO - do we even support IPv6? - - Ports: []ocicni.PortMapping{}, // TODO - maybe worth it to put this in Docker format? - SandboxKey: "", // Network namespace path - SecondaryIPAddresses: nil, // TODO - do we support this? - SecondaryIPv6Addresses: nil, // TODO - do we support this? - EndpointID: "", // TODO - is this even relevant? - Gateway: "", // TODO - GlobalIPv6Address: "", - GlobalIPv6PrefixLen: 0, - IPAddress: "", - IPPrefixLen: 0, - IPv6Gateway: "", - MacAddress: "", // TODO - }, - IsInfra: c.IsInfra(), + IsInfra: c.IsInfra(), } if c.state.ConfigPath != "" { @@ -792,13 +820,11 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) } } - // Copy port mappings into network settings - if config.PortMappings != nil { - data.NetworkSettings.Ports = config.PortMappings + networkConfig, err := c.getContainerNetworkInfo() + if err != nil { + return nil, err } - - // Get information on the container's network namespace (if present) - data = c.getContainerNetworkInfo(data) + data.NetworkSettings = networkConfig inspectConfig, err := c.generateInspectContainerConfig(ctrSpec) if err != nil { diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index fa8593f20..d57b1a8eb 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -12,13 +12,13 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "syscall" "time" cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/errorhandling" "github.com/containers/libpod/pkg/netns" "github.com/containers/libpod/pkg/rootless" @@ -556,37 +556,105 @@ func getContainerNetIO(ctr *Container) (*netlink.LinkStatistics, error) { return netStats, err } -func (c *Container) getContainerNetworkInfo(data *InspectContainerData) *InspectContainerData { - if c.state.NetNS != nil && len(c.state.NetworkStatus) > 0 { - // Report network settings from the first pod network - result := c.state.NetworkStatus[0] - // Go through our IP addresses - for _, ctrIP := range result.IPs { - ipWithMask := ctrIP.Address.String() - splitIP := strings.Split(ipWithMask, "/") - mask, _ := strconv.Atoi(splitIP[1]) - if ctrIP.Version == "4" { - data.NetworkSettings.IPAddress = splitIP[0] - data.NetworkSettings.IPPrefixLen = mask - data.NetworkSettings.Gateway = ctrIP.Gateway.String() - } else { - data.NetworkSettings.GlobalIPv6Address = splitIP[0] - data.NetworkSettings.GlobalIPv6PrefixLen = mask - data.NetworkSettings.IPv6Gateway = ctrIP.Gateway.String() +// Produce an InspectNetworkSettings containing information on the container +// network. +func (c *Container) getContainerNetworkInfo() (*InspectNetworkSettings, error) { + settings := new(InspectNetworkSettings) + settings.Ports = []ocicni.PortMapping{} + if c.config.PortMappings != nil { + // TODO: This may not be safe. + settings.Ports = c.config.PortMappings + } + + // We can't do more if the network is down. + if c.state.NetNS == nil { + return settings, nil + } + + // Set network namespace path + settings.SandboxKey = c.state.NetNS.Path() + + // If this is empty, we're probably slirp4netns + if len(c.state.NetworkStatus) == 0 { + return settings, nil + } + + // If we have CNI networks - handle that here + if len(c.config.Networks) > 0 { + if len(c.config.Networks) != len(c.state.NetworkStatus) { + return nil, errors.Wrapf(define.ErrInternal, "network inspection mismatch: asked to join %d CNI networks but have information on %d networks", len(c.config.Networks), len(c.state.NetworkStatus)) + } + + settings.Networks = make(map[string]*InspectAdditionalNetwork) + + // CNI results should be in the same order as the list of + // networks we pass into CNI. + for index, name := range c.config.Networks { + cniResult := c.state.NetworkStatus[index] + addedNet := new(InspectAdditionalNetwork) + addedNet.NetworkID = name + + basicConfig, err := resultToBasicNetworkConfig(cniResult) + if err != nil { + return nil, err } + addedNet.InspectBasicNetworkConfig = basicConfig + + settings.Networks[name] = addedNet } - // Set network namespace path - data.NetworkSettings.SandboxKey = c.state.NetNS.Path() + return settings, nil + } + + // If not joining networks, we should have at most 1 result + if len(c.state.NetworkStatus) > 1 { + return nil, errors.Wrapf(define.ErrInternal, "should have at most 1 CNI result if not joining networks, instead got %d", len(c.state.NetworkStatus)) + } + + if len(c.state.NetworkStatus) == 1 { + basicConfig, err := resultToBasicNetworkConfig(c.state.NetworkStatus[0]) + if err != nil { + return nil, err + } - // Set MAC address of interface linked with network namespace path - for _, i := range result.Interfaces { - if i.Sandbox == data.NetworkSettings.SandboxKey { - data.NetworkSettings.MacAddress = i.Mac + settings.InspectBasicNetworkConfig = basicConfig + } + + return settings, nil +} + +// resultToBasicNetworkConfig produces an InspectBasicNetworkConfig from a CNI +// result +func resultToBasicNetworkConfig(result *cnitypes.Result) (InspectBasicNetworkConfig, error) { + config := InspectBasicNetworkConfig{} + + for _, ctrIP := range result.IPs { + size, _ := ctrIP.Address.Mask.Size() + switch { + case ctrIP.Version == "4" && config.IPAddress == "": + config.IPAddress = ctrIP.Address.IP.String() + config.IPPrefixLen = size + config.Gateway = ctrIP.Gateway.String() + if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface > 0 { + config.MacAddress = result.Interfaces[*ctrIP.Interface].Mac + } + case ctrIP.Version == "4" && config.IPAddress != "": + config.SecondaryIPAddresses = append(config.SecondaryIPAddresses, ctrIP.Address.String()) + if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface > 0 { + config.AdditionalMacAddresses = append(config.AdditionalMacAddresses, result.Interfaces[*ctrIP.Interface].Mac) } + case ctrIP.Version == "6" && config.IPAddress == "": + config.GlobalIPv6Address = ctrIP.Address.IP.String() + config.GlobalIPv6PrefixLen = size + config.IPv6Gateway = ctrIP.Gateway.String() + case ctrIP.Version == "6" && config.IPAddress != "": + config.SecondaryIPv6Addresses = append(config.SecondaryIPv6Addresses, ctrIP.Address.String()) + default: + return config, errors.Wrapf(define.ErrInternal, "unrecognized IP version %q", ctrIP.Version) } } - return data + + return config, nil } type logrusDebugWriter struct { diff --git a/libpod/networking_unsupported.go b/libpod/networking_unsupported.go index d9b3730aa..7f343cf35 100644 --- a/libpod/networking_unsupported.go +++ b/libpod/networking_unsupported.go @@ -20,6 +20,6 @@ func (r *Runtime) createNetNS(ctr *Container) (err error) { return define.ErrNotImplemented } -func (c *Container) getContainerNetworkInfo(data *InspectContainerData) *InspectContainerData { - return nil +func (c *Container) getContainerNetworkInfo() (*InspectNetworkSettings, error) { + return nil, define.ErrNotImplemented } diff --git a/test/e2e/network_test.go b/test/e2e/network_test.go index 9aed5351a..440d307b5 100644 --- a/test/e2e/network_test.go +++ b/test/e2e/network_test.go @@ -4,13 +4,15 @@ package integration import ( "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + . "github.com/containers/libpod/test/utils" "github.com/containers/storage/pkg/stringid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "io/ioutil" - "os" - "path/filepath" ) func writeConf(conf []byte, confPath string) { @@ -155,4 +157,76 @@ var _ = Describe("Podman network", func() { Expect(session.IsJSONOutputValid()).To(BeTrue()) }) + It("podman inspect container single CNI network", func() { + SkipIfRootless() + netName := "testNetSingleCNI" + network := podmanTest.Podman([]string{"network", "create", "--subnet", "10.50.50.0/24", netName}) + network.WaitWithDefaultTimeout() + Expect(network.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctrName := "testCtr" + container := podmanTest.Podman([]string{"run", "-dt", "--network", netName, "--name", ctrName, ALPINE, "top"}) + container.WaitWithDefaultTimeout() + Expect(container.ExitCode()).To(BeZero()) + + inspect := podmanTest.Podman([]string{"inspect", ctrName}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(BeZero()) + conData := inspect.InspectContainerToJSON() + Expect(len(conData)).To(Equal(1)) + Expect(len(conData[0].NetworkSettings.Networks)).To(Equal(1)) + net, ok := conData[0].NetworkSettings.Networks[netName] + Expect(ok).To(BeTrue()) + Expect(net.NetworkID).To(Equal(netName)) + Expect(net.IPPrefixLen).To(Equal(24)) + Expect(strings.HasPrefix(net.IPAddress, "10.50.50.")).To(BeTrue()) + + // Necessary to ensure the CNI network is removed cleanly + rmAll := podmanTest.Podman([]string{"rm", "-f", ctrName}) + rmAll.WaitWithDefaultTimeout() + Expect(rmAll.ExitCode()).To(BeZero()) + }) + + It("podman inspect container two CNI networks", func() { + SkipIfRootless() + netName1 := "testNetTwoCNI1" + network1 := podmanTest.Podman([]string{"network", "create", "--subnet", "10.50.51.0/25", netName1}) + network1.WaitWithDefaultTimeout() + Expect(network1.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName1) + + netName2 := "testNetTwoCNI2" + network2 := podmanTest.Podman([]string{"network", "create", "--subnet", "10.50.51.128/26", netName2}) + network2.WaitWithDefaultTimeout() + Expect(network2.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName2) + + ctrName := "testCtr" + container := podmanTest.Podman([]string{"run", "-dt", "--network", fmt.Sprintf("%s,%s", netName1, netName2), "--name", ctrName, ALPINE, "top"}) + container.WaitWithDefaultTimeout() + Expect(container.ExitCode()).To(BeZero()) + + inspect := podmanTest.Podman([]string{"inspect", ctrName}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(BeZero()) + conData := inspect.InspectContainerToJSON() + Expect(len(conData)).To(Equal(1)) + Expect(len(conData[0].NetworkSettings.Networks)).To(Equal(2)) + net1, ok := conData[0].NetworkSettings.Networks[netName1] + Expect(ok).To(BeTrue()) + Expect(net1.NetworkID).To(Equal(netName1)) + Expect(net1.IPPrefixLen).To(Equal(25)) + Expect(strings.HasPrefix(net1.IPAddress, "10.50.51.")).To(BeTrue()) + net2, ok := conData[0].NetworkSettings.Networks[netName2] + Expect(ok).To(BeTrue()) + Expect(net2.NetworkID).To(Equal(netName2)) + Expect(net2.IPPrefixLen).To(Equal(26)) + Expect(strings.HasPrefix(net2.IPAddress, "10.50.51.")).To(BeTrue()) + + // Necessary to ensure the CNI network is removed cleanly + rmAll := podmanTest.Podman([]string{"rm", "-f", ctrName}) + rmAll.WaitWithDefaultTimeout() + Expect(rmAll.ExitCode()).To(BeZero()) + }) }) -- cgit v1.2.3-54-g00ecf