154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from ipaddress import IPv4Address, IPv4Network
|
|
|
|
from uvm.config import Settings
|
|
from uvm.domain import VmRecord
|
|
from uvm.errors import UvmError, ValidationError
|
|
from uvm.network import NetworkManager
|
|
from uvm.system import CommandResult
|
|
|
|
|
|
class FakeRunner:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, ...]] = []
|
|
self.existing_links: set[str] = set()
|
|
self.link_details: dict[str, str] = {}
|
|
|
|
def run(self, command, *, check=True, capture=False, timeout=None) -> CommandResult:
|
|
del check, capture, timeout
|
|
args = tuple(str(part) for part in command)
|
|
self.calls.append(args)
|
|
if args[:3] == ("ip", "link", "show") and args[3] in self.existing_links:
|
|
return CommandResult(args=args, returncode=0)
|
|
if args[:6] == ("ip", "-j", "-d", "link", "show", "dev"):
|
|
return CommandResult(args=args, returncode=0, stdout=self.link_details[args[6]])
|
|
if args[:4] == ("ip", "link", "show", "uvm0"):
|
|
return CommandResult(args=args, returncode=1)
|
|
if args == ("ip", "route", "show", "default"):
|
|
return CommandResult(args=args, returncode=0, stdout="default via 192.0.2.1 dev eth0\n")
|
|
if len(args) > 3 and args[0] == "iptables" and "-C" in args:
|
|
return CommandResult(args=args, returncode=1)
|
|
return CommandResult(args=args, returncode=0)
|
|
|
|
|
|
class NetworkManagerTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.runner = FakeRunner()
|
|
self.network = NetworkManager(Settings(), runner=self.runner) # type: ignore[arg-type]
|
|
|
|
def test_allocates_first_available_guest_address(self) -> None:
|
|
self.assertEqual(self.network.allocate_ip([], None), IPv4Address("10.42.0.2"))
|
|
|
|
def test_rejects_gateway_and_used_requested_addresses(self) -> None:
|
|
vm = VmRecord(
|
|
id="vm-test",
|
|
cpu=1,
|
|
ram_mib=512,
|
|
guest_ip="10.42.0.2",
|
|
gateway="10.42.0.1",
|
|
tap="uvm-test",
|
|
mac="02:fc:00:00:00:01",
|
|
socket="/tmp/firecracker.sock",
|
|
config="/tmp/config.json",
|
|
log="/tmp/firecracker.log",
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
self.network.allocate_ip([vm], IPv4Address("10.42.0.1"))
|
|
with self.assertRaises(ValidationError):
|
|
self.network.allocate_ip([vm], IPv4Address("10.42.0.2"))
|
|
with self.assertRaises(ValidationError):
|
|
self.network.allocate_ip([], IPv4Address("10.42.0.0"))
|
|
with self.assertRaises(ValidationError):
|
|
self.network.allocate_ip([], IPv4Address("10.42.0.255"))
|
|
|
|
def test_mac_and_tap_names_are_bounded_and_deterministic(self) -> None:
|
|
self.assertEqual(NetworkManager.mac_for(1), "02:fc:00:00:00:01")
|
|
tap = NetworkManager.tap_name("vm-0123456789abcdef")
|
|
self.assertLessEqual(len(tap), 15)
|
|
self.assertTrue(tap.startswith("uvm-"))
|
|
|
|
def test_bridge_setup_adds_a_missing_bridge_and_one_nat_rule(self) -> None:
|
|
self.network.ensure_bridge()
|
|
|
|
self.assertIn(("ip", "link", "add", "uvm0", "type", "bridge"), self.runner.calls)
|
|
self.assertIn(
|
|
("ip", "addr", "replace", "10.42.0.1/24", "dev", "uvm0"),
|
|
self.runner.calls,
|
|
)
|
|
self.assertIn(
|
|
(
|
|
"iptables",
|
|
"-A",
|
|
"FORWARD",
|
|
"-i",
|
|
"uvm0",
|
|
"-o",
|
|
"eth0",
|
|
"-s",
|
|
"10.42.0.0/24",
|
|
"-j",
|
|
"ACCEPT",
|
|
),
|
|
self.runner.calls,
|
|
)
|
|
checks = [
|
|
call
|
|
for call in self.runner.calls
|
|
if call[:5] == ("iptables", "-t", "nat", "-C", "POSTROUTING")
|
|
]
|
|
self.assertEqual(len(checks), 1)
|
|
self.assertIn(
|
|
(
|
|
"iptables",
|
|
"-t",
|
|
"nat",
|
|
"-A",
|
|
"POSTROUTING",
|
|
"-s",
|
|
"10.42.0.0/24",
|
|
"-o",
|
|
"eth0",
|
|
"-j",
|
|
"MASQUERADE",
|
|
),
|
|
self.runner.calls,
|
|
)
|
|
|
|
def test_bridge_and_guest_network_support_non_default_prefixes(self) -> None:
|
|
settings = Settings(
|
|
network=IPv4Network("10.50.0.0/16"),
|
|
gateway=IPv4Address("10.50.0.1"),
|
|
)
|
|
runner = FakeRunner()
|
|
NetworkManager(settings, runner=runner).ensure_bridge() # type: ignore[arg-type]
|
|
|
|
self.assertIn(
|
|
("ip", "addr", "replace", "10.50.0.1/16", "dev", "uvm0"),
|
|
runner.calls,
|
|
)
|
|
|
|
def test_refuses_to_adopt_an_existing_tap_device(self) -> None:
|
|
self.runner.existing_links.add("uvm-existing")
|
|
|
|
with self.assertRaises(UvmError):
|
|
self.network.create_tap("uvm-existing")
|
|
|
|
self.assertNotIn(
|
|
("ip", "tuntap", "add", "dev", "uvm-existing", "mode", "tap"),
|
|
self.runner.calls,
|
|
)
|
|
|
|
def test_refuses_to_reconfigure_an_existing_non_bridge_interface(self) -> None:
|
|
self.runner.existing_links.add("uvm0")
|
|
self.runner.link_details["uvm0"] = '[{"linkinfo": {"info_kind": "dummy"}}]'
|
|
|
|
with self.assertRaises(UvmError):
|
|
self.network.ensure_bridge()
|
|
|
|
self.assertNotIn(
|
|
("ip", "addr", "replace", "10.42.0.1/24", "dev", "uvm0"),
|
|
self.runner.calls,
|
|
)
|