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 <matthew.heon@pm.me>
This commit is contained in:
Matthew Heon 2020-02-20 13:34:51 -05:00
parent 3d37dc639d
commit f9fc9a7b7b
4 changed files with 236 additions and 68 deletions

View file

@ -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 {

View file

@ -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))
}
// Set network namespace path
data.NetworkSettings.SandboxKey = c.state.NetNS.Path()
settings.Networks = make(map[string]*InspectAdditionalNetwork)
// 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
// 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
}
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
}
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 {

View file

@ -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
}

View file

@ -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())
})
})