Merge pull request #867 from baszoetekouw/fix-networks

Fix multiple networks with separately specified ip and mac
This commit is contained in:
Povilas Kanapickas 2024-04-08 23:21:38 +03:00 committed by GitHub
commit e893d06313
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 638 additions and 49 deletions

2
.gitignore vendored
View File

@ -47,6 +47,8 @@ coverage.xml
*.cover *.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
test-compose.yaml
test-compose-?.yaml
# Translations # Translations
*.mo *.mo

67
docs/Extensions.md Normal file
View File

@ -0,0 +1,67 @@
# Podman specific extensions to the docker-compose format
Podman-compose supports the following extension to the docker-compose format.
## Per-network MAC-addresses
Generic docker-compose files support specification of the MAC address on the container level. If the
container has multiple network interfaces, the specified MAC address is applied to the first
specified network.
Podman-compose in addition supports the specification of MAC addresses on a per-network basis. This
is done by adding a `podman.mac_address` key to the network configuration in the container. The
value of the `podman.mac_address` key is the MAC address to be used for the network interface.
Specifying a MAC address for the container and for individual networks at the same time is not
supported.
Example:
```yaml
---
version: "3"
networks:
net0:
driver: "bridge"
ipam:
config:
- subnet: "192.168.0.0/24"
net1:
driver: "bridge"
ipam:
config:
- subnet: "192.168.1.0/24"
services:
webserver
image: "busybox"
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc", "-p", "8001"]
networks:
net0:
ipv4_address: "192.168.0.10"
podman.mac_address: "02:aa:aa:aa:aa:aa"
net1:
ipv4_address: "192.168.1.10"
podman.mac_address: "02:bb:bb:bb:bb:bb"
```
## Podman-specific network modes
Generic docker-compose supports the following values for `network-mode` for a container:
- `bridge`
- `host`
- `none`
- `service`
- `container`
In addition, podman-compose supports the following podman-specific values for `network-mode`:
- `slirp4netns[:<options>,...]`
- `ns:<options>`
- `pasta[:<options>,...]`
- `private`
The options to the network modes are passed to the `--network` option of the `podman create` command
as-is.

View File

