Support podman-specific per-network mac_address specifiation

Signed-off-by: Bas Zoetekouw <bas.zoetekouw@surf.nl>
This commit is contained in:
Bas Zoetekouw 2024-03-21 15:36:49 +01:00 committed by Povilas Kanapickas
parent 91fbea3d89
commit 45ca1f994f
5 changed files with 144 additions and 11 deletions

47
docs/Extensions.md Normal file
View File

@ -0,0 +1,47 @@
# 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"
```

View File

@ -779,10 +779,8 @@ 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":
@ -866,6 +864,18 @@ def get_net_args(compose, cnt):
else: else:
multiple_nets = {net: net_config or {} for net, net_config in multiple_nets.items()} multiple_nets = {net: net_config or {} for net, net_config in multiple_nets.items()}
# 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(): 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)
@ -875,6 +885,16 @@ def get_net_args(compose, cnt):
ipv4 = net_config_.get("ipv4_address", None) ipv4 = net_config_.get("ipv4_address", None)
ipv6 = net_config_.get("ipv6_address", None) ipv6 = net_config_.get("ipv6_address", None)
# custom extension; not supported by docker-compose v3
mac = net_config_.get("podman.mac_address", None)
# if a mac_address was specified on the container level, apply it to the first network
# This works for Python > 3.6, because dict insert ordering is preserved, so we are
# 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 = [] net_options = []
if ipv4: if ipv4:
@ -893,6 +913,8 @@ def get_net_args(compose, cnt):
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: if is_bridge:
net_args.extend(["--network-alias", ",".join(aliases)]) net_args.extend(["--network-alias", ",".join(aliases)])

View File

@ -104,10 +104,27 @@ class TestGetNetArgs(unittest.TestCase):
container["mac_address"] = mac container["mac_address"] = mac
expected_args = [ expected_args = [
"--mac-address",
mac,
"--network", "--network",
f"{PROJECT_NAME}_net0", f"{PROJECT_NAME}_net0",
"--mac-address=" + mac,
"--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 = [
"--network",
f"{PROJECT_NAME}_net0:mac={mac}",
"--network",
f"{PROJECT_NAME}_net1",
"--network-alias", "--network-alias",
SERVICE_NAME, SERVICE_NAME,
] ]
@ -182,6 +199,49 @@ class TestGetNetArgs(unittest.TestCase):
args = get_net_args(compose, container) args = get_net_args(compose, container)
self.assertListEqual(expected_args, args) 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 = [
"--network",
f"{PROJECT_NAME}_net0:mac={mac0}",
"--network",
f"{PROJECT_NAME}_net1:mac={mac1}",
"--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): def test_mixed_config(self):
ip4_0 = "192.168.0.42" ip4_0 = "192.168.0.42"
ip4_1 = "192.168.1.42" ip4_1 = "192.168.1.42"
@ -199,10 +259,8 @@ class TestGetNetArgs(unittest.TestCase):
container["mac_address"] = mac container["mac_address"] = mac
expected_args = [ expected_args = [
"--mac-address",
mac,
"--network", "--network",
f"{PROJECT_NAME}_net0:ip={ip4_0},ip={ip6_0}", f"{PROJECT_NAME}_net0:ip={ip4_0},ip={ip6_0},mac={mac}",
"--network", "--network",
f"{PROJECT_NAME}_net1:ip={ip4_1}", f"{PROJECT_NAME}_net1:ip={ip4_1}",
"--network", "--network",

View File

@ -20,8 +20,10 @@ services:
networks: networks:
shared-network: shared-network:
ipv4_address: "172.19.1.10" ipv4_address: "172.19.1.10"
podman.mac_address: "02:01:01:00:01:01"
internal-network: internal-network:
ipv4_address: "172.19.2.10" ipv4_address: "172.19.2.10"
podman.mac_address: "02:01:01:00:02:01"
volumes: volumes:
- ./test1.txt:/var/www/html/index.txt:ro,z - ./test1.txt:/var/www/html/index.txt:ro,z
web2: web2:

View File

@ -91,8 +91,11 @@ class TestPodmanComposeNetwork(RunSubprocessMixin, unittest.TestCase):
self.assertEqual(f"{expect}\r\n", out.decode('utf-8')) self.assertEqual(f"{expect}\r\n", out.decode('utf-8'))
expected_macip = { expected_macip = {
"web1": {"eth0": ["172.19.1.10"], "eth1": ["172.19.2.10"]}, "web1": {
"web2": {"eth0": ["172.19.2.11"]}, "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(): for service, interfaces in expected_macip.items():
@ -108,5 +111,6 @@ class TestPodmanComposeNetwork(RunSubprocessMixin, unittest.TestCase):
] ]
out, _ = self.run_subprocess_assert_returncode(ip_cmd) out, _ = self.run_subprocess_assert_returncode(ip_cmd)
for interface, values in interfaces.items(): for interface, values in interfaces.items():
ip = values[0] ip, mac = values
self.assertIn(f"ether {mac}", out.decode('utf-8'))
self.assertIn(f"inet {ip}/", out.decode('utf-8')) self.assertIn(f"inet {ip}/", out.decode('utf-8'))