#!/usr/bin/env python3 # # Copyright VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import unittest import glob import json from base_vyostest_shim import VyOSUnitTestSHIM from ipaddress import ip_interface from vyos.configsession import ConfigSessionError from vyos.utils.network import get_interface_vrf from vyos.utils.process import cmdl from vyos.utils.process import process_named_running base_path = ['container'] PROCESS_NAME = 'conmon' busybox_image = 'busybox:stable' busybox_image_path = '/usr/share/vyos/busybox-stable.tar' def cmd_to_json(command): c = cmdl(['podman'] + command + ['--format=json'], sudo=True) data = json.loads(c)[0] return data class TestContainer(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestContainer, cls).setUpClass() # Load image for smoketest provided in vyos-1x-smoketest if not os.path.exists(busybox_image_path): cls.fail(cls, f'{busybox_image} image not available') cmdl(['podman', 'load', '-i', busybox_image_path], sudo=True) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) @classmethod def tearDownClass(cls): super(TestContainer, cls).tearDownClass() # Cleanup podman image cmdl(['podman', 'image', 'rm', '-f', busybox_image], sudo=True) def tearDown(self): self.cli_delete(base_path) self.cli_commit() # Ensure no container process remains self.assertIsNone(process_named_running(PROCESS_NAME)) # Ensure systemd units are removed units = glob.glob('/run/containers/systemd/vyos*') self.assertEqual(units, []) # always forward to base class super().tearDown() def is_running(self, name): command = ['systemctl', 'show', f'vyos-container-{name}', '--property=ActiveState', '--value'] return cmdl(command).strip() == 'active' def test_basic(self): cont_name = 'c1' env_key = 'TestKey' env_key1 = 'TestKey1' env_value = 'TestValue,*' env_value1 = 'Test Spaced Values' self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '10.0.2.15/24']) self.cli_set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2', 'distance', '230']) self.cli_set(['system', 'name-server', '1.1.1.1']) self.cli_set(['system', 'name-server', '8.8.8.8']) self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096']) self.cli_set(base_path + ['name', cont_name, 'log-driver', 'journald']) self.cli_set(base_path + ['name', cont_name, 'allow-host-cgroups']) self.cli_set(base_path + ['name', cont_name, 'stop-timeout', '1']) self.cli_set(base_path + ['name', cont_name, 'environment', env_key, 'value', env_value]) self.cli_set(base_path + ['name', cont_name, 'environment', env_key1, 'value', env_value1]) # commit changes self.cli_commit() self.assertTrue(self.is_running(cont_name)) # verify tmp = cmdl(['podman', 'exec', '-it', cont_name, 'sysctl', 'kernel.msgmax'], sudo=True) self.assertEqual(tmp, 'kernel.msgmax = 4096') l = cmd_to_json(['container', 'inspect', cont_name]) self.assertEqual(l['HostConfig']['LogConfig']['Type'], 'journald') self.assertEqual(l['Config']['Healthcheck']['Test'], ['NONE']) self.assertEqual(l['HostConfig']['CgroupMode'], 'host') self.assertIn(f'{env_key}={env_value}', l['Config']['Env']) self.assertIn(f'{env_key1}={env_value1}', l['Config']['Env']) # cleanup self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address']) self.cli_delete(['protocols', 'static']) self.cli_delete(['system', 'name-server']) def test_healthcheck(self): cont_name = 'health-test' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'stop-timeout', '1']) self.cli_set(base_path + ['name', cont_name, 'health-check', 'command', 'true']) self.cli_set(base_path + ['name', cont_name, 'health-check', 'interval', '10']) self.cli_set(base_path + ['name', cont_name, 'health-check', 'timeout', '1']) self.cli_set(base_path + ['name', cont_name, 'health-check', 'retry', '2']) self.cli_commit() l = cmd_to_json(['container', 'inspect', cont_name]) self.assertEqual(l['HostConfig']['LogConfig']['Type'], 'journald') self.assertEqual(l['Config']['Healthcheck']['Test'], ['CMD-SHELL', 'true']) self.assertEqual(l['Config']['Healthcheck']['Interval'], 10000000000) self.assertEqual(l['Config']['Healthcheck']['Timeout'], 1000000000) self.assertEqual(l['Config']['Healthcheck']['Retries'], 2) def test_name_server(self): cont_name = 'dns-test' net_name = 'net-test' name_servers = ['192.168.0.1', '192.168.0.2'] prefix = '192.0.2.0/24' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'stop-timeout', '1']) for name_server in name_servers: self.cli_set(base_path + ['name', cont_name, 'name-server', name_server]) self.cli_set( base_path + [ 'name', cont_name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2), ] ) # verify() - name server has no effect when container network has dns enabled with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['network', net_name, 'no-name-server']) self.cli_commit() n = cmd_to_json(['inspect', cont_name]) self.assertEqual(n['HostConfig']['Dns'], name_servers) tmp = cmdl(['podman', 'exec', '-it', cont_name, 'cat', '/etc/resolv.conf'], sudo=True) self.assertIn(name_server, tmp) def test_cpu_limit(self): cont_name = 'c2' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'cpu-quota', '1.25']) self.cli_set(base_path + ['name', cont_name, 'stop-timeout', '1']) self.cli_commit() self.assertTrue(self.is_running(cont_name)) def test_network_types(self): self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '100']) self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '101']) # MACVLAN Networks self.cli_set(base_path + ['network', 'macvlan1', 'prefix', '10.0.0.0/24']) self.cli_set(base_path + ['network', 'macvlan1', 'type', 'macvlan', 'parent', 'eth0']) self.cli_set(base_path + ['network', 'macvlan1', 'type', 'macvlan', 'mode', 'bridge']) self.cli_set(base_path + ['network', 'macvlan2', 'prefix', '10.0.100.0/24']) self.cli_set(base_path + ['network', 'macvlan2', 'gateway', '10.0.100.5']) self.cli_set(base_path + ['network', 'macvlan2', 'type', 'macvlan', 'parent', 'eth0.100']) self.cli_set(base_path + ['network', 'macvlan2', 'type', 'macvlan', 'mode', 'private']) self.cli_set(base_path + ['network', 'macvlan3', 'prefix', '2001::/64']) self.cli_set(base_path + ['network', 'macvlan3', 'type', 'macvlan', 'parent', 'eth0.101']) self.cli_set(base_path + ['network', 'macvlan3', 'type', 'macvlan', 'mode', 'vepa']) # Bridge Network self.cli_set(base_path + ['network', 'bridge1', 'prefix', '10.0.1.0/24']) self.cli_set(base_path + ['network', 'bridge1', 'type', 'bridge']) # Bridge Network before T7186; default network type is bridge self.cli_set(base_path + ['network', 'bridge2', 'prefix', '10.0.2.0/24']) self.cli_commit() # Force-start the network quadlets, as not mapped to containers for network in ['macvlan1', 'macvlan2', 'macvlan3', 'bridge1', 'bridge2']: systemd_unit = f'vyos-{network}-network.service' cmdl(['systemctl', 'start', systemd_unit], sudo=True) n = cmd_to_json(['network', 'inspect', 'macvlan1']) self.assertEqual(n['driver'], 'macvlan') self.assertEqual(n['network_interface'], 'eth0') self.assertEqual(n['options']['mode'], 'bridge') self.assertEqual(n['subnets'][0]['subnet'], '10.0.0.0/24') self.assertEqual(n['subnets'][0]['gateway'], '10.0.0.1') n = cmd_to_json(['network', 'inspect', 'macvlan2']) self.assertEqual(n['driver'], 'macvlan') self.assertEqual(n['network_interface'], 'eth0.100') self.assertEqual(n['options']['mode'], 'private') self.assertEqual(n['subnets'][0]['subnet'], '10.0.100.0/24') self.assertEqual(n['subnets'][0]['gateway'], '10.0.100.5') n = cmd_to_json(['network', 'inspect', 'macvlan3']) self.assertEqual(n['driver'], 'macvlan') self.assertEqual(n['network_interface'], 'eth0.101') self.assertEqual(n['options']['mode'], 'vepa') self.assertEqual(n['subnets'][0]['subnet'], '2001::/64') self.assertEqual(n['subnets'][0]['gateway'], '2001::1') n = cmd_to_json(['network', 'inspect', 'bridge1']) self.assertEqual(n['driver'], 'bridge') self.assertEqual(n['network_interface'], 'pod-bridge1') self.assertEqual(n['subnets'][0]['subnet'], '10.0.1.0/24') self.assertEqual(n['subnets'][0]['gateway'], '10.0.1.1') n = cmd_to_json(['network', 'inspect', 'bridge2']) self.assertEqual(n['driver'], 'bridge') self.assertEqual(n['network_interface'], 'pod-bridge2') self.assertEqual(n['subnets'][0]['subnet'], '10.0.2.0/24') self.assertEqual(n['subnets'][0]['gateway'], '10.0.2.1') # Cleanup self.cli_delete(['interfaces', 'ethernet', 'eth0', 'vif']) def test_user_defined_mac(self): # Bridge Network self.cli_set(base_path + ['network', 'bridge1', 'prefix', '10.0.1.0/24']) self.cli_set(base_path + ['network', 'bridge1', 'type', 'bridge']) self.cli_set(base_path + ['name', "test1", 'image', busybox_image]) self.cli_set(base_path + ['name', "test1", 'network', 'bridge1', 'address', '10.0.1.11']) self.cli_set(base_path + ['name', "test1", 'network', 'bridge1', 'mac', '02:00:00:00:00:01']) self.cli_set(base_path + ['name', "test1", 'stop-timeout', '1']) self.cli_set(base_path + ['name', "test2", 'image', busybox_image]) self.cli_set(base_path + ['name', "test2", 'network', 'bridge1', 'address', '10.0.1.12']) self.cli_set(base_path + ['name', "test2", 'network', 'bridge1', 'mac', '02:00:00:00:00:02']) self.cli_set(base_path + ['name', "test2", 'stop-timeout', '1']) self.cli_commit() n = cmd_to_json(['container', 'inspect', 'test1']) self.assertEqual(n['NetworkSettings']['Networks']['bridge1']['MacAddress'], '02:00:00:00:00:01') n = cmd_to_json(['container', 'inspect', 'test2']) self.assertEqual(n['NetworkSettings']['Networks']['bridge1']['MacAddress'], '02:00:00:00:00:02') def test_long_name_host_interface_uniqueness(self): # T7736: the deterministic host-side veth interface name derived # from a container name is truncated to fit IFNAMSIZ. Two distinct # but similarly-prefixed long names must not truncate to the same # interface name - Podman would then refuse to attach the second # container's network, and its systemd unit would fail to start. net_name = 'longiftest' prefix = '192.0.2.0/24' name_1 = 'abcdefghij-1' name_2 = 'abcdefghij-2' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) self.cli_set(base_path + ['name', name_1, 'image', busybox_image]) self.cli_set(base_path + ['name', name_1, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) self.cli_set(base_path + ['name', name_1, 'stop-timeout', '1']) self.cli_set(base_path + ['name', name_2, 'image', busybox_image]) self.cli_set(base_path + ['name', name_2, 'network', net_name, 'address', str(ip_interface(prefix).ip + 3)]) self.cli_set(base_path + ['name', name_2, 'stop-timeout', '1']) self.cli_commit() # Both containers run a "conmon" process at once, so checking by # process name alone can't distinguish which container it belongs # to - verify each container's own recorded PID is still alive for name in (name_1, name_2): self.assertTrue(self.is_running(name)) def test_colliding_host_interface_names(self): # T7736: the host-side veth name is "veth--" for # long container names - two distinct names can still (rarely) hash # to the same result. These two are a confirmed real collision # (both produce "veth-aaaa9-4ded") - verify() must reject the # commit with a clear error instead of leaving it to Podman to fail # obscurely when the second container's network attachment clashes # with the first's interface name. net_name = 'collidetest' prefix = '192.0.2.0/24' name_1 = 'aaaa9000005' name_2 = 'aaaa9000336' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) self.cli_set(base_path + ['name', name_1, 'image', busybox_image]) self.cli_set(base_path + ['name', name_1, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) self.cli_set(base_path + ['name', name_1, 'stop-timeout', '1']) self.cli_set(base_path + ['name', name_2, 'image', busybox_image]) self.cli_set(base_path + ['name', name_2, 'network', net_name, 'address', str(ip_interface(prefix).ip + 3)]) self.cli_set(base_path + ['name', name_2, 'stop-timeout', '1']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(base_path + ['name', name_2]) self.cli_commit() def test_ipv4_network(self): prefix = '192.0.2.0/24' base_name = 'ipv4' net_name = 'NET01' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) for ii in range(1, 6): name = f'{base_name}-{ii}' self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'stop-timeout', '1']) self.cli_set( base_path + [ 'name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii), ] ) # verify() - first IP address of a prefix cannot be used by a container with self.assertRaises(ConfigSessionError): self.cli_commit() tmp = f'{base_name}-1' self.cli_delete(base_path + ['name', tmp]) self.cli_commit() n = cmd_to_json(['network', 'inspect', net_name]) self.assertEqual(n['subnets'][0]['subnet'], prefix) # skip first container, it was never created for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(['container', 'inspect', name]) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['Gateway'], str(ip_interface(prefix).ip + 1), ) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['IPAddress'], str(ip_interface(prefix).ip + ii), ) def test_ipv6_network(self): prefix = '2001:db8::/64' base_name = 'ipv6' net_name = 'NET02' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) for ii in range(1, 6): name = f'{base_name}-{ii}' self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'stop-timeout', '1']) self.cli_set( base_path + [ 'name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii), ] ) # verify() - first IP address of a prefix cannot be used by a container with self.assertRaises(ConfigSessionError): self.cli_commit() tmp = f'{base_name}-1' self.cli_delete(base_path + ['name', tmp]) self.cli_commit() n = cmd_to_json(['network', 'inspect', net_name]) self.assertEqual(n['subnets'][0]['subnet'], prefix) # skip first container, it was never created for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(['container', 'inspect', name]) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'], str(ip_interface(prefix).ip + 1), ) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix).ip + ii), ) def test_dual_stack_network(self): prefix4 = '192.0.2.0/24' prefix6 = '2001:db8::/64' base_name = 'dual-stack' net_name = 'net-4-6' self.cli_set(base_path + ['network', net_name, 'prefix', prefix4]) self.cli_set(base_path + ['network', net_name, 'prefix', prefix6]) for ii in range(1, 6): name = f'{base_name}-{ii}' self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'stop-timeout', '1']) self.cli_set( base_path + [ 'name', name, 'network', net_name, 'address', str(ip_interface(prefix4).ip + ii), ] ) self.cli_set( base_path + [ 'name', name, 'network', net_name, 'address', str(ip_interface(prefix6).ip + ii), ] ) # verify() - first IP address of a prefix cannot be used by a container with self.assertRaises(ConfigSessionError): self.cli_commit() tmp = f'{base_name}-1' self.cli_delete(base_path + ['name', tmp]) self.cli_commit() n = cmd_to_json(['network', 'inspect', net_name]) self.assertEqual(n['subnets'][0]['subnet'], prefix4) self.assertEqual(n['subnets'][1]['subnet'], prefix6) # skip first container, it was never created for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(['container', 'inspect', name]) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'], str(ip_interface(prefix6).ip + 1), ) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix6).ip + ii), ) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['Gateway'], str(ip_interface(prefix4).ip + 1), ) self.assertEqual( c['NetworkSettings']['Networks'][net_name]['IPAddress'], str(ip_interface(prefix4).ip + ii), ) def test_no_name_server(self): prefix = '192.0.2.0/24' base_name = 'ipv4' net_name = 'NET01' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) self.cli_set(base_path + ['network', net_name, 'no-name-server']) name = f'{base_name}-2' self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'stop-timeout', '1']) self.cli_set( base_path + [ 'name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2), ] ) self.cli_commit() n = cmd_to_json(['network', 'inspect', net_name]) self.assertEqual(n['dns_enabled'], False) def test_network_mtu(self): prefix = '192.0.2.0/24' base_name = 'ipv4' net_name = 'NET01' self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) self.cli_set(base_path + ['network', net_name, 'mtu', '1280']) name = f'{base_name}-2' self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'stop-timeout', '1']) self.cli_set( base_path + [ 'name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2), ] ) self.cli_commit() n = cmd_to_json(['network', 'inspect', net_name]) self.assertEqual(n['options']['mtu'], '1280') def test_uid_gid(self): cont_name = 'uid-test' gid = '100' uid = '1001' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'gid', gid]) self.cli_set(base_path + ['name', cont_name, 'stop-timeout', '1']) # verify() - GID can only be set if UID is set with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['name', cont_name, 'uid', uid]) self.cli_commit() # verify tmp = cmdl(['podman', 'exec', '-it', cont_name, 'id', '-u'], sudo=True) self.assertEqual(tmp, uid) tmp = cmdl(['podman', 'exec', '-it', cont_name, 'id', '-g'], sudo=True) self.assertEqual(tmp, gid) def test_api_socket(self): base_name = 'api-test' container_list = range(1, 5) for ii in container_list: name = f'{base_name}-{ii}' self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'allow-host-networks']) self.cli_set(base_path + ['name', name, 'stop-timeout', '1']) self.cli_commit() # Query API about running containers tmp = cmdl( ['curl', '--unix-socket', '/run/podman/podman.sock', '-H', 'content-type: application/json', '-sf', 'http://localhost/containers/json'], sudo=True, ) tmp = json.loads(tmp) # We expect the same amount of containers from the API that we started above self.assertEqual(len(container_list), len(tmp)) def test_network_vrf(self): cont_name = 'vrf-test50' net_name = 'vrf-test50' vrf_name = 'red-15' # create temporary VRF for testing self.cli_set(['vrf', 'name', vrf_name, 'table', '100']) self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'network', net_name]) self.cli_set(base_path + ['name', cont_name, 'stop-timeout', '1']) self.cli_set(base_path + ['network', net_name, 'prefix', '192.168.0.0/24']) self.cli_set(base_path + ['network', net_name, 'vrf', vrf_name]) self.cli_commit() tmp = get_interface_vrf(f'pod-{net_name}') self.assertEqual(tmp, vrf_name) # Restart container and validate VRF assignment self.op_mode(['restart', 'container', cont_name]) tmp = get_interface_vrf(f'pod-{net_name}') self.assertEqual(tmp, vrf_name) self.cli_delete(['vrf', 'name', vrf_name]) if __name__ == '__main__': unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())