@ -780,27 +780,29 @@ async def assert_cnt_nets(compose, cnt):
def get_net_args(compose, cnt): def get_net_args(compose, cnt):
service_name = cnt["service_name"] service_name = cnt["service_name"]
net_args = [] net_args = []
mac_address = cnt.get("mac_address", None)
if mac_address:
net_args.extend(["--mac-address", mac_address])
is_bridge = False is_bridge = False
mac_address = cnt.get("mac_address", None)
net = cnt.get("network_mode", None) net = cnt.get("network_mode", None)
if net: if net:
if net == "none": if net == "none":
is_bridge = False is_bridge = False
elif net == "host": elif net == "host":
net_args.extend(["--network", net]) net_args.append(f"--network={net}")
elif net.startswith("slirp4netns:"): elif net.startswith("slirp4netns"): # Note: podman-specific network mode
net_args.extend(["--network", net]) net_args.append(f"--network={net}")
elif net.startswith("ns:"): elif net == "private": # Note: podman-specific network mode
net_args.extend(["--network", net]) net_args.append("--network=private")
elif net.startswith("pasta"): # Note: podman-specific network mode
net_args.append(f"--network={net}")
elif net.startswith("ns:"): # Note: podman-specific network mode
net_args.append(f"--network={net}")
elif net.startswith("service:"): elif net.startswith("service:"):
other_srv = net.split(":", 1)[1].strip() other_srv = net.split(":", 1)[1].strip()
other_cnt = compose.container_names_by_service[other_srv][0] other_cnt = compose.container_names_by_service[other_srv][0]
net_args.extend(["--network", f"container:{other_cnt}"]) net_args.append(f"--network=container:{other_cnt}")
elif net.startswith("container:"): elif net.startswith("container:"):
other_cnt = net.split(":", 1)[1].strip() other_cnt = net.split(":", 1)[1].strip()
net_args.extend(["--network", f"container:{other_cnt}"]) net_args.append(f"--network=container:{other_cnt}")
elif net.startswith("bridge"): elif net.startswith("bridge"):
is_bridge = True is_bridge = True
else: else:
@ -812,6 +814,7 @@ def get_net_args(compose, cnt):
default_net = compose.default_net default_net = compose.default_net
nets = compose.networks nets = compose.networks
cnt_nets = cnt.get("networks", None) cnt_nets = cnt.get("networks", None)
aliases = [service_name] aliases = [service_name]
# NOTE: from podman manpage: # NOTE: from podman manpage:
# NOTE: A container will only have access to aliases on the first network # NOTE: A container will only have access to aliases on the first network
@ -856,32 +859,82 @@ def get_net_args(compose, cnt):
net_names.append(net_name) net_names.append(net_name)
net_names_str = ",".join(net_names) net_names_str = ",".join(net_names)
if ip_assignments > 1: # TODO: add support for per-interface aliases
multiple_nets = cnt.get("networks", None) # See https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases
multiple_net_names = multiple_nets.keys() # Even though podman accepts network-specific aliases (e.g., --network=bridge:alias=foo,
# podman currently ignores this if a per-container network-alias is set; as pdoman-compose
# always sets a network-alias to the container name, is currently doesn't make sense to
# implement this.
multiple_nets = cnt.get("networks", None)
if multiple_nets and len(multiple_nets) > 1:
# networks can be specified as a dict with config per network or as a plain list without
# config. Support both cases by converting the plain list to a dict with empty config.
if is_list(multiple_nets):
multiple_nets = {net: {} for net in multiple_nets}
else:
multiple_nets = {net: net_config or {} for net, net_config in multiple_nets.items()}
for net_ in multiple_net_names: # if a mac_address was specified on the container level, we need to check that it is not
# specified on the network level as well
if mac_address is not None:
for net_config_ in multiple_nets.values():
network_mac = net_config_.get("podman.mac_address", None)
if network_mac is not None:
raise RuntimeError(
f"conflicting mac addresses {mac_address} and {network_mac}:"
"specifying mac_address on both container and network level "
"is not supported"
)
for net_, net_config_ in multiple_nets.items():
net_desc = nets[net_] or {} net_desc = nets[net_] or {}
is_ext = net_desc.get("external", None) is_ext = net_desc.get("external", None)
ext_desc = is_ext if is_dict(is_ext) else {} ext_desc = is_ext if is_dict(is_ext) else {}
default_net_name = net_ if is_ext else f"{proj_name}_{net_}" default_net_name = net_ if is_ext else f"{proj_name}_{net_}"
net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name
ipv4 = multiple_nets[net_].get("ipv4_address", None) ipv4 = net_config_.get("ipv4_address", None)
ipv6 = multiple_nets[net_].get("ipv6_address", None) ipv6 = net_config_.get("ipv6_address", None)
if ipv4 is not None and ipv6 is not None: # custom extension; not supported by docker-compose v3
net_args.extend(["--network", f"{net_name}:ip={ipv4},ip={ipv6}"]) mac = net_config_.get("podman.mac_address", None)
elif ipv4 is None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv6}"]) # if a mac_address was specified on the container level, apply it to the first network
elif ipv6 is None and ipv4 is not None: # This works for Python > 3.6, because dict insert ordering is preserved, so we are
net_args.extend(["--network", f"{net_name}:ip={ipv4}"]) # sure that the first network we encounter here is also the first one specified by
# the user
if mac is None and mac_address is not None:
mac = mac_address
mac_address = None
net_options = []
if ipv4:
net_options.append(f"ip={ipv4}")
if ipv6:
net_options.append(f"ip={ipv6}")
if mac:
net_options.append(f"mac={mac}")
if net_options:
net_args.append(f"--network={net_name}:" + ",".join(net_options))
else:
net_args.append(f"--network={net_name}")
else: else:
if is_bridge: if is_bridge:
net_args.extend(["--net", net_names_str, "--network-alias", ",".join(aliases)]) if net_names_str:
net_args.append(f"--network={net_names_str}")
else:
net_args.append("--network=bridge")
if ip: if ip:
net_args.append(f"--ip={ip}") net_args.append(f"--ip={ip}")
if ip6: if ip6:
net_args.append(f"--ip6={ip6}") net_args.append(f"--ip6={ip6}")
if mac_address:
net_args.append(f"--mac-address={mac_address}")
if is_bridge:
for alias in aliases:
net_args.extend([f"--network-alias={alias}"])
return net_args return net_args

View File

