Skip to content

Commit

Permalink
Merge pull request #5295 from mheon/advanced_network_inspect
Browse files Browse the repository at this point in the history
Add support for multiple CNI networks in podman inspect
  • Loading branch information
openshift-merge-robot authored Feb 27, 2020
2 parents 2f5d0d8 + f9fc9a7 commit c132a4b
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 67 deletions.
100 changes: 63 additions & 37 deletions libpod/container_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,28 +606,76 @@ 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"`
LinkLocalIPv6Address string `json:"LinkLocalIPv6Address"`
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.
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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 {
Expand Down
118 changes: 93 additions & 25 deletions libpod/networking_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions libpod/networking_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
80 changes: 77 additions & 3 deletions test/e2e/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
})
})

0 comments on commit c132a4b

Please sign in to comment.