adding changes to properly pass/fail a scenario if errors occur (#1065)

Signed-off-by: Paige Patton <prubenda@redhat.com>
This commit is contained in:
Paige Patton
2026-03-31 12:31:25 -04:00
committed by GitHub
parent 626e203d33
commit 35ee9d7bae
12 changed files with 572 additions and 73 deletions
@@ -26,22 +26,26 @@ class ManagedClusterScenarioPlugin(AbstractScenarioPlugin):
lib_telemetry.get_lib_kubernetes()
)
if managedcluster_scenario["actions"]:
for action in managedcluster_scenario["actions"]:
try:
self.inject_managedcluster_scenario(
action,
managedcluster_scenario,
managedcluster_scenario_object,
lib_telemetry.get_lib_kubernetes(),
)
except Exception as e:
logging.error(
"ManagedClusterScenarioPlugin exiting due to Exception %s"
% e
)
return 1
else:
return 0
try:
self.inject_managedcluster_scenario(
action,
managedcluster_scenario,
managedcluster_scenario_object,
lib_telemetry.get_lib_kubernetes(),
)
except Exception as e:
logging.error(
"ManagedClusterScenarioPlugin exiting due to Exception %s"
% e
)
return 1
else:
logging.error(
"ManagedClusterScenarioPlugin: 'actions' must be defined and non-empty in the scenario config"
)
return 1
return 0
def inject_managedcluster_scenario(
@@ -40,7 +40,9 @@ class ZoneOutageScenarioPlugin(AbstractScenarioPlugin):
start_time = int(time.time())
if cloud_type.lower() == "aws":
self.cloud_object = AWS()
self.network_based_zone(scenario_config)
result = self.network_based_zone(scenario_config)
if result != 0:
return 1
else:
kubecli = lib_telemetry.get_lib_kubernetes()
if cloud_type.lower() == "gcp":
@@ -171,70 +173,77 @@ class ZoneOutageScenarioPlugin(AbstractScenarioPlugin):
raise
def network_based_zone(self, scenario_config: dict[str, any]):
vpc_id = scenario_config["vpc_id"]
subnet_ids = scenario_config["subnet_id"]
duration = scenario_config["duration"]
# Add support for user-provided default network ACL
default_acl_id = scenario_config.get("default_acl_id")
ids = {}
acl_ids_created = []
for subnet_id in subnet_ids:
logging.info("Targeting subnet_id")
network_association_ids = []
associations, original_acl_id = self.cloud_object.describe_network_acls(
vpc_id, subnet_id
)
for entry in associations:
if entry["SubnetId"] == subnet_id:
network_association_ids.append(
entry["NetworkAclAssociationId"]
)
logging.info(
"Network association ids associated with "
"the subnet %s: %s" % (subnet_id, network_association_ids)
)
# Use provided default ACL if available, otherwise create a new one
if default_acl_id:
acl_id = default_acl_id
try:
vpc_id = scenario_config["vpc_id"]
subnet_ids = scenario_config["subnet_id"]
duration = scenario_config["duration"]
# Add support for user-provided default network ACL
default_acl_id = scenario_config.get("default_acl_id")
ids = {}
acl_ids_created = []
for subnet_id in subnet_ids:
logging.info("Targeting subnet_id")
network_association_ids = []
associations, original_acl_id = self.cloud_object.describe_network_acls(
vpc_id, subnet_id
)
for entry in associations:
if entry["SubnetId"] == subnet_id:
network_association_ids.append(
entry["NetworkAclAssociationId"]
)
logging.info(
"Using provided default ACL ID %s - this ACL will not be deleted after the scenario",
default_acl_id
"Network association ids associated with "
"the subnet %s: %s" % (subnet_id, network_association_ids)
)
# Don't add to acl_ids_created since we don't want to delete user-provided ACLs at cleanup
else:
acl_id = self.cloud_object.create_default_network_acl(vpc_id)
logging.info("Created new default ACL %s", acl_id)
acl_ids_created.append(acl_id)
new_association_id = self.cloud_object.replace_network_acl_association(
network_association_ids[0], acl_id
# Use provided default ACL if available, otherwise create a new one
if default_acl_id:
acl_id = default_acl_id
logging.info(
"Using provided default ACL ID %s - this ACL will not be deleted after the scenario",
default_acl_id
)
# Don't add to acl_ids_created since we don't want to delete user-provided ACLs at cleanup
else:
acl_id = self.cloud_object.create_default_network_acl(vpc_id)
logging.info("Created new default ACL %s", acl_id)
acl_ids_created.append(acl_id)
new_association_id = self.cloud_object.replace_network_acl_association(
network_association_ids[0], acl_id
)
# capture the orginal_acl_id, created_acl_id and
# new association_id to use during the recovery
ids[new_association_id] = original_acl_id
# wait for the specified duration
logging.info(
"Waiting for the specified duration " "in the config: %s" % duration
)
time.sleep(duration)
# capture the original_acl_id, created_acl_id and
# new association_id to use during the recovery
ids[new_association_id] = original_acl_id
# wait for the specified duration
logging.info(
"Waiting for the specified duration " "in the config: %s" % duration
)
time.sleep(duration)
# replace the applied acl with the previous acl in use
for new_association_id, original_acl_id in ids.items():
self.cloud_object.replace_network_acl_association(
new_association_id, original_acl_id
# replace the applied acl with the previous acl in use
for new_association_id, original_acl_id in ids.items():
self.cloud_object.replace_network_acl_association(
new_association_id, original_acl_id
)
logging.info(
"Wating for 60 seconds to make sure " "the changes are in place"
)
logging.info(
"Waiting for 60 seconds to make sure " "the changes are in place"
)
time.sleep(60)
time.sleep(60)
# delete the network acl created for the run
for acl_id in acl_ids_created:
self.cloud_object.delete_network_acl(acl_id)
# delete the network acl created for the run
for acl_id in acl_ids_created:
self.cloud_object.delete_network_acl(acl_id)
except Exception as e:
logging.error(
f"Network based zone outage scenario failed with exception: {e}"
)
return 1
return 0
def get_scenario_types(self) -> list[str]:
return ["zone_outages_scenarios"]
@@ -26,6 +26,10 @@ class TestApplicationOutageScenarioPlugin(unittest.TestCase):
"""
self.plugin = ApplicationOutageScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
+4
View File
@@ -26,6 +26,10 @@ class TestContainerScenarioPlugin(unittest.TestCase):
"""
self.plugin = ContainerScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
@@ -36,6 +36,187 @@ class TestManagedClusterScenarioPlugin(unittest.TestCase):
self.assertEqual(result, ["managedcluster_scenarios"])
self.assertEqual(len(result), 1)
@patch('time.time')
@patch('builtins.open', create=True)
@patch('yaml.full_load')
@patch('krkn.cerberus.get_status')
def test_run_multiple_actions_executes_all(self, mock_cerberus, mock_yaml, mock_open, mock_time):
"""
Test that run() executes all actions, not just the first one
This tests the fix for the early return bug
"""
mock_time.return_value = 1234567890
# Setup mock scenario config with multiple actions
mock_yaml.return_value = {
"managedcluster_scenarios": [
{
"actions": [
"managedcluster_start_scenario",
"managedcluster_stop_scenario",
"managedcluster_reboot_scenario"
],
"managedcluster_name": "test-cluster",
"runs": 1,
"instance_count": 1,
"timeout": 120
}
]
}
mock_lib_telemetry = Mock(spec=KrknTelemetryOpenshift)
mock_lib_telemetry.get_lib_kubernetes.return_value = self.mock_kubecli
mock_scenario_telemetry = Mock()
# Mock inject_managedcluster_scenario to track calls
call_tracker = []
original_inject = self.plugin.inject_managedcluster_scenario
def track_inject(action, *args, **kwargs):
call_tracker.append(action)
with patch.object(self.plugin, 'inject_managedcluster_scenario', side_effect=track_inject):
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert all three actions were called
self.assertEqual(result, 0)
self.assertEqual(len(call_tracker), 3)
self.assertIn("managedcluster_start_scenario", call_tracker)
self.assertIn("managedcluster_stop_scenario", call_tracker)
self.assertIn("managedcluster_reboot_scenario", call_tracker)
@patch('time.time')
@patch('builtins.open', create=True)
@patch('yaml.full_load')
def test_run_stops_on_first_error(self, mock_yaml, mock_open, mock_time):
"""
Test that run() returns 1 and stops executing on first error
"""
mock_time.return_value = 1234567890
# Setup mock scenario config with multiple actions
mock_yaml.return_value = {
"managedcluster_scenarios": [
{
"actions": [
"managedcluster_start_scenario",
"managedcluster_stop_scenario",
"managedcluster_reboot_scenario"
],
"managedcluster_name": "test-cluster",
"runs": 1,
"instance_count": 1,
"timeout": 120
}
]
}
mock_lib_telemetry = Mock(spec=KrknTelemetryOpenshift)
mock_lib_telemetry.get_lib_kubernetes.return_value = self.mock_kubecli
mock_scenario_telemetry = Mock()
# Mock inject_managedcluster_scenario to raise exception on first call
call_tracker = []
def track_inject_with_error(action, *args, **kwargs):
call_tracker.append(action)
if action == "managedcluster_start_scenario":
raise Exception("Test failure")
with patch.object(self.plugin, 'inject_managedcluster_scenario', side_effect=track_inject_with_error):
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert failure and only first action was attempted
self.assertEqual(result, 1)
self.assertEqual(len(call_tracker), 1)
self.assertEqual(call_tracker[0], "managedcluster_start_scenario")
@patch('builtins.open', create=True)
@patch('yaml.full_load')
def test_run_returns_error_when_actions_empty(self, mock_yaml, _mock_open):
"""
Test that run() returns 1 and logs an error when actions is an empty list
"""
mock_yaml.return_value = {
"managedcluster_scenarios": [
{
"actions": [],
"managedcluster_name": "test-cluster",
"runs": 1,
"instance_count": 1,
"timeout": 120
}
]
}
mock_lib_telemetry = Mock(spec=KrknTelemetryOpenshift)
mock_lib_telemetry.get_lib_kubernetes.return_value = self.mock_kubecli
mock_scenario_telemetry = Mock()
with self.assertLogs('root', level='ERROR') as log_ctx:
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry,
)
self.assertEqual(result, 1)
self.assertTrue(
any("actions" in msg for msg in log_ctx.output),
f"Expected 'actions' in error log, got: {log_ctx.output}",
)
@patch('builtins.open', create=True)
@patch('yaml.full_load')
def test_run_returns_error_when_actions_none(self, mock_yaml, _mock_open):
"""
Test that run() returns 1 and logs an error when actions is None
"""
mock_yaml.return_value = {
"managedcluster_scenarios": [
{
"actions": None,
"managedcluster_name": "test-cluster",
"runs": 1,
"instance_count": 1,
"timeout": 120
}
]
}
mock_lib_telemetry = Mock(spec=KrknTelemetryOpenshift)
mock_lib_telemetry.get_lib_kubernetes.return_value = self.mock_kubecli
mock_scenario_telemetry = Mock()
with self.assertLogs('root', level='ERROR') as log_ctx:
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry,
)
self.assertEqual(result, 1)
self.assertTrue(
any("actions" in msg for msg in log_ctx.output),
f"Expected 'actions' in error log, got: {log_ctx.output}",
)
class TestCommonFunctions(unittest.TestCase):
"""
+4
View File
@@ -26,6 +26,10 @@ class TestNativeScenarioPlugin(unittest.TestCase):
"""
self.plugin = NativeScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario types
+11
View File
@@ -30,6 +30,10 @@ class TestOPENSTACKCLOUD(unittest.TestCase):
"""Set up test fixtures"""
self.openstack = OPENSTACKCLOUD()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.openstack = None
def test_openstackcloud_init(self):
"""Test OPENSTACKCLOUD class initialization"""
self.assertEqual(self.openstack.Wait, 30)
@@ -276,6 +280,13 @@ class TestOpenstackNodeScenarios(unittest.TestCase):
affected_nodes_status=self.affected_nodes_status
)
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.scenario = None
self.kubecli = None
self.mock_openstack = None
self.affected_nodes_status = None
@patch('krkn.scenario_plugins.node_actions.common_node_functions.wait_for_ready_status')
def test_node_start_scenario_success(self, mock_wait_ready):
"""Test node start scenario successfully"""
@@ -26,6 +26,10 @@ class TestPodDisruptionScenarioPlugin(unittest.TestCase):
"""
self.plugin = PodDisruptionScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
+16
View File
@@ -32,6 +32,10 @@ class TestPvcScenarioPlugin(unittest.TestCase):
"""
self.plugin = PvcScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
@@ -49,6 +53,10 @@ class TestToKbytes(unittest.TestCase):
"""Set up test fixtures"""
self.plugin = PvcScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_to_kbytes_1ki(self):
"""Test to_kbytes with 1Ki"""
self.assertEqual(self.plugin.to_kbytes("1Ki"), 1)
@@ -120,6 +128,10 @@ class TestRemoveTempFile(unittest.TestCase):
"""Set up test fixtures"""
self.plugin = PvcScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_remove_temp_file_success(self):
"""Test successful removal of temp file"""
mock_kubecli = MagicMock(spec=KrknKubernetes)
@@ -231,6 +243,10 @@ class TestPvcScenarioPluginRun(unittest.TestCase):
"""Set up test fixtures"""
self.plugin = PvcScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def create_scenario_file(self, config: dict, temp_dir: str) -> str:
"""Helper to create a temporary scenario YAML file in the given directory"""
path = os.path.join(temp_dir, "scenario.yaml")
+12
View File
@@ -27,6 +27,10 @@ class TestSynFloodScenarioPlugin(unittest.TestCase):
"""
self.plugin = SynFloodScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
@@ -63,6 +67,10 @@ class TestIsNodeAffinityCorrect(unittest.TestCase):
def setUp(self):
self.plugin = SynFloodScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_valid_node_affinity(self):
"""Test valid node affinity configuration"""
valid_affinity = {
@@ -110,6 +118,10 @@ class TestParseConfig(unittest.TestCase):
def setUp(self):
self.plugin = SynFloodScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def _create_scenario_file(self, tmp_path, config=None):
"""Helper to create a temporary scenario YAML file"""
import yaml
@@ -26,6 +26,10 @@ class TestTimeActionsScenarioPlugin(unittest.TestCase):
"""
self.plugin = TimeActionsScenarioPlugin()
def tearDown(self):
"""Clean up after each test to prevent state leakage"""
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
@@ -50,6 +54,72 @@ class TestTimeActionsScenarioPlugin(unittest.TestCase):
logged_msg = mock_logging.error.call_args[0][0]
self.assertIn("disk quota exceeded", logged_msg)
self.assertNotIn("NameError", logged_msg)
@unittest.mock.patch('builtins.open', create=True)
@unittest.mock.patch('yaml.full_load')
@unittest.mock.patch('logging.error')
def test_run_exception_handling_with_variable(self, mock_logging_error, mock_yaml, mock_open):
"""
Test that run() properly captures exception variable and logs it
This tests the fix for the undefined variable 'e' bug
"""
# Setup mock to raise exception
mock_yaml.side_effect = RuntimeError("Test exception message")
mock_lib_telemetry = MagicMock()
mock_scenario_telemetry = MagicMock()
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert failure is returned
self.assertEqual(result, 1)
# Assert logging.error was called with the exception message
mock_logging_error.assert_called_once()
error_call_args = str(mock_logging_error.call_args)
self.assertIn("Test exception message", error_call_args)
self.assertIn("TimeActionsScenarioPlugin", error_call_args)
@unittest.mock.patch('builtins.open', create=True)
@unittest.mock.patch('yaml.full_load')
def test_run_with_skew_time_exception(self, mock_yaml, mock_open):
"""
Test that run() handles exceptions from skew_time method
"""
# Setup mock scenario config
mock_yaml.return_value = {
"time_scenarios": [
{
"action": "skew_time",
"object_type": "node",
"object_name": ["test-node"]
}
]
}
mock_lib_telemetry = MagicMock()
mock_kubecli = MagicMock()
mock_lib_telemetry.get_lib_kubernetes.return_value = mock_kubecli
# Make skew_time raise an exception
with unittest.mock.patch.object(self.plugin, 'skew_time', side_effect=Exception("Skew failed")):
mock_scenario_telemetry = MagicMock()
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert failure is returned
self.assertEqual(result, 1)
if __name__ == "__main__":
+180
View File
@@ -31,9 +31,20 @@ class TestZoneOutageScenarioPlugin(unittest.TestCase):
def setUp(self):
"""
Set up test fixtures for ZoneOutageScenarioPlugin
Creates a fresh plugin instance for each test to avoid state pollution
"""
self.plugin = ZoneOutageScenarioPlugin()
def tearDown(self):
"""
Clean up after each test to prevent state leakage between tests
"""
# Clear any cloud_object that might have been set
if hasattr(self.plugin, 'cloud_object'):
delattr(self.plugin, 'cloud_object')
# Create a completely fresh instance for the next test
self.plugin = None
def test_get_scenario_types(self):
"""
Test get_scenario_types returns correct scenario type
@@ -43,6 +54,175 @@ class TestZoneOutageScenarioPlugin(unittest.TestCase):
self.assertEqual(result, ["zone_outages_scenarios"])
self.assertEqual(len(result), 1)
@unittest.mock.patch('builtins.open', create=True)
@unittest.mock.patch('yaml.full_load')
@unittest.mock.patch('krkn.scenario_plugins.zone_outage.zone_outage_scenario_plugin.gcp_node_scenarios')
@unittest.mock.patch('krkn.cerberus.publish_kraken_status')
def test_run_propagates_node_based_zone_failure(self, mock_cerberus, mock_gcp_scenarios, mock_yaml, mock_open):
"""
Test that run() properly propagates failure from node_based_zone method
This tests the fix for the ignored return value bug
"""
# Setup mock scenario config for GCP
mock_yaml.return_value = {
"zone_outage": {
"cloud_type": "gcp",
"zone": "us-central1-a",
"duration": 60,
"timeout": 180,
"kube_check": True
}
}
mock_lib_telemetry = MagicMock()
mock_kubecli = MagicMock()
mock_lib_telemetry.get_lib_kubernetes.return_value = mock_kubecli
# Mock GCP scenarios
mock_gcp_instance = MagicMock()
mock_affected_nodes_status = MagicMock()
mock_affected_nodes_status.affected_nodes = []
mock_gcp_instance.affected_nodes_status = mock_affected_nodes_status
mock_gcp_scenarios.return_value = mock_gcp_instance
# Mock node_based_zone to return failure
with unittest.mock.patch.object(self.plugin, 'node_based_zone', return_value=1):
mock_scenario_telemetry = MagicMock()
mock_scenario_telemetry.affected_nodes = [] # Must be a list for .extend()
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert failure is properly propagated
self.assertEqual(result, 1)
@unittest.mock.patch('builtins.open', create=True)
@unittest.mock.patch('yaml.full_load')
@unittest.mock.patch('krkn.scenario_plugins.zone_outage.zone_outage_scenario_plugin.gcp_node_scenarios')
@unittest.mock.patch('krkn.cerberus.publish_kraken_status')
def test_run_succeeds_when_node_based_zone_succeeds(self, mock_cerberus, mock_gcp_scenarios, mock_yaml, mock_open):
"""
Test that run() returns 0 when node_based_zone succeeds
"""
# Setup mock scenario config for GCP
mock_yaml.return_value = {
"zone_outage": {
"cloud_type": "gcp",
"zone": "us-central1-a",
"duration": 60,
"timeout": 180,
"kube_check": True
}
}
mock_lib_telemetry = MagicMock()
mock_kubecli = MagicMock()
mock_lib_telemetry.get_lib_kubernetes.return_value = mock_kubecli
# Mock GCP scenarios
mock_gcp_instance = MagicMock()
mock_affected_nodes_status = MagicMock()
mock_affected_nodes_status.affected_nodes = []
mock_gcp_instance.affected_nodes_status = mock_affected_nodes_status
mock_gcp_scenarios.return_value = mock_gcp_instance
# Mock node_based_zone to return success
with unittest.mock.patch.object(self.plugin, 'node_based_zone', return_value=0):
mock_scenario_telemetry = MagicMock()
mock_scenario_telemetry.affected_nodes = [] # Must be a list for .extend()
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert success
self.assertEqual(result, 0)
@unittest.mock.patch('builtins.open', create=True)
@unittest.mock.patch('yaml.full_load')
@unittest.mock.patch('krkn.scenario_plugins.zone_outage.zone_outage_scenario_plugin.AWS')
@unittest.mock.patch('krkn.cerberus.publish_kraken_status')
def test_run_aws_network_based_zone(self, mock_cerberus, mock_aws_class, mock_yaml, mock_open):
"""
Test that run() handles AWS network-based zone outage correctly
"""
# Setup mock scenario config for AWS
mock_yaml.return_value = {
"zone_outage": {
"cloud_type": "aws",
"vpc_id": "vpc-12345",
"subnet_id": ["subnet-1", "subnet-2"],
"duration": 60
}
}
mock_aws_instance = MagicMock()
mock_aws_class.return_value = mock_aws_instance
# Mock the network_based_zone method to return success
with unittest.mock.patch.object(self.plugin, 'network_based_zone', return_value=0):
mock_lib_telemetry = MagicMock()
mock_scenario_telemetry = MagicMock()
mock_scenario_telemetry.affected_nodes = [] # Must be a list for .extend()
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert success
self.assertEqual(result, 0)
@unittest.mock.patch('builtins.open', create=True)
@unittest.mock.patch('yaml.full_load')
@unittest.mock.patch('krkn.scenario_plugins.zone_outage.zone_outage_scenario_plugin.AWS')
@unittest.mock.patch('krkn.cerberus.publish_kraken_status')
def test_run_aws_network_based_zone_failure(self, mock_cerberus, mock_aws_class, mock_yaml, mock_open):
"""
Test that run() properly propagates failure from network_based_zone method
"""
# Setup mock scenario config for AWS
mock_yaml.return_value = {
"zone_outage": {
"cloud_type": "aws",
"vpc_id": "vpc-12345",
"subnet_id": ["subnet-1", "subnet-2"],
"duration": 60
}
}
mock_aws_instance = MagicMock()
mock_aws_class.return_value = mock_aws_instance
# Mock the network_based_zone method to return failure
with unittest.mock.patch.object(self.plugin, 'network_based_zone', return_value=1):
mock_lib_telemetry = MagicMock()
mock_scenario_telemetry = MagicMock()
mock_scenario_telemetry.affected_nodes = [] # Must be a list for .extend()
# Execute the run method
result = self.plugin.run(
run_uuid="test-uuid",
scenario="test_scenario.yaml",
lib_telemetry=mock_lib_telemetry,
scenario_telemetry=mock_scenario_telemetry
)
# Assert failure is properly propagated
self.assertEqual(result, 1)
class TestRollbackGcpZoneOutage(unittest.TestCase):
"""Tests for the GCP zone outage rollback functionality"""