addinig vmi network outages (#1234)

Assisted By: Claude Code:
Assisted By: Claude Code:

Signed-off-by: Paige Patton <prubenda@redhat.com>
This commit is contained in:
Paige Patton
2026-05-14 14:34:19 -04:00
committed by GitHub
parent 7ec263f84c
commit 0e8af957f3
11 changed files with 1315 additions and 17 deletions
+5 -3
View File
@@ -167,13 +167,15 @@ class TestNodeInterfaceDownModule(unittest.TestCase):
@patch("krkn.scenario_plugins.network_chaos_ng.modules.node_interface_down.deploy_network_chaos_ng_pod")
@patch("krkn.scenario_plugins.network_chaos_ng.modules.node_interface_down.log_info")
def test_run_sleeps_test_duration(self, mock_log, mock_deploy, mock_sleep):
# test_duration is embedded in the shell command (sleep {n} && ip link set up),
# so no Python time.sleep(test_duration) should be called.
self.config.test_duration = 45
self.config.recovery_time = 0
self.module.run("worker-1")
sleep_values = [c[0][0] for c in mock_sleep.call_args_list]
self.assertIn(45, sleep_values)
self.assertNotIn(45, sleep_values)
@patch("krkn.scenario_plugins.network_chaos_ng.modules.node_interface_down.time.sleep")
@patch("krkn.scenario_plugins.network_chaos_ng.modules.node_interface_down.deploy_network_chaos_ng_pod")
@@ -185,7 +187,7 @@ class TestNodeInterfaceDownModule(unittest.TestCase):
self.module.run("worker-1")
sleep_values = [c[0][0] for c in mock_sleep.call_args_list]
self.assertIn(30, sleep_values)
self.assertNotIn(30, sleep_values)
self.assertIn(15, sleep_values)
@patch("krkn.scenario_plugins.network_chaos_ng.modules.node_interface_down.time.sleep")
@@ -198,7 +200,7 @@ class TestNodeInterfaceDownModule(unittest.TestCase):
self.module.run("worker-1")
sleep_values = [c[0][0] for c in mock_sleep.call_args_list]
self.assertIn(30, sleep_values)
self.assertNotIn(30, sleep_values)
self.assertNotIn(0, sleep_values)
@patch("krkn.scenario_plugins.network_chaos_ng.modules.node_interface_down.time.sleep")
+792
View File
@@ -0,0 +1,792 @@
#!/usr/bin/env python3
"""
Test suite for VmiNetworkFilterModule
Usage:
python -m unittest tests/test_vmi_network_filter.py -v
python -m coverage run -a -m unittest tests/test_vmi_network_filter.py -v
"""
import queue
import unittest
from unittest.mock import MagicMock, call, patch
from krkn.scenario_plugins.network_chaos_ng.models import (
NetworkChaosScenarioType,
NetworkFilterConfig,
)
from krkn.scenario_plugins.network_chaos_ng.modules.vmi_network_filter import (
VmiNetworkFilterModule,
)
MODULE = "krkn.scenario_plugins.network_chaos_ng.modules.vmi_network_filter"
def _make_config(**overrides):
defaults = dict(
id="vmi_network_filter",
image="quay.io/krkn-chaos/krkn-network-chaos:latest",
wait_duration=300,
test_duration=60,
label_selector="",
service_account="",
taints=[],
namespace="virt-density-udn-3",
instance_count=1,
execution="serial",
target=".*",
interfaces=[],
ingress=True,
egress=True,
ports=[],
protocols=["tcp", "udp"],
)
defaults.update(overrides)
return NetworkFilterConfig(**defaults)
def _make_container(name, ready=True, container_id="containerd://abc123"):
c = MagicMock()
c.name = name
c.ready = ready
c.containerId = container_id
return c
class TestVmiNetworkFilterModuleInit(unittest.TestCase):
def setUp(self):
self.mock_kubecli = MagicMock()
self.config = _make_config()
self.module = VmiNetworkFilterModule(self.config, self.mock_kubecli)
def test_initialization(self):
self.assertEqual(self.module.config, self.config)
self.assertEqual(self.module.kubecli, self.mock_kubecli)
self.assertEqual(self.module.base_network_config, self.config)
def test_get_config(self):
scenario_type, config = self.module.get_config()
self.assertEqual(scenario_type, NetworkChaosScenarioType.VMI)
self.assertEqual(config, self.config)
class TestVmiNetworkFilterModuleGetTargets(unittest.TestCase):
def setUp(self):
self.mock_kubecli = MagicMock()
self.mock_kubernetes = MagicMock()
self.mock_kubecli.get_lib_kubernetes.return_value = self.mock_kubernetes
self.config = _make_config(
namespace="virt-density-udn-3",
target="virt-server-.*",
)
self.module = VmiNetworkFilterModule(self.config, self.mock_kubecli)
def test_get_targets_success(self):
vmis = [
{"metadata": {"name": "virt-server-1", "namespace": "virt-density-udn-3"}},
{"metadata": {"name": "virt-server-2", "namespace": "virt-density-udn-3"}},
]
self.mock_kubernetes.get_vmis.return_value = vmis
result = self.module.get_targets()
self.assertEqual(
result,
[
"virt-density-udn-3/virt-server-1",
"virt-density-udn-3/virt-server-2",
],
)
self.mock_kubernetes.get_vmis.assert_called_once_with(
"virt-server-.*", "virt-density-udn-3", label_selector=None
)
def test_get_targets_no_namespace_raises(self):
self.config.namespace = None
with self.assertRaises(Exception) as ctx:
self.module.get_targets()
self.assertIn("namespace not specified", str(ctx.exception))
def test_get_targets_no_vmis_returns_empty(self):
self.mock_kubernetes.get_vmis.return_value = []
result = self.module.get_targets()
self.assertEqual(result, [])
def test_get_targets_regex_filters_name(self):
self.config.target = "virt-server-1"
vmis = [
{"metadata": {"name": "virt-server-1", "namespace": "virt-density-udn-3"}},
{"metadata": {"name": "virt-server-10", "namespace": "virt-density-udn-3"}},
]
self.mock_kubernetes.get_vmis.return_value = vmis
result = self.module.get_targets()
# re.match("virt-server-1", "virt-server-10") matches (prefix), both included
self.assertIn("virt-density-udn-3/virt-server-1", result)
def test_get_targets_regex_filters_namespace(self):
self.config.namespace = "virt-density-udn-3"
vmis = [
{"metadata": {"name": "virt-server-1", "namespace": "virt-density-udn-3"}},
{"metadata": {"name": "virt-server-2", "namespace": "other-namespace"}},
]
self.mock_kubernetes.get_vmis.return_value = vmis
result = self.module.get_targets()
self.assertIn("virt-density-udn-3/virt-server-1", result)
self.assertNotIn("other-namespace/virt-server-2", result)
def test_get_targets_passes_label_selector(self):
self.config.label_selector = "app=myapp"
self.mock_kubernetes.get_vmis.return_value = []
self.module.get_targets()
self.mock_kubernetes.get_vmis.assert_called_once_with(
"virt-server-.*", "virt-density-udn-3", label_selector="app=myapp"
)
def test_get_targets_empty_label_selector_passes_none(self):
self.config.label_selector = ""
self.mock_kubernetes.get_vmis.return_value = []
self.module.get_targets()
self.mock_kubernetes.get_vmis.assert_called_once_with(
"virt-server-.*", "virt-density-udn-3", label_selector=None
)
class TestVmiNetworkFilterModuleRun(unittest.TestCase):
def setUp(self):
self.mock_kubecli = MagicMock()
self.mock_kubernetes = MagicMock()
self.mock_kubecli.get_lib_kubernetes.return_value = self.mock_kubernetes
self.config = _make_config(
namespace="virt-density-udn-.*",
target="virt-server-.*",
test_duration=60,
interfaces=[],
)
self.module = VmiNetworkFilterModule(self.config, self.mock_kubecli)
# common happy-path mocks
self.mock_kubernetes.get_vmi.return_value = {
"status": {"nodeName": "worker-1"}
}
self.mock_kubernetes.list_pods.return_value = [
"virt-launcher-virt-server-3-abc12"
]
compute = _make_container("compute", ready=True, container_id="containerd://deadbeef")
virt_launcher = _make_container("virt-launcher", ready=False, container_id="")
mock_pod_info = MagicMock()
mock_pod_info.containers = [virt_launcher, compute]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
self.mock_kubernetes.get_pod_pids.return_value = ["100", "101", "102"]
# ------------------------------------------------------------------ helpers
def _patch_run(self):
"""Return a context-manager stack that patches all external calls in run."""
return [
patch(f"{MODULE}.deploy_network_chaos_ng_pod"),
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"),
patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0"),
patch(f"{MODULE}.apply_tc_vmi_chaos"),
patch(f"{MODULE}.clean_tc_vmi_chaos"),
patch(f"{MODULE}.time.sleep"),
patch(f"{MODULE}.log_info"),
patch(f"{MODULE}.log_error"),
]
# ------------------------------------------------------------------ success
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_success(
self,
mock_log,
mock_sleep,
mock_deploy,
mock_find_pid,
mock_tap,
mock_apply,
mock_clean,
):
self.module.run("virt-density-udn-3/virt-server-3")
mock_deploy.assert_called_once()
mock_find_pid.assert_called_once()
mock_tap.assert_called_once()
mock_apply.assert_called_once()
mock_sleep.assert_called_once_with(60)
mock_clean.assert_called_once()
self.mock_kubernetes.delete_pod.assert_called_once()
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_uses_resolved_namespace_not_regex(
self,
mock_log,
mock_sleep,
mock_deploy,
mock_find_pid,
mock_tap,
mock_apply,
mock_clean,
):
"""Kubernetes calls must use the real namespace, not the regex pattern."""
self.module.run("virt-density-udn-3/virt-server-3")
# get_vmi called with the resolved namespace
self.mock_kubernetes.get_vmi.assert_called_once_with(
"virt-server-3", "virt-density-udn-3"
)
# deploy called with scoped_config (namespace = resolved), not regex
deploy_config = mock_deploy.call_args[0][0]
self.assertEqual(deploy_config.namespace, "virt-density-udn-3")
self.assertNotEqual(deploy_config.namespace, "virt-density-udn-.*")
# ------------------------------------------------------------------ vmi not found
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_vmi_not_found_raises(self, mock_log, mock_deploy):
self.mock_kubernetes.get_vmi.return_value = None
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("not found", str(ctx.exception))
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_vmi_no_node_raises(self, mock_log, mock_deploy):
self.mock_kubernetes.get_vmi.return_value = {"status": {}}
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("unable to determine node", str(ctx.exception))
# ------------------------------------------------------------------ virt-launcher pod
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_no_virt_launcher_pod_raises(self, mock_log, mock_deploy):
self.mock_kubernetes.list_pods.return_value = []
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("no virt-launcher pod found", str(ctx.exception))
# ------------------------------------------------------------------ compute container
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_no_pod_info_raises(self, mock_log, mock_deploy):
self.mock_kubernetes.get_pod_info.return_value = None
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("impossible to retrieve info", str(ctx.exception))
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_compute_not_ready_raises(self, mock_log, mock_deploy):
compute = _make_container("compute", ready=False, container_id="containerd://abc")
mock_pod_info = MagicMock()
mock_pod_info.containers = [compute]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("compute container", str(ctx.exception))
self.assertIn("not ready", str(ctx.exception))
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_no_compute_container_raises(self, mock_log, mock_deploy):
other = _make_container("virt-launcher", ready=True, container_id="containerd://abc")
mock_pod_info = MagicMock()
mock_pod_info.containers = [other]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("compute container", str(ctx.exception))
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_strips_container_id_prefix(self, mock_log, mock_deploy, mock_find):
"""containerd:// prefix must be stripped before passing to get_pod_pids."""
compute = _make_container(
"compute", ready=True, container_id="containerd://deadbeef123"
)
mock_pod_info = MagicMock()
mock_pod_info.containers = [compute]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
self.mock_kubernetes.get_pod_pids.return_value = ["100"]
with patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0"), \
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])), \
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
patch(f"{MODULE}.time.sleep"):
self.module.run("virt-density-udn-3/virt-server-3")
call_kwargs = self.mock_kubernetes.get_pod_pids.call_args[1]
self.assertEqual(call_kwargs["pod_container_id"], "deadbeef123")
# ------------------------------------------------------------------ pids / netns
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_no_pids_raises(self, mock_log, mock_deploy):
self.mock_kubernetes.get_pod_pids.return_value = None
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("impossible to resolve PIDs", str(ctx.exception))
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value=None)
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_no_netns_pid_raises(self, mock_log, mock_deploy, mock_find):
with self.assertRaises(Exception) as ctx:
self.module.run("virt-density-udn-3/virt-server-3")
self.assertIn("could not find a PID", str(ctx.exception))
# ------------------------------------------------------------------ error queue
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_error_queue_captures_exception(self, mock_log, mock_deploy):
self.mock_kubernetes.get_vmi.return_value = None
error_queue = queue.Queue()
self.module.run("virt-density-udn-3/virt-server-3", error_queue)
self.assertFalse(error_queue.empty())
self.assertIn("not found", error_queue.get())
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_no_error_queue_raises_directly(
self, mock_log, mock_sleep, mock_deploy, mock_find, mock_tap, mock_apply, mock_clean
):
mock_apply.side_effect = RuntimeError("tc failed")
with self.assertRaises(RuntimeError):
self.module.run("virt-density-udn-3/virt-server-3")
# ------------------------------------------------------------------ apply / clean called correctly
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_apply_and_clean_called_with_tap_and_pid(
self, mock_log, mock_sleep, mock_deploy, mock_find, mock_tap, mock_apply, mock_clean
):
self.module.run("virt-density-udn-3/virt-server-3")
apply_args = mock_apply.call_args[0]
self.assertEqual(apply_args[3], "101") # pid
self.assertEqual(apply_args[4], "tap0") # iface
clean_args = mock_clean.call_args[0]
self.assertEqual(clean_args[3], "101") # pid
self.assertEqual(clean_args[4], "tap0") # iface
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_chaos_pod_deleted_after_clean(
self, mock_log, mock_sleep, mock_deploy, mock_find, mock_tap, mock_apply, mock_clean
):
"""Chaos pod must be cleaned up even after a successful run."""
self.module.run("virt-density-udn-3/virt-server-3")
self.mock_kubernetes.delete_pod.assert_called_once()
# namespace passed to delete_pod must be the resolved one
delete_ns = self.mock_kubernetes.delete_pod.call_args[0][1]
self.assertEqual(delete_ns, "virt-density-udn-3")
class TestVmiNetworkFilterModuleRollback(unittest.TestCase):
def setUp(self):
self.mock_kubecli = MagicMock()
self.mock_kubernetes = MagicMock()
self.mock_kubecli.get_lib_kubernetes.return_value = self.mock_kubernetes
self.config = _make_config(
namespace="virt-density-udn-3",
target="virt-server-.*",
test_duration=60,
interfaces=[],
)
self.module = VmiNetworkFilterModule(self.config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = {
"status": {"nodeName": "worker-1"}
}
self.mock_kubernetes.list_pods.return_value = [
"virt-launcher-virt-server-3-abc12"
]
compute = _make_container("compute", ready=True, container_id="containerd://deadbeef")
mock_pod_info = MagicMock()
mock_pod_info.containers = [compute]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
self.mock_kubernetes.get_pod_pids.return_value = ["100", "101", "102"]
# ------------------------------------------------------------------ _rollback directly
def test_rollback_calls_clean_then_delete_when_tc_applied(self):
input_rules = ["nsenter ... iptables -I INPUT 1 -i tap0 -p tcp -j DROP"]
output_rules = ["nsenter ... iptables -I OUTPUT 1 -p tcp -j DROP"]
with patch(f"{MODULE}.clean_tc_vmi_chaos") as mock_clean:
self.module._rollback("ns", "chaos-pod", "101", "tap0", input_rules, output_rules)
mock_clean.assert_called_once_with(
self.mock_kubernetes, "chaos-pod", "ns", "101", "tap0", input_rules, output_rules
)
self.mock_kubernetes.delete_pod.assert_called_once_with("chaos-pod", "ns")
def test_rollback_skips_clean_when_no_rules(self):
with patch(f"{MODULE}.clean_tc_vmi_chaos") as mock_clean:
self.module._rollback("ns", "chaos-pod")
mock_clean.assert_not_called()
self.mock_kubernetes.delete_pod.assert_called_once_with("chaos-pod", "ns")
def test_rollback_skips_clean_when_rules_none_but_pid_set(self):
"""Chaos pod deployed, netns_pid found, but rules never applied: skip clean."""
with patch(f"{MODULE}.clean_tc_vmi_chaos") as mock_clean:
self.module._rollback("ns", "chaos-pod", "101", "tap0", None, None)
mock_clean.assert_not_called()
self.mock_kubernetes.delete_pod.assert_called_once_with("chaos-pod", "ns")
# ------------------------------------------------------------------ rollback from run: error before tc
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_rollback_deletes_pod_on_error_before_tc(
self, mock_log, mock_deploy, mock_clean
):
"""Pod deployed but setup fails before tc is applied: delete only, no clean."""
self.mock_kubernetes.get_pod_info.return_value = None
with self.assertRaises(Exception):
self.module.run("virt-density-udn-3/virt-server-3")
self.mock_kubernetes.delete_pod.assert_called_once()
mock_clean.assert_not_called()
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.log_info")
def test_run_rollback_does_not_clean_when_no_netns_pid(
self, mock_log, mock_deploy, mock_clean
):
"""No netns_pid resolved means tc was never applied: clean must not be called."""
with patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value=None):
with self.assertRaises(Exception):
self.module.run("virt-density-udn-3/virt-server-3")
mock_clean.assert_not_called()
self.mock_kubernetes.delete_pod.assert_called_once()
# ------------------------------------------------------------------ rollback from run: error after tc
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=(["in_rule"], ["out_rule"]))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_rollback_cleans_tc_and_deletes_pod_on_error_after_tc(
self, mock_log, mock_sleep, mock_deploy, mock_find, mock_tap, mock_apply, mock_clean
):
"""If interrupted after tc is applied, both clean and delete must be called."""
mock_sleep.side_effect = RuntimeError("interrupted")
with self.assertRaises(RuntimeError):
self.module.run("virt-density-udn-3/virt-server-3")
mock_clean.assert_called_once()
self.mock_kubernetes.delete_pod.assert_called_once()
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=(["in_rule"], ["out_rule"]))
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@patch(f"{MODULE}.time.sleep")
@patch(f"{MODULE}.log_info")
def test_run_rollback_passes_correct_pid_iface_and_rules_to_clean(
self, mock_log, mock_sleep, mock_deploy, mock_find, mock_tap, mock_apply, mock_clean
):
"""Clean must receive the resolved netns_pid, iface, and the rules returned by apply."""
mock_sleep.side_effect = RuntimeError("interrupted")
with self.assertRaises(RuntimeError):
self.module.run("virt-density-udn-3/virt-server-3")
clean_args = mock_clean.call_args[0]
self.assertEqual(clean_args[3], "101") # netns_pid
self.assertEqual(clean_args[4], "tap0") # iface
self.assertEqual(clean_args[5], ["in_rule"]) # input_rules from apply
self.assertEqual(clean_args[6], ["out_rule"]) # output_rules from apply
class TestVmiNetworkFilterNetworkMode(unittest.TestCase):
def setUp(self):
self.mock_kubecli = MagicMock()
self.mock_kubernetes = MagicMock()
self.mock_kubecli.get_lib_kubernetes.return_value = self.mock_kubernetes
self.mock_kubernetes.list_pods.return_value = ["virt-launcher-virt-server-3-abc12"]
compute = _make_container("compute", ready=True, container_id="containerd://deadbeef")
mock_pod_info = MagicMock()
mock_pod_info.containers = [compute]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
self.mock_kubernetes.get_pod_pids.return_value = ["100", "101", "102"]
def _vmi_with_mode(self, mode: str) -> dict:
binding = {mode: {}}
return {
"status": {"nodeName": "worker-1"},
"spec": {"domain": {"devices": {"interfaces": [{"name": "default", **binding}]}}},
}
def _run_success(self, vmi: dict, **config_overrides):
config = _make_config(**config_overrides)
module = VmiNetworkFilterModule(config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = vmi
with patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"), \
patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0"), \
patch(f"{MODULE}.get_vmi_masquerade_interface", return_value="eth0"), \
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])) as mock_apply, \
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
patch(f"{MODULE}.time.sleep"), \
patch(f"{MODULE}.log_info"):
module.run("virt-density-udn-3/virt-server-3")
return mock_apply
# ------------------------------------------------------------------ mode detection
def test_bridge_mode_uses_tap_interface(self):
with patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0") as mock_tap, \
patch(f"{MODULE}.get_vmi_masquerade_interface") as mock_masq, \
patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"), \
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])), \
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
patch(f"{MODULE}.time.sleep"), \
patch(f"{MODULE}.log_info"):
config = _make_config()
module = VmiNetworkFilterModule(config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = self._vmi_with_mode("bridge")
module.run("virt-density-udn-3/virt-server-3")
mock_tap.assert_called_once()
mock_masq.assert_not_called()
def test_masquerade_mode_uses_default_interface(self):
with patch(f"{MODULE}.get_vmi_tap_interface") as mock_tap, \
patch(f"{MODULE}.get_vmi_masquerade_interface", return_value="eth0") as mock_masq, \
patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"), \
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])), \
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
patch(f"{MODULE}.time.sleep"), \
patch(f"{MODULE}.log_info"):
config = _make_config()
module = VmiNetworkFilterModule(config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = self._vmi_with_mode("masquerade")
module.run("virt-density-udn-3/virt-server-3")
mock_masq.assert_called_once()
mock_tap.assert_not_called()
def test_masquerade_mode_apply_called_with_eth0(self):
mock_apply = self._run_success(self._vmi_with_mode("masquerade"))
iface_arg = mock_apply.call_args[0][4]
self.assertEqual(iface_arg, "eth0")
def test_bridge_mode_apply_called_with_tap0(self):
mock_apply = self._run_success(self._vmi_with_mode("bridge"))
iface_arg = mock_apply.call_args[0][4]
self.assertEqual(iface_arg, "tap0")
def test_sriov_mode_raises(self):
config = _make_config()
module = VmiNetworkFilterModule(config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = self._vmi_with_mode("sriov")
with patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.log_info"):
with self.assertRaises(Exception) as ctx:
module.run("virt-density-udn-3/virt-server-3")
self.assertIn("sriov", str(ctx.exception))
self.assertIn("not supported", str(ctx.exception))
def test_macvtap_mode_raises(self):
config = _make_config()
module = VmiNetworkFilterModule(config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = self._vmi_with_mode("macvtap")
with patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.log_info"):
with self.assertRaises(Exception) as ctx:
module.run("virt-density-udn-3/virt-server-3")
self.assertIn("macvtap", str(ctx.exception))
self.assertIn("not supported", str(ctx.exception))
def test_explicit_interface_skips_mode_detection(self):
"""If interfaces is set in config, neither tap nor masquerade detection is called."""
with patch(f"{MODULE}.get_vmi_tap_interface") as mock_tap, \
patch(f"{MODULE}.get_vmi_masquerade_interface") as mock_masq, \
patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"), \
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])) as mock_apply, \
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
patch(f"{MODULE}.time.sleep"), \
patch(f"{MODULE}.log_info"):
config = _make_config(interfaces=["bond0"])
module = VmiNetworkFilterModule(config, self.mock_kubecli)
self.mock_kubernetes.get_vmi.return_value = self._vmi_with_mode("masquerade")
module.run("virt-density-udn-3/virt-server-3")
mock_tap.assert_not_called()
mock_masq.assert_not_called()
iface_arg = mock_apply.call_args[0][4]
self.assertEqual(iface_arg, "bond0")
def test_no_mode_in_spec_defaults_to_bridge(self):
"""VMI with no binding in spec should be treated as bridge mode."""
vmi = {"status": {"nodeName": "worker-1"}, "spec": {}}
mock_apply = self._run_success(vmi)
iface_arg = mock_apply.call_args[0][4]
self.assertEqual(iface_arg, "tap0")
class TestVmiNetworkFilterPortsProtocols(unittest.TestCase):
def setUp(self):
self.mock_kubecli = MagicMock()
self.mock_kubernetes = MagicMock()
self.mock_kubecli.get_lib_kubernetes.return_value = self.mock_kubernetes
self.mock_kubernetes.get_vmi.return_value = {"status": {"nodeName": "worker-1"}}
self.mock_kubernetes.list_pods.return_value = ["virt-launcher-virt-server-3-abc12"]
compute = _make_container("compute", ready=True, container_id="containerd://deadbeef")
mock_pod_info = MagicMock()
mock_pod_info.containers = [compute]
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
self.mock_kubernetes.get_pod_pids.return_value = ["100", "101", "102"]
def _run_and_capture_apply(self, **config_overrides):
config = _make_config(**config_overrides)
module = VmiNetworkFilterModule(config, self.mock_kubecli)
with patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"), \
patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0"), \
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])) as mock_apply, \
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
patch(f"{MODULE}.time.sleep"), \
patch(f"{MODULE}.log_info"):
module.run("virt-density-udn-3/virt-server-3")
return mock_apply
def test_apply_receives_specific_ports(self):
mock_apply = self._run_and_capture_apply(ports=[53, 80, 443])
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.ports, [53, 80, 443])
def test_apply_receives_empty_ports_for_all_traffic(self):
mock_apply = self._run_and_capture_apply(ports=[])
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.ports, [])
def test_apply_receives_tcp_only_protocol(self):
mock_apply = self._run_and_capture_apply(protocols=["tcp"])
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.protocols, ["tcp"])
def test_apply_receives_udp_only_protocol(self):
mock_apply = self._run_and_capture_apply(protocols=["udp"])
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.protocols, ["udp"])
def test_apply_receives_both_protocols(self):
mock_apply = self._run_and_capture_apply(protocols=["tcp", "udp"])
config_arg = mock_apply.call_args[0][5]
self.assertIn("tcp", config_arg.protocols)
self.assertIn("udp", config_arg.protocols)
def test_apply_receives_dns_ports_with_both_protocols(self):
"""DNS blackout: port 53 on tcp and udp."""
mock_apply = self._run_and_capture_apply(ports=[53], protocols=["tcp", "udp"])
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.ports, [53])
self.assertIn("tcp", config_arg.protocols)
self.assertIn("udp", config_arg.protocols)
def test_apply_receives_management_ports(self):
"""Management plane loss: SSH + HTTPS + k8s API."""
mock_apply = self._run_and_capture_apply(ports=[22, 443, 6443], protocols=["tcp"])
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.ports, [22, 443, 6443])
self.assertEqual(config_arg.protocols, ["tcp"])
def test_apply_config_has_resolved_namespace_not_regex(self):
"""The config passed to apply must use the real namespace from the target string."""
mock_apply = self._run_and_capture_apply(namespace="virt-density-udn-.*")
config_arg = mock_apply.call_args[0][5]
self.assertEqual(config_arg.namespace, "virt-density-udn-3")
self.assertNotEqual(config_arg.namespace, "virt-density-udn-.*")
if __name__ == "__main__":
unittest.main()