diff options
author | Paul Holzinger <pholzing@redhat.com> | 2021-12-10 15:22:09 +0100 |
---|---|---|
committer | Paul Holzinger <pholzing@redhat.com> | 2021-12-14 15:23:39 +0100 |
commit | 535818414c2a6bdcf6434e36c33775ea1a43f1cf (patch) | |
tree | bc7130eb922b7d2918527f13c3155506af4444f1 /pkg | |
parent | d072167fe2f75db9648bf1be4181b42e9b7db9a4 (diff) | |
download | podman-535818414c2a6bdcf6434e36c33775ea1a43f1cf.tar.gz podman-535818414c2a6bdcf6434e36c33775ea1a43f1cf.tar.bz2 podman-535818414c2a6bdcf6434e36c33775ea1a43f1cf.zip |
support advanced network configuration via cli
Rework the --network parse logic to support multiple networks with
specific network configuration settings.
--network can now be set multiple times. For bridge network mode the
following options have been added:
- **alias=name**: Add network-scoped alias for the container.
- **ip=IPv4**: Specify a static ipv4 address for this container.
- **ip=IPv6**: Specify a static ipv6 address for this container.
- **mac=MAC**: Specify a static mac address address for this container.
- **interface_name**: Specify a name for the created network interface inside the container.
So now you can set --network bridge:ip=10.88.0.10,mac=44:33:22:11:00:99
for the default bridge network as well as for network names.
This is better than using --ip because we can set the ip per network
without any confusion which network the ip address should be assigned
to.
The --ip, --mac-address and --network-alias options are still supported
but --ip or --mac-address can only be set when only one network is set.
This limitation already existed previously.
The ability to specify a custom network interface name is new
Fixes #11534
Signed-off-by: Paul Holzinger <pholzing@redhat.com>
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/domain/infra/abi/play.go | 2 | ||||
-rw-r--r-- | pkg/specgen/generate/namespaces.go | 16 | ||||
-rw-r--r-- | pkg/specgen/namespaces.go | 164 | ||||
-rw-r--r-- | pkg/specgen/namespaces_test.go | 265 | ||||
-rw-r--r-- | pkg/specgen/pod_validate.go | 5 |
5 files changed, 436 insertions, 16 deletions
diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index b0b68567a..409ba938a 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -196,7 +196,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } if options.Network != "" { - ns, networks, netOpts, err := specgen.ParseNetworkString(options.Network) + ns, networks, netOpts, err := specgen.ParseNetworkFlag([]string{options.Network}) if err != nil { return nil, err } diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 782156663..a2bc37e34 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -67,7 +67,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) case "cgroup": return specgen.ParseCgroupNamespace(cfg.Containers.CgroupNS) case "net": - ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS, cfg.Containers.RootlessNetworking == "cni") + ns, _, _, err := specgen.ParseNetworkFlag(nil) return ns, err } @@ -259,6 +259,11 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. if err != nil { return nil, err } + + rtConfig, err := rt.GetConfigNoCopy() + if err != nil { + return nil, err + } // if no network was specified use add the default if len(s.Networks) == 0 { // backwards config still allow the old cni networks list and convert to new format @@ -271,15 +276,16 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. s.Networks = networks } else { // no networks given but bridge is set so use default network - rtConfig, err := rt.GetConfigNoCopy() - if err != nil { - return nil, err - } s.Networks = map[string]types.PerNetworkOptions{ rtConfig.Network.DefaultNetwork: {}, } } } + // rename the "default" network to the correct default name + if opts, ok := s.Networks["default"]; ok { + s.Networks[rtConfig.Network.DefaultNetwork] = opts + delete(s.Networks, "default") + } toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, "bridge", s.Networks)) } diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 121e1ecf7..15a8ece17 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -2,10 +2,12 @@ package specgen import ( "fmt" + "net" "os" "strings" "github.com/containers/common/pkg/cgroups" + "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/util" @@ -325,21 +327,163 @@ func ParseNetworkNamespace(ns string, rootlessDefaultCNI bool) (Namespace, map[s return toReturn, networks, nil } -func ParseNetworkString(network string) (Namespace, map[string]types.PerNetworkOptions, map[string][]string, error) { +// ParseNetworkFlag parses a network string slice into the network options +// If the input is nil or empty it will use the default setting from containers.conf +func ParseNetworkFlag(networks []string) (Namespace, map[string]types.PerNetworkOptions, map[string][]string, error) { var networkOptions map[string][]string - parts := strings.SplitN(network, ":", 2) + // by default we try to use the containers.conf setting + // if we get at least one value use this instead + ns := containerConfig.Containers.NetNS + if len(networks) > 0 { + ns = networks[0] + } - ns, nets, err := ParseNetworkNamespace(network, containerConfig.Containers.RootlessNetworking == "cni") - if err != nil { - return Namespace{}, nil, nil, err + toReturn := Namespace{} + podmanNetworks := make(map[string]types.PerNetworkOptions) + + switch { + case ns == string(Slirp), strings.HasPrefix(ns, string(Slirp)+":"): + parts := strings.SplitN(ns, ":", 2) + if len(parts) > 1 { + networkOptions = make(map[string][]string) + networkOptions[parts[0]] = strings.Split(parts[1], ",") + } + toReturn.NSMode = Slirp + case ns == string(FromPod): + toReturn.NSMode = FromPod + case ns == "" || ns == string(Default) || ns == string(Private): + // Net defaults to Slirp on rootless + if rootless.IsRootless() && containerConfig.Containers.RootlessNetworking != "cni" { + toReturn.NSMode = Slirp + break + } + // if not slirp we use bridge + fallthrough + case ns == string(Bridge), strings.HasPrefix(ns, string(Bridge)+":"): + toReturn.NSMode = Bridge + parts := strings.SplitN(ns, ":", 2) + netOpts := types.PerNetworkOptions{} + if len(parts) > 1 { + var err error + netOpts, err = parseBridgeNetworkOptions(parts[1]) + if err != nil { + return toReturn, nil, nil, err + } + } + // we have to set the special default network name here + podmanNetworks["default"] = netOpts + + case ns == string(NoNetwork): + toReturn.NSMode = NoNetwork + case ns == string(Host): + toReturn.NSMode = Host + case strings.HasPrefix(ns, "ns:"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, nil, nil, errors.Errorf("must provide a path to a namespace when specifying ns:") + } + toReturn.NSMode = Path + toReturn.Value = split[1] + case strings.HasPrefix(ns, string(FromContainer)+":"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, nil, nil, errors.Errorf("must provide name or ID or a container when specifying container:") + } + toReturn.NSMode = FromContainer + toReturn.Value = split[1] + default: + // we should have a normal network + parts := strings.SplitN(ns, ":", 2) + if len(parts) == 1 { + // Assume we have been given a comma separated list of networks for backwards compat. + networkList := strings.Split(ns, ",") + for _, net := range networkList { + podmanNetworks[net] = types.PerNetworkOptions{} + } + } else { + if parts[0] == "" { + return toReturn, nil, nil, errors.New("network name cannot be empty") + } + netOpts, err := parseBridgeNetworkOptions(parts[1]) + if err != nil { + return toReturn, nil, nil, errors.Wrapf(err, "invalid option for network %s", parts[0]) + } + podmanNetworks[parts[0]] = netOpts + } + + // networks need bridge mode + toReturn.NSMode = Bridge } - if len(parts) > 1 { - networkOptions = make(map[string][]string) - networkOptions[parts[0]] = strings.Split(parts[1], ",") - nets = nil + if len(networks) > 1 { + if !toReturn.IsBridge() { + return toReturn, nil, nil, errors.Wrapf(define.ErrInvalidArg, "cannot set multiple networks without bridge network mode, selected mode %s", toReturn.NSMode) + } + + for _, network := range networks[1:] { + parts := strings.SplitN(network, ":", 2) + if parts[0] == "" { + return toReturn, nil, nil, errors.Wrapf(define.ErrInvalidArg, "network name cannot be empty") + } + if util.StringInSlice(parts[0], []string{string(Bridge), string(Slirp), string(FromPod), string(NoNetwork), + string(Default), string(Private), string(Path), string(FromContainer), string(Host)}) { + return toReturn, nil, nil, errors.Wrapf(define.ErrInvalidArg, "can only set extra network names, selected mode %s conflicts with bridge", parts[0]) + } + netOpts := types.PerNetworkOptions{} + if len(parts) > 1 { + var err error + netOpts, err = parseBridgeNetworkOptions(parts[1]) + if err != nil { + return toReturn, nil, nil, errors.Wrapf(err, "invalid option for network %s", parts[0]) + } + } + podmanNetworks[parts[0]] = netOpts + } + } + + return toReturn, podmanNetworks, networkOptions, nil +} + +func parseBridgeNetworkOptions(opts string) (types.PerNetworkOptions, error) { + netOpts := types.PerNetworkOptions{} + if len(opts) == 0 { + return netOpts, nil + } + allopts := strings.Split(opts, ",") + for _, opt := range allopts { + split := strings.SplitN(opt, "=", 2) + switch split[0] { + case "ip", "ip6": + ip := net.ParseIP(split[1]) + if ip == nil { + return netOpts, errors.Errorf("invalid ip address %q", split[1]) + } + netOpts.StaticIPs = append(netOpts.StaticIPs, ip) + + case "mac": + mac, err := net.ParseMAC(split[1]) + if err != nil { + return netOpts, err + } + netOpts.StaticMAC = types.HardwareAddr(mac) + + case "alias": + if split[1] == "" { + return netOpts, errors.New("alias cannot be empty") + } + netOpts.Aliases = append(netOpts.Aliases, split[1]) + + case "interface_name": + if split[1] == "" { + return netOpts, errors.New("interface_name cannot be empty") + } + netOpts.InterfaceName = split[1] + + default: + return netOpts, errors.Errorf("unknown bridge network option: %s", split[0]) + } } - return ns, nets, networkOptions, nil + return netOpts, nil } func SetupUserNS(idmappings *storage.IDMappingOptions, userns Namespace, g *generate.Generator) (string, error) { diff --git a/pkg/specgen/namespaces_test.go b/pkg/specgen/namespaces_test.go new file mode 100644 index 000000000..4f69e6b98 --- /dev/null +++ b/pkg/specgen/namespaces_test.go @@ -0,0 +1,265 @@ +package specgen + +import ( + "net" + "testing" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/pkg/rootless" + "github.com/stretchr/testify/assert" +) + +func parsMacNoErr(mac string) types.HardwareAddr { + m, _ := net.ParseMAC(mac) + return types.HardwareAddr(m) +} + +func TestParseNetworkFlag(t *testing.T) { + // root and rootless have different defaults + defaultNetName := "default" + defaultNetworks := map[string]types.PerNetworkOptions{ + defaultNetName: {}, + } + defaultNsMode := Namespace{NSMode: Bridge} + if rootless.IsRootless() { + defaultNsMode = Namespace{NSMode: Slirp} + defaultNetworks = map[string]types.PerNetworkOptions{} + } + + tests := []struct { + name string + args []string + nsmode Namespace + networks map[string]types.PerNetworkOptions + options map[string][]string + err string + }{ + { + name: "empty input", + args: nil, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "empty string as input", + args: []string{}, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "default mode", + args: []string{"default"}, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "private mode", + args: []string{"private"}, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "bridge mode", + args: []string{"bridge"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: {}, + }, + }, + { + name: "slirp4netns mode", + args: []string{"slirp4netns"}, + nsmode: Namespace{NSMode: Slirp}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "from pod mode", + args: []string{"pod"}, + nsmode: Namespace{NSMode: FromPod}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "no network mode", + args: []string{"none"}, + nsmode: Namespace{NSMode: NoNetwork}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "container mode", + args: []string{"container:abc"}, + nsmode: Namespace{NSMode: FromContainer, Value: "abc"}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "ns path mode", + args: []string{"ns:/path"}, + nsmode: Namespace{NSMode: Path, Value: "/path"}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "slirp4netns mode with options", + args: []string{"slirp4netns:cidr=10.0.0.0/24"}, + nsmode: Namespace{NSMode: Slirp}, + networks: map[string]types.PerNetworkOptions{}, + options: map[string][]string{ + "slirp4netns": {"cidr=10.0.0.0/24"}, + }, + }, + { + name: "bridge mode with options 1", + args: []string{"bridge:ip=10.0.0.1,mac=11:22:33:44:55:66"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + StaticIPs: []net.IP{net.ParseIP("10.0.0.1")}, + StaticMAC: parsMacNoErr("11:22:33:44:55:66"), + }, + }, + }, + { + name: "bridge mode with options 2", + args: []string{"bridge:ip=10.0.0.1,ip=10.0.0.5"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + StaticIPs: []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.5")}, + }, + }, + }, + { + name: "bridge mode with ip6 option", + args: []string{"bridge:ip6=fd10::"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + StaticIPs: []net.IP{net.ParseIP("fd10::")}, + }, + }, + }, + { + name: "bridge mode with alias option", + args: []string{"bridge:alias=myname,alias=myname2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + Aliases: []string{"myname", "myname2"}, + }, + }, + }, + { + name: "bridge mode with alias option", + args: []string{"bridge:alias=myname,alias=myname2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + Aliases: []string{"myname", "myname2"}, + }, + }, + }, + { + name: "bridge mode with interface option", + args: []string{"bridge:interface_name=eth123"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + InterfaceName: "eth123", + }, + }, + }, + { + name: "bridge mode with invalid option", + args: []string{"bridge:abc=123"}, + nsmode: Namespace{NSMode: Bridge}, + err: "unknown bridge network option: abc", + }, + { + name: "bridge mode with invalid ip", + args: []string{"bridge:ip=10..1"}, + nsmode: Namespace{NSMode: Bridge}, + err: "invalid ip address \"10..1\"", + }, + { + name: "bridge mode with invalid mac", + args: []string{"bridge:mac=123"}, + nsmode: Namespace{NSMode: Bridge}, + err: "address 123: invalid MAC address", + }, + { + name: "network name", + args: []string{"someName"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {}, + }, + }, + { + name: "network name with options", + args: []string{"someName:ip=10.0.0.1"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {StaticIPs: []net.IP{net.ParseIP("10.0.0.1")}}, + }, + }, + { + name: "multiple networks", + args: []string{"someName", "net2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {}, + "net2": {}, + }, + }, + { + name: "multiple networks with options", + args: []string{"someName:ip=10.0.0.1", "net2:ip=10.10.0.1"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {StaticIPs: []net.IP{net.ParseIP("10.0.0.1")}}, + "net2": {StaticIPs: []net.IP{net.ParseIP("10.10.0.1")}}, + }, + }, + { + name: "multiple networks with bridge mode first should map to default net", + args: []string{"bridge", "net2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: {}, + "net2": {}, + }, + }, + { + name: "conflicting network modes should error", + args: []string{"bridge", "host"}, + nsmode: Namespace{NSMode: Bridge}, + err: "can only set extra network names, selected mode host conflicts with bridge: invalid argument", + }, + { + name: "multiple networks empty name should error", + args: []string{"someName", ""}, + nsmode: Namespace{NSMode: Bridge}, + err: "network name cannot be empty: invalid argument", + }, + { + name: "multiple networks on invalid mode should error", + args: []string{"host", "net2"}, + nsmode: Namespace{NSMode: Host}, + err: "cannot set multiple networks without bridge network mode, selected mode host: invalid argument", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, got1, got2, err := ParseNetworkFlag(tt.args) + if tt.err != "" { + assert.EqualError(t, err, tt.err, tt.name) + } else { + assert.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.nsmode, got, tt.name) + assert.Equal(t, tt.networks, got1, tt.name) + assert.Equal(t, tt.options, got2, tt.name) + }) + } +} diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index 32c1159c6..224a5b12d 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -42,6 +42,11 @@ func (p *PodSpecGenerator) Validate() error { if p.NetNS.NSMode != Default && p.NetNS.NSMode != "" { return errors.New("NoInfra and network modes cannot be used together") } + // Note that networks might be set when --ip or --mac was set + // so we need to check that no networks are set without the infra + if len(p.Networks) > 0 { + return errors.New("cannot set networks options without infra container") + } if len(p.DNSOption) > 0 { return exclusivePodOptions("NoInfra", "DNSOption") } |