@ -6,9 +6,9 @@ from unittest import mock
from podman_compose import container_to_args from podman_compose import container_to_args
def create_compose_mock(): def create_compose_mock(project_name="test_project_name"):
compose = mock.Mock() compose = mock.Mock()
compose.project_name = "test_project_name" compose.project_name = project_name
compose.dirname = "test_dirname" compose.dirname = "test_dirname"
compose.container_names_by_service.get = mock.Mock(return_value=None) compose.container_names_by_service.get = mock.Mock(return_value=None)
compose.prefer_volume_over_mount = False compose.prefer_volume_over_mount = False
@ -37,10 +37,8 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
[ [
"--name=project_name_service_name1", "--name=project_name_service_name1",
"-d", "-d",
"--net", "--network=bridge",
"", "--network-alias=service_name",
"--network-alias",
"service_name",
"busybox", "busybox",
], ],
) )
@ -57,10 +55,8 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
[ [
"--name=project_name_service_name1", "--name=project_name_service_name1",
"-d", "-d",
"--net", "--network=bridge",
"", "--network-alias=service_name",
"--network-alias",
"service_name",
"--runtime", "--runtime",
"runsc", "runsc",
"busybox", "busybox",
@ -82,10 +78,8 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
[ [
"--name=project_name_service_name1", "--name=project_name_service_name1",
"-d", "-d",
"--net", "--network=bridge",
"", "--network-alias=service_name",
"--network-alias",
"service_name",
"--sysctl", "--sysctl",
"net.core.somaxconn=1024", "net.core.somaxconn=1024",
"--sysctl", "--sysctl",
@ -109,10 +103,8 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
[ [
"--name=project_name_service_name1", "--name=project_name_service_name1",
"-d", "-d",
"--net", "--network=bridge",
"", "--network-alias=service_name",
"--network-alias",
"service_name",
"--sysctl", "--sysctl",
"net.core.somaxconn=1024", "net.core.somaxconn=1024",
"--sysctl", "--sysctl",
@ -143,10 +135,8 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
[ [
"--name=project_name_service_name1", "--name=project_name_service_name1",
"-d", "-d",
"--net", "--network=bridge",
"", "--network-alias=service_name",
"--network-alias",
"service_name",
"--pid", "--pid",
"host", "host",
"busybox", "busybox",
@ -166,10 +156,8 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
"--name=project_name_service_name1", "--name=project_name_service_name1",
"-d", "-d",
"--http-proxy=false", "--http-proxy=false",
"--net", "--network=bridge",
"", "--network-alias=service_name",
"--network-alias",
"service_name",
"busybox", "busybox",
], ],
) )

View File

@ -0,0 +1,298 @@
import unittest
from parameterized import parameterized
from podman_compose import get_net_args
from .test_container_to_args import create_compose_mock
PROJECT_NAME = "test_project_name"
SERVICE_NAME = "service_name"
CONTAINER_NAME = f"{PROJECT_NAME}_{SERVICE_NAME}_1"
def get_networked_compose(num_networks=1):
compose = create_compose_mock(PROJECT_NAME)
for network in range(num_networks):
compose.networks[f"net{network}"] = {
"driver": "bridge",
"ipam": {
"config": [
{"subnet": f"192.168.{network}.0/24"},
{"subnet": f"fd00:{network}::/64"},
]
},
"enable_ipv6": True,
}
return compose
def get_minimal_container():
return {
"name": CONTAINER_NAME,
"service_name": SERVICE_NAME,
"image": "busybox",
}
class TestGetNetArgs(unittest.TestCase):
def test_minimal(self):
compose = get_networked_compose()
container = get_minimal_container()
expected_args = [
"--network=bridge",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_one_net(self):
compose = get_networked_compose()
container = get_minimal_container()
container["networks"] = {"net0": {}}
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_alias(self):
compose = get_networked_compose()
container = get_minimal_container()
container["networks"] = {"net0": {}}
container["_aliases"] = ["alias1", "alias2"]
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--network-alias={SERVICE_NAME}",
"--network-alias=alias1",
"--network-alias=alias2",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_one_ipv4(self):
ip = "192.168.0.42"
compose = get_networked_compose()
container = get_minimal_container()
container["networks"] = {"net0": {"ipv4_address": ip}}
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--ip={ip}",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertEqual(expected_args, args)
def test_one_ipv6(self):
ipv6_address = "fd00:0::42"
compose = get_networked_compose()
container = get_minimal_container()
container["networks"] = {"net0": {"ipv6_address": ipv6_address}}
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--ip6={ipv6_address}",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_one_mac(self):
mac = "00:11:22:33:44:55"
compose = get_networked_compose()
container = get_minimal_container()
container["networks"] = {"net0": {}}
container["mac_address"] = mac
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--mac-address={mac}",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_one_mac_two_nets(self):
mac = "00:11:22:33:44:55"
compose = get_networked_compose(num_networks=6)
container = get_minimal_container()
container["networks"] = {"net0": {}, "net1": {}}
container["mac_address"] = mac
expected_args = [
f"--network={PROJECT_NAME}_net0:mac={mac}",
f"--network={PROJECT_NAME}_net1",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_two_nets_as_dict(self):
compose = get_networked_compose(num_networks=2)
container = get_minimal_container()
container["networks"] = {"net0": {}, "net1": {}}
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--network={PROJECT_NAME}_net1",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_two_nets_as_list(self):
compose = get_networked_compose(num_networks=2)
container = get_minimal_container()
container["networks"] = ["net0", "net1"]
expected_args = [
f"--network={PROJECT_NAME}_net0",
f"--network={PROJECT_NAME}_net1",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_two_ipv4(self):
ip0 = "192.168.0.42"
ip1 = "192.168.1.42"
compose = get_networked_compose(num_networks=2)
container = get_minimal_container()
container["networks"] = {"net0": {"ipv4_address": ip0}, "net1": {"ipv4_address": ip1}}
expected_args = [
f"--network={PROJECT_NAME}_net0:ip={ip0}",
f"--network={PROJECT_NAME}_net1:ip={ip1}",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_two_ipv6(self):
ip0 = "fd00:0::42"
ip1 = "fd00:1::42"
compose = get_networked_compose(num_networks=2)
container = get_minimal_container()
container["networks"] = {"net0": {"ipv6_address": ip0}, "net1": {"ipv6_address": ip1}}
expected_args = [
f"--network={PROJECT_NAME}_net0:ip={ip0}",
f"--network={PROJECT_NAME}_net1:ip={ip1}",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
# custom extension; not supported by docker-compose
def test_two_mac(self):
mac0 = "00:00:00:00:00:01"
mac1 = "00:00:00:00:00:02"
compose = get_networked_compose(num_networks=2)
container = get_minimal_container()
container["networks"] = {
"net0": {"podman.mac_address": mac0},
"net1": {"podman.mac_address": mac1},
}
expected_args = [
f"--network={PROJECT_NAME}_net0:mac={mac0}",
f"--network={PROJECT_NAME}_net1:mac={mac1}",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_mixed_mac(self):
ip4_0 = "192.168.0.42"
ip4_1 = "192.168.1.42"
ip4_2 = "192.168.2.42"
mac_0 = "00:00:00:00:00:01"
mac_1 = "00:00:00:00:00:02"
compose = get_networked_compose(num_networks=3)
container = get_minimal_container()
container["networks"] = {
"net0": {"ipv4_address": ip4_0},
"net1": {"ipv4_address": ip4_1, "podman.mac_address": mac_0},
"net2": {"ipv4_address": ip4_2},
}
container["mac_address"] = mac_1
expected_exception = (
r"specifying mac_address on both container and network level " r"is not supported"
)
self.assertRaisesRegex(RuntimeError, expected_exception, get_net_args, compose, container)
def test_mixed_config(self):
ip4_0 = "192.168.0.42"
ip4_1 = "192.168.1.42"
ip6_0 = "fd00:0::42"
ip6_2 = "fd00:2::42"
mac = "00:11:22:33:44:55"
compose = get_networked_compose(num_networks=4)
container = get_minimal_container()
container["networks"] = {
"net0": {"ipv4_address": ip4_0, "ipv6_address": ip6_0},
"net1": {"ipv4_address": ip4_1},
"net2": {"ipv6_address": ip6_2},
"net3": {},
}
container["mac_address"] = mac
expected_args = [
f"--network={PROJECT_NAME}_net0:ip={ip4_0},ip={ip6_0},mac={mac}",
f"--network={PROJECT_NAME}_net1:ip={ip4_1}",
f"--network={PROJECT_NAME}_net2:ip={ip6_2}",
f"--network={PROJECT_NAME}_net3",
f"--network-alias={SERVICE_NAME}",
]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
@parameterized.expand([
("bridge", ["--network=bridge", f"--network-alias={SERVICE_NAME}"]),
("host", ["--network=host"]),
("none", []),
("slirp4netns", ["--network=slirp4netns"]),
("slirp4netns:cidr=10.42.0.0/24", ["--network=slirp4netns:cidr=10.42.0.0/24"]),
("private", ["--network=private"]),
("pasta", ["--network=pasta"]),
("pasta:--ipv4-only,-a,10.0.2.0", ["--network=pasta:--ipv4-only,-a,10.0.2.0"]),
("ns:my_namespace", ["--network=ns:my_namespace"]),
("container:my_container", ["--network=container:my_container"]),
])
def test_network_modes(self, network_mode, expected_args):
compose = get_networked_compose()
container = get_minimal_container()
container["network_mode"] = network_mode
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)
def test_network_mode_invalid(self):
compose = get_networked_compose()
container = get_minimal_container()
container["network_mode"] = "invalid_mode"
with self.assertRaises(SystemExit):
get_net_args(compose, container)
def test_network__mode_service(self):
compose = get_networked_compose()
compose.container_names_by_service = {
"service_1": ["container_1"],
"service_2": ["container_2"],
}
container = get_minimal_container()
container["network_mode"] = "service:service_2"
expected_args = ["--network=container:container_2"]
args = get_net_args(compose, container)
self.assertListEqual(expected_args, args)

View File

@ -0,0 +1,61 @@
version: "3"
networks:
shared-network:
driver: bridge
ipam:
config:
- subnet: "172.19.1.0/24"
internal-network:
driver: bridge
ipam:
config:
- subnet: "172.19.2.0/24"
services:
web1:
image: busybox
hostname: web1
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"]
working_dir: /var/www/html
networks:
shared-network:
ipv4_address: "172.19.1.10"
podman.mac_address: "02:01:01:00:01:01"
internal-network:
ipv4_address: "172.19.2.10"
podman.mac_address: "02:01:01:00:02:01"
volumes:
- ./test1.txt:/var/www/html/index.txt:ro,z
web2:
image: busybox
hostname: web2
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"]
working_dir: /var/www/html
mac_address: "02:01:01:00:02:02"
networks:
internal-network:
ipv4_address: "172.19.2.11"
volumes:
- ./test2.txt:/var/www/html/index.txt:ro,z
web3:
image: busybox
hostname: web2
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"]
working_dir: /var/www/html
networks:
internal-network:
volumes:
- ./test3.txt:/var/www/html/index.txt:ro,z
web4:
image: busybox
hostname: web2
command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"]
working_dir: /var/www/html
networks:
internal-network:
shared-network:
ipv4_address: "172.19.1.13"
volumes:
- ./test4.txt:/var/www/html/index.txt:ro,z

View File

@ -0,0 +1 @@
test1

View File

@ -0,0 +1 @@
test2

View File

@ -0,0 +1 @@
test3

View File

@ -0,0 +1 @@
test4

View File

@ -0,0 +1,116 @@
# SPDX-License-Identifier: GPL-2.0
"""
test_podman_compose_networks.py
Tests the podman networking parameters
"""
# pylint: disable=redefined-outer-name
import os
import unittest
from .test_podman_compose import podman_compose_path
from .test_podman_compose import test_path
from .test_utils import RunSubprocessMixin
class TestPodmanComposeNetwork(RunSubprocessMixin, unittest.TestCase):
@staticmethod
def compose_file():
"""Returns the path to the compose file used for this test module"""
return os.path.join(test_path(), "nets_test_ip", "docker-compose.yml")
def teardown(self):
"""
Ensures that the services within the "profile compose file" are removed between
each test case.
"""
# run the test case
yield
down_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
self.compose_file(),
"kill",
"-a",
]
self.run_subprocess(down_cmd)
def test_networks(self):
up_cmd = [
"coverage",
"run",
podman_compose_path(),
"-f",
self.compose_file(),
"up",
"-d",
"--force-recreate",
]
self.run_subprocess_assert_returncode(up_cmd)
check_cmd = [
podman_compose_path(),
"-f",
self.compose_file(),
"ps",
"--format",
'"{{.Names}}"',
]
out, _ = self.run_subprocess_assert_returncode(check_cmd)
self.assertIn(b"nets_test_ip_web1_1", out)
self.assertIn(b"nets_test_ip_web2_1", out)
expected_wget = {
"172.19.1.10": "test1",
"172.19.2.10": "test1",
"172.19.2.11": "test2",
"web3": "test3",
"172.19.1.13": "test4",
}
for service in ("web1", "web2"):
for ip, expect in expected_wget.items():
wget_cmd = [
podman_compose_path(),
"-f",
self.compose_file(),
"exec",
service,
"wget",
"-q",
"-O-",
f"http://{ip}:8001/index.txt",
]
out, _ = self.run_subprocess_assert_returncode(wget_cmd)
self.assertEqual(f"{expect}\r\n", out.decode('utf-8'))
expected_macip = {
"web1": {
"eth0": ["172.19.1.10", "02:01:01:00:01:01"],
"eth1": ["172.19.2.10", "02:01:01:00:02:01"],
},
"web2": {"eth0": ["172.19.2.11", "02:01:01:00:02:02"]},
}
for service, interfaces in expected_macip.items():
ip_cmd = [
podman_compose_path(),
"-f",
self.compose_file(),
"exec",
service,
"ip",
"addr",
"show",
]
out, _ = self.run_subprocess_assert_returncode(ip_cmd)
for interface, values in interfaces.items():
ip, mac = values
self.assertIn(f"ether {mac}", out.decode('utf-8'))
self.assertIn(f"inet {ip}/", out.decode('utf-8'))