From a8b2256cc11cfa266d48f17e49a079a8697066d4 Mon Sep 17 00:00:00 2001 From: Vikas Goel Date: Tue, 30 Jan 2024 12:40:39 -0800 Subject: [PATCH] Set interface name to the network_interface name for macvlan and ipvlan networks When interface_name attribute in containers.conf file is set to "device", then set interface names inside containers same as the network_interface names of the respective network. The change applies to macvlan and ipvlan networks only. The interface_name attribute value has no impact on any other types of networks. If the interface name is set in the user request, then that takes precedence. Fixes: #21313 Signed-off-by: Vikas Goel --- libpod/networking_common.go | 30 ++- libpod/runtime_ctr.go | 11 +- test/e2e/container_iface_name_test.go | 280 ++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 test/e2e/container_iface_name_test.go diff --git a/libpod/networking_common.go b/libpod/networking_common.go index dc188b3fa4..b43939c960 100644 --- a/libpod/networking_common.go +++ b/libpod/networking_common.go @@ -353,7 +353,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro // check if network exists and if the input is an ID we get the name // CNI and netavark and the libpod db only uses names so it is important that we only use the name - netName, err = c.runtime.normalizeNetworkName(netName) + netName, _, err = c.runtime.normalizeNetworkName(netName) if err != nil { return err } @@ -467,7 +467,8 @@ func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNe // check if network exists and if the input is an ID we get the name // CNI and netavark and the libpod db only uses names so it is important that we only use the name - netName, err = c.runtime.normalizeNetworkName(netName) + var nicName string + netName, nicName, err = c.runtime.normalizeNetworkName(netName) if err != nil { return err } @@ -481,6 +482,13 @@ func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNe netOpts.Aliases = append(netOpts.Aliases, getExtraNetworkAliases(c)...) + // check whether interface is to be named as the network_interface + // when name left unspecified + if netOpts.InterfaceName == "" { + netOpts.InterfaceName = nicName + } + + // set default interface name if netOpts.InterfaceName == "" { netOpts.InterfaceName = getFreeInterfaceName(networks) if netOpts.InterfaceName == "" { @@ -632,14 +640,24 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, netOpts ty return ctr.NetworkConnect(nameOrID, netName, netOpts) } -// normalizeNetworkName takes a network name, a partial or a full network ID and returns the network name. +// normalizeNetworkName takes a network name, a partial or a full network ID and +// returns: 1) the network name and 2) the network_interface name for macvlan +// and ipvlan drivers if the naming pattern is "device" defined in the +// containers.conf file. Else, "". // If the network is not found an error is returned. -func (r *Runtime) normalizeNetworkName(nameOrID string) (string, error) { +func (r *Runtime) normalizeNetworkName(nameOrID string) (string, string, error) { net, err := r.network.NetworkInspect(nameOrID) if err != nil { - return "", err + return "", "", err } - return net.Name, nil + + netIface := "" + namingPattern := r.config.Containers.InterfaceName + if namingPattern == "device" && (net.Driver == types.MacVLANNetworkDriver || net.Driver == types.IPVLANNetworkDriver) { + netIface = net.NetworkInterface + } + + return net.Name, netIface, nil } // ocicniPortsToNetTypesPorts convert the old port format to the new one diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 481d1b7c94..a7dd6bfe1e 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -264,11 +264,18 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai } i := 0 for nameOrID, opts := range ctr.config.Networks { - netName, err := r.normalizeNetworkName(nameOrID) + netName, nicName, err := r.normalizeNetworkName(nameOrID) if err != nil { return nil, err } - // assign interface name if empty + + // check whether interface is to be named as the network_interface + // when name left unspecified + if opts.InterfaceName == "" { + opts.InterfaceName = nicName + } + + // assign default interface name if empty if opts.InterfaceName == "" { for i < 100000 { ifName := fmt.Sprintf("eth%d", i) diff --git a/test/e2e/container_iface_name_test.go b/test/e2e/container_iface_name_test.go new file mode 100644 index 0000000000..7f70157e17 --- /dev/null +++ b/test/e2e/container_iface_name_test.go @@ -0,0 +1,280 @@ +package integration + +import ( + "os" + "path/filepath" + + . "github.com/containers/podman/v4/test/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func isDebianRunc(pTest *PodmanTestIntegration) bool { + info := GetHostDistributionInfo() + if info.Distribution == "debian" && pTest.OCIRuntime == "runc" { + return true + } + + return false +} + +func createNetworkDevice(name string) { + session := SystemExec("ip", []string{"link", "add", name, "type", "bridge"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) +} + +func deleteNetworkDevice(name string) { + session := SystemExec("ip", []string{"link", "delete", name}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) +} + +func createContainersConfFileWithDeviceIfaceName(pTest *PodmanTestIntegration) { + configPath := filepath.Join(pTest.TempDir, "containers.conf") + containersConf := []byte("[containers]\ninterface_name = \"device\"\n") + err := os.WriteFile(configPath, containersConf, os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + // Set custom containers.conf file + os.Setenv("CONTAINERS_CONF_OVERRIDE", configPath) + if IsRemote() { + pTest.RestartRemoteService() + } +} + +var _ = Describe("Podman container interface name", func() { + + It("podman container interface name for bridge network", func() { + // Assert that the network interface name inside container for + // bridge network is ethX regardless of interface_name setting + // in the containers.conf file. + + netName1 := createNetworkName("bridge") + netName2 := createNetworkName("bridge") + + defer podmanTest.removeNetwork(netName1) + nc1 := podmanTest.Podman([]string{"network", "create", netName1}) + nc1.WaitWithDefaultTimeout() + Expect(nc1).Should(ExitCleanly()) + + defer podmanTest.removeNetwork(netName2) + nc2 := podmanTest.Podman([]string{"network", "create", netName2}) + nc2.WaitWithDefaultTimeout() + Expect(nc2).Should(ExitCleanly()) + + for _, override := range []bool{false, true} { + if override { + createContainersConfFileWithDeviceIfaceName(podmanTest) + } + + ctr := podmanTest.Podman([]string{"run", "-d", "--network", netName1, "--name", "test", ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr).Should(ExitCleanly()) + + exec1 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", "eth0"}) + exec1.WaitWithDefaultTimeout() + Expect(exec1).Should(ExitCleanly()) + Expect(exec1.OutputToString()).Should(ContainSubstring("eth0")) + + conn := podmanTest.Podman([]string{"network", "connect", netName2, "test"}) + conn.WaitWithDefaultTimeout() + Expect(conn).Should(ExitCleanly()) + + exec2 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", "eth1"}) + exec2.WaitWithDefaultTimeout() + Expect(exec2).Should(ExitCleanly()) + Expect(exec2.OutputToString()).Should(ContainSubstring("eth1")) + + rm := podmanTest.Podman([]string{"rm", "--time=0", "-f", "test"}) + rm.WaitWithDefaultTimeout() + Expect(rm).Should(ExitCleanly()) + } + }) + + It("podman container interface name for macvlan/ipvlan network with no parent", func() { + // Assert that the network interface name inside container for + // macvlan/ipvlan network with no parent interface is ethX + // regardless of interface_name setting in the containers.conf + // file. + + for _, override := range []bool{false, true} { + if override { + createContainersConfFileWithDeviceIfaceName(podmanTest) + } + + for _, driverType := range []string{"macvlan", "ipvlan"} { + if driverType == "ipvlan" && isDebianRunc(podmanTest) { + GinkgoWriter.Println("FIXME: Fails with netavark < 1.10. Re-enable once Debian gets an update") + continue + } + + netName1 := createNetworkName(driverType) + netName2 := createNetworkName(driverType) + + // There is no nic created by the macvlan/ipvlan driver. + defer podmanTest.removeNetwork(netName1) + nc1 := podmanTest.Podman([]string{"network", "create", "-d", driverType, "--subnet", "10.10.0.0/24", netName1}) + nc1.WaitWithDefaultTimeout() + Expect(nc1).Should(ExitCleanly()) + + ctr := podmanTest.Podman([]string{"run", "-d", "--network", netName1, "--name", "test", ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr).Should(ExitCleanly()) + + exec1 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", "eth0"}) + exec1.WaitWithDefaultTimeout() + Expect(exec1).Should(ExitCleanly()) + Expect(exec1.OutputToString()).Should(ContainSubstring("eth0")) + + defer podmanTest.removeNetwork(netName2) + nc2 := podmanTest.Podman([]string{"network", "create", "-d", driverType, "--subnet", "10.25.40.0/24", netName2}) + nc2.WaitWithDefaultTimeout() + Expect(nc2).Should(ExitCleanly()) + + conn := podmanTest.Podman([]string{"network", "connect", netName2, "test"}) + conn.WaitWithDefaultTimeout() + Expect(conn).Should(ExitCleanly()) + + exec2 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", "eth1"}) + exec2.WaitWithDefaultTimeout() + Expect(exec2).Should(ExitCleanly()) + Expect(exec2.OutputToString()).Should(ContainSubstring("eth1")) + + rm := podmanTest.Podman([]string{"rm", "--time=0", "-f", "test"}) + rm.WaitWithDefaultTimeout() + Expect(rm).Should(ExitCleanly()) + } + } + }) + + It("podman container interface name with default scheme for macvlan/ipvlan network with parent", func() { + // Assert that the network interface name inside container for + // macvlan/ipvlan network, created with a specific parent + // interface, continues to be ethX when interface_name in the + // containers.conf file is set to default value, i.e., "". + + SkipIfRootless("cannot create network device in rootless mode.") + + for _, driverType := range []string{"macvlan", "ipvlan"} { + if driverType == "ipvlan" && isDebianRunc(podmanTest) { + GinkgoWriter.Println("FIXME: Fails with netavark < 1.10. Re-enable once Debian gets an update") + continue + } + + // Create a nic to be used as a parent for macvlan/ipvlan network. + nicName1 := createNetworkName("nic")[:8] + nicName2 := createNetworkName("nic")[:8] + + netName1 := createNetworkName(driverType) + netName2 := createNetworkName(driverType) + + parent1 := "parent=" + nicName1 + parent2 := "parent=" + nicName2 + + defer deleteNetworkDevice(nicName1) + createNetworkDevice(nicName1) + + defer podmanTest.removeNetwork(netName1) + nc1 := podmanTest.Podman([]string{"network", "create", "-d", driverType, "-o", parent1, "--subnet", "10.10.0.0/24", netName1}) + nc1.WaitWithDefaultTimeout() + Expect(nc1).Should(ExitCleanly()) + + ctr := podmanTest.Podman([]string{"run", "-d", "--network", netName1, "--name", "test", ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr).Should(ExitCleanly()) + + exec1 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", "eth0"}) + exec1.WaitWithDefaultTimeout() + Expect(exec1).Should(ExitCleanly()) + Expect(exec1.OutputToString()).Should(ContainSubstring("eth0")) + + defer deleteNetworkDevice(nicName2) + createNetworkDevice(nicName2) + + defer podmanTest.removeNetwork(netName2) + nc2 := podmanTest.Podman([]string{"network", "create", "-d", driverType, "-o", parent2, "--subnet", "10.25.40.0/24", netName2}) + nc2.WaitWithDefaultTimeout() + Expect(nc2).Should(ExitCleanly()) + + conn := podmanTest.Podman([]string{"network", "connect", netName2, "test"}) + conn.WaitWithDefaultTimeout() + Expect(conn).Should(ExitCleanly()) + + exec2 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", "eth1"}) + exec2.WaitWithDefaultTimeout() + Expect(exec2).Should(ExitCleanly()) + Expect(exec2.OutputToString()).Should(ContainSubstring("eth1")) + + rm := podmanTest.Podman([]string{"rm", "--time=0", "-f", "test"}) + rm.WaitWithDefaultTimeout() + Expect(rm).Should(ExitCleanly()) + } + }) + + It("podman container interface name with device scheme for macvlan/ipvlan network with parent", func() { + // Assert that the network interface name inside container for + // macvlan/ipvlan network, created with a specific parent + // interface, is the parent interface name ethX when + // interface_name in the containers.conf file is set to "device" + + SkipIfRootless("cannot create network device in rootless mode.") + + createContainersConfFileWithDeviceIfaceName(podmanTest) + + for _, driverType := range []string{"macvlan", "ipvlan"} { + if driverType == "ipvlan" && isDebianRunc(podmanTest) { + GinkgoWriter.Println("FIXME: Fails with netavark < 1.10. Re-enable once Debian gets an update") + continue + } + + // Create a nic to be used as a parent for the network. + nicName1 := createNetworkName("nic")[:8] + nicName2 := createNetworkName("nic")[:8] + + netName1 := createNetworkName(driverType) + netName2 := createNetworkName(driverType) + + parent1 := "parent=" + nicName1 + parent2 := "parent=" + nicName2 + + defer deleteNetworkDevice(nicName1) + createNetworkDevice(nicName1) + + defer podmanTest.removeNetwork(netName1) + nc1 := podmanTest.Podman([]string{"network", "create", "-d", driverType, "-o", parent1, "--subnet", "10.10.0.0/24", netName1}) + nc1.WaitWithDefaultTimeout() + Expect(nc1).Should(ExitCleanly()) + + ctr := podmanTest.Podman([]string{"run", "-d", "--network", netName1, "--name", "test", ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr).Should(ExitCleanly()) + + exec1 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", nicName1}) + exec1.WaitWithDefaultTimeout() + Expect(exec1).Should(ExitCleanly()) + Expect(exec1.OutputToString()).Should(ContainSubstring(nicName1)) + + defer deleteNetworkDevice(nicName2) + createNetworkDevice(nicName2) + + defer podmanTest.removeNetwork(netName2) + nc2 := podmanTest.Podman([]string{"network", "create", "-d", driverType, "-o", parent2, "--subnet", "10.25.40.0/24", netName2}) + nc2.WaitWithDefaultTimeout() + Expect(nc2).Should(ExitCleanly()) + + conn := podmanTest.Podman([]string{"network", "connect", netName2, "test"}) + conn.WaitWithDefaultTimeout() + Expect(conn).Should(ExitCleanly()) + + exec2 := podmanTest.Podman([]string{"exec", "test", "ip", "addr", "show", nicName2}) + exec2.WaitWithDefaultTimeout() + Expect(exec2).Should(ExitCleanly()) + Expect(exec2.OutputToString()).Should(ContainSubstring(nicName2)) + + rm := podmanTest.Podman([]string{"rm", "--time=0", "-f", "test"}) + rm.WaitWithDefaultTimeout() + Expect(rm).Should(ExitCleanly()) + } + }) +})