import logging import os import random from time import sleep import allure import pytest from frostfs_testlib.resources.wellknown_acl import PUBLIC_ACL from frostfs_testlib.s3 import AwsCliClient from frostfs_testlib.steps.cli.container import create_container from frostfs_testlib.steps.cli.object import ( get_object, get_object_nodes, neo_go_query_height, put_object, put_object_to_random_node, ) from frostfs_testlib.steps.storage_object import delete_objects from frostfs_testlib.storage.cluster import ClusterNode from frostfs_testlib.storage.controllers import ClusterStateController from frostfs_testlib.storage.dataclasses.object_size import ObjectSize from frostfs_testlib.storage.dataclasses.storage_object_info import Interfaces, StorageObjectInfo from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.utils.failover_utils import wait_all_storage_nodes_returned, wait_object_replication from frostfs_testlib.utils.file_utils import generate_file, get_file_hash logger = logging.getLogger("NeoLogger") STORAGE_NODE_COMMUNICATION_PORT = "8080" STORAGE_NODE_COMMUNICATION_PORT_TLS = "8082" PORTS_TO_BLOCK = [STORAGE_NODE_COMMUNICATION_PORT, STORAGE_NODE_COMMUNICATION_PORT_TLS] blocked_nodes: list[ClusterNode] = [] file_wait_delete = [] OBJECT_ATTRIBUTES = [ None, {"key1": 1, "key2": "abc", "common_key": "common_value"}, {"key1": 2, "common_key": "common_value"}, ] def pytest_generate_tests(metafunc: pytest.Metafunc): if "s3_client" in metafunc.fixturenames: metafunc.parametrize("s3_client", [AwsCliClient], indirect=True) @pytest.mark.failover @pytest.mark.failover_network class TestFailoverNetwork(ClusterTestBase): @pytest.fixture(autouse=True) @allure.title("Restore network") def restore_network(self, cluster_state_controller: ClusterStateController): yield with allure.step(f"Count blocked nodes {len(blocked_nodes)}"): not_empty = len(blocked_nodes) != 0 for node in list(blocked_nodes): with allure.step(f"Restore network for {node}"): cluster_state_controller.restore_traffic(mode="ports", node=node) blocked_nodes.remove(node) if not_empty: wait_all_storage_nodes_returned(self.shell, self.cluster) @pytest.fixture() @allure.title("Restore drop traffic to system") def restore_down_interfaces(self, cluster_state_controller: ClusterStateController): yield cluster_state_controller.restore_interfaces() @pytest.fixture() @allure.title("Delete file after test") def delete_file_after_test(self) -> None: yield for path in file_wait_delete: os.remove(path) file_wait_delete.clear() @pytest.fixture() def storage_objects( self, simple_object_size: ObjectSize, default_wallet: str, ) -> list[StorageObjectInfo]: file_path = generate_file(simple_object_size.value) file_hash = get_file_hash(file_path) with allure.step("Create container"): placement_rule = "REP 1 CBF 1" cid = create_container( wallet=default_wallet, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, rule=placement_rule, await_mode=True, ) storage_objects = [] with allure.step("Put object"): for attribute in OBJECT_ATTRIBUTES: oid = put_object_to_random_node( wallet=default_wallet, path=file_path, cid=cid, shell=self.shell, cluster=self.cluster, ) storage_object = StorageObjectInfo(cid=cid, oid=oid) storage_object.size = simple_object_size.value storage_object.wallet_file_path = default_wallet storage_object.file_path = file_path storage_object.file_hash = file_hash storage_object.attributes = attribute storage_objects.append(storage_object) yield storage_objects delete_objects(storage_objects, self.shell, self.cluster) @allure.title("Block Storage node traffic") def test_block_storage_node_traffic( self, default_wallet: str, require_multiple_hosts, simple_object_size: ObjectSize, cluster_state_controller: ClusterStateController, ): """ Block storage nodes traffic using iptables and wait for replication for objects. """ wallet = default_wallet placement_rule = "REP 2 IN X CBF 2 SELECT 2 FROM * AS X" wakeup_node_timeout = 10 # timeout to let nodes detect that traffic has blocked nodes_to_block_count = 2 source_file_path = generate_file(simple_object_size.value) cid = create_container( wallet, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, rule=placement_rule, basic_acl=PUBLIC_ACL, ) oid = put_object_to_random_node(wallet, source_file_path, cid, shell=self.shell, cluster=self.cluster) nodes = wait_object_replication(cid, oid, 2, shell=self.shell, nodes=self.cluster.storage_nodes) logger.info(f"Nodes are {nodes}") nodes_to_block = nodes if nodes_to_block_count > len(nodes): # TODO: the intent of this logic is not clear, need to revisit nodes_to_block = random.choices(nodes, k=2) excluded_nodes = [] for node in nodes_to_block: with allure.step(f"Block incoming traffic at node {node} on port {PORTS_TO_BLOCK}"): block_node = [ cluster_node for cluster_node in self.cluster.cluster_nodes if cluster_node.storage_node == node ] blocked_nodes.append(*block_node) excluded_nodes.append(node) cluster_state_controller.drop_traffic( mode="ports", node=node, wakeup_timeout=wakeup_node_timeout, ports=PORTS_TO_BLOCK, ) with allure.step(f"Check object is not stored on node {node}"): new_nodes = wait_object_replication( cid, oid, 2, shell=self.shell, nodes=list(set(self.cluster.storage_nodes) - set(excluded_nodes)), ) assert node not in new_nodes with allure.step("Check object data is not corrupted"): got_file_path = get_object(wallet, cid, oid, endpoint=new_nodes[0].get_rpc_endpoint(), shell=self.shell) assert get_file_hash(source_file_path) == get_file_hash(got_file_path) for node in nodes_to_block: with allure.step(f"Unblock incoming traffic at host {node} on port {PORTS_TO_BLOCK}"): cluster_state_controller.restore_traffic(mode="ports", node=node) block_node = [ cluster_node for cluster_node in self.cluster.cluster_nodes if cluster_node.storage_node == node ] blocked_nodes.remove(*block_node) sleep(wakeup_node_timeout) with allure.step("Check object data is not corrupted"): new_nodes = wait_object_replication(cid, oid, 2, shell=self.shell, nodes=self.cluster.storage_nodes) got_file_path = get_object(wallet, cid, oid, shell=self.shell, endpoint=new_nodes[0].get_rpc_endpoint()) assert get_file_hash(source_file_path) == get_file_hash(got_file_path) @pytest.mark.interfaces @allure.title("Block DATA interface node") def test_block_data_interface( self, cluster_state_controller: ClusterStateController, default_wallet: str, restore_down_interfaces: None, delete_file_after_test: None, storage_objects: list[StorageObjectInfo], ): storage_object = storage_objects[0] with allure.step("Search nodes with object"): nodes_with_object = get_object_nodes( cluster=self.cluster, wallet=default_wallet, cid=storage_object.cid, oid=storage_object.oid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, ) with allure.step("Get data interface to node"): config_interfaces = list(nodes_with_object[0].host.config.interfaces.keys()) with allure.step(f"Get data in {config_interfaces}"): data_interfaces = [interface for interface in config_interfaces if "data" in interface] with allure.step("Block data interfaces for node"): for interface in data_interfaces: cluster_state_controller.down_interface(nodes=nodes_with_object, interface=interface) with allure.step("Tick epoch and wait 2 block"): nodes_without_an_object = list(set(self.cluster.cluster_nodes) - set(nodes_with_object)) self.tick_epochs(1, alive_node=nodes_without_an_object[0].storage_node, wait_block=2) with allure.step("Get object for target nodes to data interfaces, expect false"): with pytest.raises(RuntimeError, match="return code: 1"): get_object( wallet=default_wallet, cid=storage_object.cid, oid=storage_object.oid, shell=self.shell, endpoint=nodes_with_object[0].storage_node.get_rpc_endpoint(), ) with allure.step(f"Get object others nodes, expect true"): input_file = get_object( wallet=default_wallet, cid=storage_object.cid, oid=storage_object.oid, shell=self.shell, endpoint=nodes_without_an_object[0].storage_node.get_rpc_endpoint(), ) file_wait_delete.append(input_file) with allure.step("Restore interface and tick 1 epoch, wait 2 block"): cluster_state_controller.restore_interfaces() self.tick_epochs(1, alive_node=nodes_without_an_object[0].storage_node, wait_block=2) @pytest.mark.interfaces @allure.title("Block INTERNAL interface node") def test_block_internal_interface( self, cluster_state_controller: ClusterStateController, default_wallet: str, restore_down_interfaces: None, delete_file_after_test: None, storage_objects: list[StorageObjectInfo], simple_object_size: ObjectSize, ): storage_object = storage_objects[0] with allure.step("Search nodes with object"): nodes_with_object = get_object_nodes( cluster=self.cluster, wallet=default_wallet, cid=storage_object.cid, oid=storage_object.oid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, ) with allure.step("Get internal interface to node"): config_interfaces = list(nodes_with_object[0].host.config.interfaces.keys()) with allure.step(f"Get internal in {config_interfaces}"): internal_interfaces = [interface for interface in config_interfaces if "internal" in interface] with allure.step("Block internal interfaces for node"): for interface in internal_interfaces: cluster_state_controller.down_interface(nodes=nodes_with_object, interface=interface) with allure.step("Tick epoch and wait 2 block"): nodes_without_an_object = list(set(self.cluster.cluster_nodes) - set(nodes_with_object)) self.tick_epochs(1, alive_node=nodes_without_an_object[0].storage_node, wait_block=2) with allure.step("Get object others node, expect false"): with pytest.raises(RuntimeError, match="return code: 1"): get_object( wallet=default_wallet, cid=storage_object.cid, oid=storage_object.oid, shell=self.shell, endpoint=nodes_without_an_object[0].storage_node.get_rpc_endpoint(), ) with allure.step("Put object, others node, expect false"): with pytest.raises(RuntimeError, match="return code: 1"): put_object( wallet=default_wallet, path=storage_object.file_path, cid=storage_object.cid, shell=self.shell, endpoint=nodes_without_an_object[0].storage_node.get_rpc_endpoint(), ) with allure.step(f"Get object nodes with object, expect true"): input_file = get_object( wallet=default_wallet, cid=storage_object.cid, oid=storage_object.oid, shell=self.shell, endpoint=nodes_with_object[0].storage_node.get_rpc_endpoint(), ) file_wait_delete.append(input_file) with allure.step(f"Put object nodes with object, expect true"): temp_file_path = generate_file(simple_object_size.value) _ = put_object( wallet=default_wallet, path=temp_file_path, cid=storage_object.cid, shell=self.shell, endpoint=nodes_with_object[0].storage_node.get_rpc_endpoint(), ) file_wait_delete.append(temp_file_path) with allure.step("Restore interface and tick 1 epoch, wait 2 block"): cluster_state_controller.restore_interfaces() self.tick_epochs(1, alive_node=nodes_without_an_object[0].storage_node, wait_block=2) @pytest.mark.interfaces @pytest.mark.baremetal @pytest.mark.parametrize( "block_interface, other_interface", [(Interfaces.DATA_O, Interfaces.DATA_1), (Interfaces.DATA_1, Interfaces.DATA_O)], ) @allure.title("Down data interfaces to all nodes(interface={block_interface})") def test_down_data_interface( self, cluster_state_controller: ClusterStateController, default_wallet: str, simple_object_size: ObjectSize, delete_file_after_test: None, restore_down_interfaces: None, block_interface: Interfaces, other_interface: Interfaces, ): cluster_nodes = self.cluster.cluster_nodes with allure.step(f"Block {block_interface.value} interfaces"): cluster_state_controller.down_interface(cluster_nodes, block_interface.value) with allure.step("Tick 1 epoch and wait 2 block for sync all nodes"): self.tick_epochs(1, alive_node=cluster_nodes[0].storage_node, wait_block=2) with allure.step("Create container"): cid = create_container( wallet=default_wallet, shell=self.shell, endpoint=f"{cluster_nodes[0].get_data_interface(other_interface.value)[0]}:8080", rule="REP 4 CBF 1", ) with allure.step("Put object"): file_path = generate_file(simple_object_size.value) file_wait_delete.append(file_path) oid = put_object( wallet=default_wallet, path=file_path, cid=cid, shell=self.shell, endpoint=f"{cluster_nodes[0].get_data_interface(other_interface.value)[0]}:8080", ) with allure.step("Get object"): file_get_path = get_object( wallet=default_wallet, cid=cid, oid=oid, shell=self.shell, endpoint=f"{cluster_nodes[0].get_data_interface(other_interface.value)[0]}:8080", ) file_wait_delete.append(file_get_path) with allure.step("Restore interfaces all nodes"): cluster_state_controller.restore_interfaces() self.tick_epochs(1, alive_node=cluster_nodes[0].storage_node, wait_block=2) @pytest.mark.interfaces @pytest.mark.baremetal @pytest.mark.parametrize("interface", [Interfaces.INTERNAL_0, Interfaces.INTERNAL_1]) @allure.title("Down internal interfaces to all nodes(interface={interface})") def test_down_internal_interface( self, cluster_state_controller: ClusterStateController, default_wallet: str, simple_object_size: ObjectSize, delete_file_after_test: None, restore_down_interfaces: None, interface: Interfaces, ): cluster_nodes = self.cluster.cluster_nodes latest_block = {} with allure.step("Get block all nodes"): for cluster_node in cluster_nodes: latest_block[cluster_node] = neo_go_query_height( shell=cluster_node.host.get_shell(), endpoint=cluster_node.morph_chain.get_http_endpoint() ) with allure.step(f"Block {interface} interfaces"): cluster_state_controller.down_interface(cluster_nodes, interface.value) with allure.step("Tick 1 epoch and wait 2 block for sync all nodes"): self.tick_epochs(1, alive_node=cluster_nodes[0].storage_node, wait_block=2) with allure.step("Create container"): cid = create_container( wallet=default_wallet, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, rule="REP 4 CBF 1", ) with allure.step(f"Put object, after down {interface}"): file_path = generate_file(simple_object_size.value) file_wait_delete.append(file_path) oid = put_object( wallet=default_wallet, path=file_path, cid=cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, ) with allure.step("Get object"): file_get_path = get_object( wallet=default_wallet, cid=cid, oid=oid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, ) file_wait_delete.append(file_get_path) now_block = {} with allure.step("Get actual block"): for cluster_node in cluster_nodes: now_block[cluster_node] = neo_go_query_height( shell=cluster_node.host.get_shell(), endpoint=cluster_node.morph_chain.get_http_endpoint() ) with allure.step(f"Compare block"): for cluster_node, items in now_block.items(): with allure.step( f"Node - {cluster_node.host_ip}, old block - {latest_block[cluster_node]['Latest block']}, " f"now block - {now_block[cluster_node]['Latest block']}" ): assert latest_block[cluster_node]["Latest block"] < now_block[cluster_node]["Latest block"] with allure.step("Restore interfaces all nodes"): cluster_state_controller.restore_interfaces() self.tick_epochs(1, alive_node=self.cluster.cluster_nodes[0].storage_node, wait_block=2)