From 4791595b5cb3b2350c66b0d1ba91a6a171652409 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 8 Dec 2021 15:31:49 +0100 Subject: network connect allow ip, ipv6 and mac address Network connect now supports setting a static ipv4, ipv6 and mac address for the container network. The options are added to the cli and api. Fixes #9883 Signed-off-by: Paul Holzinger --- cmd/podman/networks/connect.go | 33 ++++++++++++++- docs/source/markdown/podman-network-connect.1.md | 16 +++++++- libpod/networking_linux.go | 28 ++++++------- pkg/api/handlers/compat/networks.go | 52 ++++++++++++++++++++++-- pkg/api/handlers/libpod/networks.go | 2 +- pkg/bindings/network/network.go | 25 ++++-------- pkg/bindings/network/types.go | 13 ++---- pkg/bindings/network/types_connect_options.go | 33 --------------- pkg/domain/entities/network.go | 6 ++- pkg/domain/infra/abi/network.go | 2 +- pkg/domain/infra/tunnel/network.go | 3 +- test/e2e/network_connect_disconnect_test.go | 8 +++- 12 files changed, 133 insertions(+), 88 deletions(-) delete mode 100644 pkg/bindings/network/types_connect_options.go diff --git a/cmd/podman/networks/connect.go b/cmd/podman/networks/connect.go index 0d62a45df..b0ffbfe6d 100644 --- a/cmd/podman/networks/connect.go +++ b/cmd/podman/networks/connect.go @@ -1,9 +1,12 @@ package network import ( + "net" + "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -23,13 +26,28 @@ var ( var ( networkConnectOptions entities.NetworkConnectOptions + ipv4 net.IP + ipv6 net.IP + macAddress string ) func networkConnectFlags(cmd *cobra.Command) { flags := cmd.Flags() aliasFlagName := "alias" - flags.StringSliceVar(&networkConnectOptions.Aliases, aliasFlagName, []string{}, "network scoped alias for container") + flags.StringSliceVar(&networkConnectOptions.Aliases, aliasFlagName, nil, "network scoped alias for container") _ = cmd.RegisterFlagCompletionFunc(aliasFlagName, completion.AutocompleteNone) + + ipAddressFlagName := "ip" + flags.IPVar(&ipv4, ipAddressFlagName, nil, "set a static ipv4 address for this container network") + _ = cmd.RegisterFlagCompletionFunc(ipAddressFlagName, completion.AutocompleteNone) + + ipv6AddressFlagName := "ip6" + flags.IPVar(&ipv6, ipv6AddressFlagName, nil, "set a static ipv6 address for this container network") + _ = cmd.RegisterFlagCompletionFunc(ipv6AddressFlagName, completion.AutocompleteNone) + + macAddressFlagName := "mac-address" + flags.StringVar(&macAddress, macAddressFlagName, "", "set a static mac address for this container network") + _ = cmd.RegisterFlagCompletionFunc(macAddressFlagName, completion.AutocompleteNone) } func init() { @@ -42,5 +60,18 @@ func init() { func networkConnect(cmd *cobra.Command, args []string) error { networkConnectOptions.Container = args[1] + if macAddress != "" { + mac, err := net.ParseMAC(macAddress) + if err != nil { + return err + } + networkConnectOptions.StaticMAC = types.HardwareAddr(mac) + } + for _, ip := range []net.IP{ipv4, ipv6} { + if ip != nil { + networkConnectOptions.StaticIPs = append(networkConnectOptions.StaticIPs, ip) + } + } + return registry.ContainerEngine().NetworkConnect(registry.Context(), args[0], networkConnectOptions) } diff --git a/docs/source/markdown/podman-network-connect.1.md b/docs/source/markdown/podman-network-connect.1.md index b998d4b7e..c3eef4038 100644 --- a/docs/source/markdown/podman-network-connect.1.md +++ b/docs/source/markdown/podman-network-connect.1.md @@ -11,12 +11,21 @@ Connects a container to a network. A container can be connected to a network by Once connected, the container can communicate with other containers in the same network. ## OPTIONS -#### **--alias** +#### **--alias**=*name* Add network-scoped alias for the container. If the network is using the `dnsname` CNI plugin, these aliases can be used for name resolution on the given network. Multiple *--alias* options may be specified as input. NOTE: A container will only have access to aliases on the first network that it joins. This is a limitation that will be removed in a later release. +#### **--ip**=*address* +Set a static ipv4 address for this container on this network. + +#### **--ip6**=*address* +Set a static ipv6 address for this container on this network. + +#### **--mac-address**=*address* +Set a static mac address for this container on this network. + ## EXAMPLE Connect a container named *web* to a network named *test* @@ -29,6 +38,11 @@ Connect a container name *web* to a network named *test* with two aliases: web1 podman network connect --alias web1 --alias web2 test web ``` +Connect a container name *web* to a network named *test* with a static ip. +``` +podman network connect --ip 10.89.1.13 test web +``` + ## SEE ALSO **[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)**, **[podman-network-disconnect(1)](podman-network-disconnect.1.md)** diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 3f56be855..a931774f8 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -1170,7 +1170,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro } // ConnectNetwork connects a container to a given network -func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) error { +func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNetworkOptions) error { // only the bridge mode supports cni networks if err := isBridgeNetMode(c.config.NetMode); err != nil { return err @@ -1202,22 +1202,20 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e if err != nil { return err } - if !network.DNSEnabled && len(aliases) > 0 { + if !network.DNSEnabled && len(netOpts.Aliases) > 0 { return errors.Wrapf(define.ErrInvalidArg, "cannot set network aliases for network %q because dns is disabled", netName) } + // always add the short id as alias for docker compat + netOpts.Aliases = append(netOpts.Aliases, c.config.ID[:12]) - eth := getFreeInterfaceName(networks) - if eth == "" { - return errors.New("could not find free network interface name") - } - - perNetOpt := types.PerNetworkOptions{ - // always add the short id as alias to match docker - Aliases: append(aliases, c.config.ID[:12]), - InterfaceName: eth, + if netOpts.InterfaceName == "" { + netOpts.InterfaceName = getFreeInterfaceName(networks) + if netOpts.InterfaceName == "" { + return errors.New("could not find free network interface name") + } } - if err := c.runtime.state.NetworkConnect(c, netName, perNetOpt); err != nil { + if err := c.runtime.state.NetworkConnect(c, netName, netOpts); err != nil { return err } c.newNetworkEvent(events.NetworkConnect, netName) @@ -1234,7 +1232,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e } opts.PortMappings = c.convertPortMappings() opts.Networks = map[string]types.PerNetworkOptions{ - netName: perNetOpt, + netName: netOpts, } results, err := c.runtime.setUpNetwork(c.state.NetNS.Path(), opts) @@ -1290,12 +1288,12 @@ func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force } // ConnectContainerToNetwork connects a container to a CNI network -func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error { +func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, netOpts types.PerNetworkOptions) error { ctr, err := r.LookupContainer(nameOrID) if err != nil { return err } - return ctr.NetworkConnect(nameOrID, netName, aliases) + return ctr.NetworkConnect(nameOrID, netName, netOpts) } // normalizeNetworkName takes a network name, a partial or a full network ID and returns the network name. diff --git a/pkg/api/handlers/compat/networks.go b/pkg/api/handlers/compat/networks.go index 8aab29658..db3af7d0b 100644 --- a/pkg/api/handlers/compat/networks.go +++ b/pkg/api/handlers/compat/networks.go @@ -299,20 +299,66 @@ func Connect(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) var ( - aliases []string netConnect types.NetworkConnect ) if err := json.NewDecoder(r.Body).Decode(&netConnect); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } + + netOpts := nettypes.PerNetworkOptions{} + name := utils.GetName(r) if netConnect.EndpointConfig != nil { if netConnect.EndpointConfig.Aliases != nil { - aliases = netConnect.EndpointConfig.Aliases + netOpts.Aliases = netConnect.EndpointConfig.Aliases + } + + // if IP address is provided + if len(netConnect.EndpointConfig.IPAddress) > 0 { + staticIP := net.ParseIP(netConnect.EndpointConfig.IPAddress) + if staticIP == nil { + utils.Error(w, "failed to parse the ip address", http.StatusInternalServerError, + errors.Errorf("failed to parse the ip address %q", netConnect.EndpointConfig.IPAddress)) + return + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + + if netConnect.EndpointConfig.IPAMConfig != nil { + // if IPAMConfig.IPv4Address is provided + if len(netConnect.EndpointConfig.IPAMConfig.IPv4Address) > 0 { + staticIP := net.ParseIP(netConnect.EndpointConfig.IPAMConfig.IPv4Address) + if staticIP == nil { + utils.Error(w, "failed to parse the ipv4 address", http.StatusInternalServerError, + errors.Errorf("failed to parse the ipv4 address %q", netConnect.EndpointConfig.IPAMConfig.IPv4Address)) + return + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + // if IPAMConfig.IPv6Address is provided + if len(netConnect.EndpointConfig.IPAMConfig.IPv6Address) > 0 { + staticIP := net.ParseIP(netConnect.EndpointConfig.IPAMConfig.IPv6Address) + if staticIP == nil { + utils.Error(w, "failed to parse the ipv6 address", http.StatusInternalServerError, + errors.Errorf("failed to parse the ipv6 address %q", netConnect.EndpointConfig.IPAMConfig.IPv6Address)) + return + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + } + // If MAC address is provided + if len(netConnect.EndpointConfig.MacAddress) > 0 { + staticMac, err := net.ParseMAC(netConnect.EndpointConfig.MacAddress) + if err != nil { + utils.Error(w, "failed to parse the mac address", http.StatusInternalServerError, + errors.Errorf("failed to parse the mac address %q", netConnect.EndpointConfig.IPAMConfig.IPv6Address)) + return + } + netOpts.StaticMAC = nettypes.HardwareAddr(staticMac) } } - err := runtime.ConnectContainerToNetwork(netConnect.Container, name, aliases) + err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netOpts) if err != nil { if errors.Cause(err) == define.ErrNoSuchCtr { utils.ContainerNotFound(w, netConnect.Container, err) diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go index 1f7f2e26c..a28c3c57c 100644 --- a/pkg/api/handlers/libpod/networks.go +++ b/pkg/api/handlers/libpod/networks.go @@ -125,7 +125,7 @@ func Connect(w http.ResponseWriter, r *http.Request) { return } name := utils.GetName(r) - err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netConnect.Aliases) + err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netConnect.PerNetworkOptions) if err != nil { if errors.Cause(err) == define.ErrNoSuchCtr { utils.ContainerNotFound(w, netConnect.Container, err) diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go index 172598be1..66e01a016 100644 --- a/pkg/bindings/network/network.go +++ b/pkg/bindings/network/network.go @@ -3,7 +3,6 @@ package network import ( "context" "net/http" - "net/url" "strings" "github.com/containers/podman/v3/libpod/network/types" @@ -110,8 +109,6 @@ func Disconnect(ctx context.Context, networkName string, ContainerNameOrID strin if err != nil { return err } - // No params are used for disconnect - params := url.Values{} // Disconnect sends everything in body disconnect := struct { Container string @@ -128,7 +125,7 @@ func Disconnect(ctx context.Context, networkName string, ContainerNameOrID strin return err } stringReader := strings.NewReader(body) - response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/disconnect", params, nil, networkName) + response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/disconnect", nil, nil, networkName) if err != nil { return err } @@ -138,32 +135,26 @@ func Disconnect(ctx context.Context, networkName string, ContainerNameOrID strin } // Connect adds a container to a network -func Connect(ctx context.Context, networkName string, ContainerNameOrID string, options *ConnectOptions) error { +func Connect(ctx context.Context, networkName string, containerNameOrID string, options *types.PerNetworkOptions) error { if options == nil { - options = new(ConnectOptions) + options = new(types.PerNetworkOptions) } conn, err := bindings.GetClient(ctx) if err != nil { return err } - // No params are used in connect - params := url.Values{} // Connect sends everything in body - connect := struct { - Container string - Aliases []string - }{ - Container: ContainerNameOrID, - } - if aliases := options.GetAliases(); options.Changed("Aliases") { - connect.Aliases = aliases + connect := entities.NetworkConnectOptions{ + Container: containerNameOrID, + PerNetworkOptions: *options, } + body, err := jsoniter.MarshalToString(connect) if err != nil { return err } stringReader := strings.NewReader(body) - response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/connect", params, nil, networkName) + response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/connect", nil, nil, networkName) if err != nil { return err } diff --git a/pkg/bindings/network/types.go b/pkg/bindings/network/types.go index 8088de061..b82c0e438 100644 --- a/pkg/bindings/network/types.go +++ b/pkg/bindings/network/types.go @@ -1,6 +1,8 @@ package network -import "net" +import ( + "net" +) //go:generate go run ../generator/generator.go CreateOptions // CreateOptions are optional options for creating networks @@ -61,15 +63,6 @@ type DisconnectOptions struct { Force *bool } -//go:generate go run ../generator/generator.go ConnectOptions -// ConnectOptions are optional options for connecting -// containers from a network -type ConnectOptions struct { - // Aliases are names the container will be known as - // when using the dns plugin - Aliases *[]string -} - //go:generate go run ../generator/generator.go ExistsOptions // ExistsOptions are optional options for checking // if a network exists diff --git a/pkg/bindings/network/types_connect_options.go b/pkg/bindings/network/types_connect_options.go deleted file mode 100644 index b7a465999..000000000 --- a/pkg/bindings/network/types_connect_options.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -package network - -import ( - "net/url" - - "github.com/containers/podman/v3/pkg/bindings/internal/util" -) - -// Changed returns true if named field has been set -func (o *ConnectOptions) Changed(fieldName string) bool { - return util.Changed(o, fieldName) -} - -// ToParams formats struct fields to be passed to API service -func (o *ConnectOptions) ToParams() (url.Values, error) { - return util.ToParams(o) -} - -// WithAliases set field Aliases to given value -func (o *ConnectOptions) WithAliases(value []string) *ConnectOptions { - o.Aliases = &value - return o -} - -// GetAliases returns value of field Aliases -func (o *ConnectOptions) GetAliases() []string { - if o.Aliases == nil { - var z []string - return z - } - return *o.Aliases -} diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go index d7389a699..34b89ae7d 100644 --- a/pkg/domain/entities/network.go +++ b/pkg/domain/entities/network.go @@ -2,6 +2,8 @@ package entities import ( "net" + + "github.com/containers/podman/v3/libpod/network/types" ) // NetworkListOptions describes options for listing networks in cli @@ -67,8 +69,8 @@ type NetworkDisconnectOptions struct { // NetworkConnectOptions describes options for connecting // a container to a network type NetworkConnectOptions struct { - Aliases []string - Container string + Container string `json:"container"` + types.PerNetworkOptions } // NetworkPruneReport containers the name of network and an error diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go index 49c7dc60d..c7b12663c 100644 --- a/pkg/domain/infra/abi/network.go +++ b/pkg/domain/infra/abi/network.go @@ -124,7 +124,7 @@ func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname st } func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, options entities.NetworkConnectOptions) error { - return ic.Libpod.ConnectContainerToNetwork(options.Container, networkname, options.Aliases) + return ic.Libpod.ConnectContainerToNetwork(options.Container, networkname, options.PerNetworkOptions) } // NetworkExists checks if the given network exists diff --git a/pkg/domain/infra/tunnel/network.go b/pkg/domain/infra/tunnel/network.go index 069982d30..b5050345a 100644 --- a/pkg/domain/infra/tunnel/network.go +++ b/pkg/domain/infra/tunnel/network.go @@ -81,8 +81,7 @@ func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname st // NetworkConnect removes a container from a given network func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, opts entities.NetworkConnectOptions) error { - options := new(network.ConnectOptions).WithAliases(opts.Aliases) - return network.Connect(ic.ClientCtx, networkname, opts.Container, options) + return network.Connect(ic.ClientCtx, networkname, opts.Container, &opts.PerNetworkOptions) } // NetworkExists checks if the given network exists diff --git a/test/e2e/network_connect_disconnect_test.go b/test/e2e/network_connect_disconnect_test.go index 2205a1263..be94ecb2d 100644 --- a/test/e2e/network_connect_disconnect_test.go +++ b/test/e2e/network_connect_disconnect_test.go @@ -176,12 +176,14 @@ var _ = Describe("Podman network connect and disconnect", func() { // Create a second network newNetName := "aliasTest" + stringid.GenerateNonCryptoID() - session = podmanTest.Podman([]string{"network", "create", newNetName}) + session = podmanTest.Podman([]string{"network", "create", newNetName, "--subnet", "10.11.100.0/24"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) defer podmanTest.removeCNINetwork(newNetName) - connect := podmanTest.Podman([]string{"network", "connect", newNetName, "test"}) + ip := "10.11.100.99" + mac := "44:11:44:11:44:11" + connect := podmanTest.Podman([]string{"network", "connect", "--ip", ip, "--mac-address", mac, newNetName, "test"}) connect.WaitWithDefaultTimeout() Expect(connect).Should(Exit(0)) Expect(connect.ErrorToString()).Should(Equal("")) @@ -200,6 +202,8 @@ var _ = Describe("Podman network connect and disconnect", func() { exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"}) exec.WaitWithDefaultTimeout() Expect(exec).Should(Exit(0)) + Expect(exec.OutputToString()).Should(ContainSubstring(ip)) + Expect(exec.OutputToString()).Should(ContainSubstring(mac)) // make sure no logrus errors are shown https://github.com/containers/podman/issues/9602 rm := podmanTest.Podman([]string{"rm", "--time=0", "-f", "test"}) -- cgit v1.2.3-54-g00ecf