Compare commits

..

No commits in common. "ab2397130bed5ce0cf660a6e544f275b6e6de26f" and "b33514df3cce605815aa8d530e702bf2c04e98af" have entirely different histories.

38 changed files with 2793 additions and 3687 deletions

View file

@ -10,7 +10,7 @@ repos:
- id: isort - id: isort
name: isort (python) name: isort (python)
- repo: https://git.frostfs.info/TrueCloudLab/allure-validator - repo: https://git.frostfs.info/TrueCloudLab/allure-validator
rev: 1.1.1 rev: 1.1.0
hooks: hooks:
- id: allure-validator - id: allure-validator
args: [ args: [

View file

@ -37,12 +37,11 @@ markers =
session_token: tests for operations with session token session_token: tests for operations with session token
static_session: tests for operations with static session token static_session: tests for operations with static session token
bearer: tests for bearer tokens bearer: tests for bearer tokens
ape: tests for APE acl: All tests for ACL
ape_allow: tests for APE allow rules acl_basic: tests for basic ACL
ape_deny: tests for APE deny rules acl_bearer: tests for ACL with bearer
ape_container: tests for APE on container operations acl_extended: tests for extended ACL
ape_object: tests for APE on object operations acl_filters: tests for extended ACL with filters and headers
ape_namespace: tests for APE on namespace scope
storage_group: tests for storage groups storage_group: tests for storage groups
failover: tests for system recovery after a failure failover: tests for system recovery after a failure
failover_panic: tests for system recovery after panic reboot of a node failover_panic: tests for system recovery after panic reboot of a node

View file

@ -1,79 +0,0 @@
import json
import time
from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.resources.common import MORPH_BLOCK_TIME
from frostfs_testlib.shell.interfaces import Shell
from frostfs_testlib.steps.cli.container import create_container, search_nodes_with_container
from frostfs_testlib.storage.cluster import Cluster
from frostfs_testlib.storage.dataclasses import ape
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.parallel import parallel
from frostfs_testlib.utils import datetime_utils
from .container_request import ContainerRequest, MultipleContainersRequest
def create_container_with_ape(
container_request: ContainerRequest,
frostfs_cli: FrostfsCli,
wallet: WalletInfo,
shell: Shell,
cluster: Cluster,
endpoint: str,
) -> str:
with reporter.step("Create container"):
cid = _create_container_by_spec(container_request, wallet, shell, cluster, endpoint)
if container_request.ape_rules:
with reporter.step("Apply APE rules for container"):
_apply_ape_rules(cid, frostfs_cli, endpoint, container_request.ape_rules)
with reporter.step("Wait for one block"):
time.sleep(datetime_utils.parse_time(MORPH_BLOCK_TIME))
with reporter.step("Search nodes holding the container"):
container_holder_nodes = search_nodes_with_container(wallet, cid, shell, cluster.default_rpc_endpoint, cluster)
report_data = {node.id: node.host_ip for node in container_holder_nodes}
reporter.attach(json.dumps(report_data, indent=2), "container_nodes.json")
return cid
@reporter.step("Create multiple containers with APE")
def create_containers_with_ape(
frostfs_cli: FrostfsCli,
wallet: WalletInfo,
shell: Shell,
cluster: Cluster,
endpoint: str,
multiple_containers_request: MultipleContainersRequest,
) -> list[str]:
cids_futures = parallel(create_container_with_ape, multiple_containers_request, frostfs_cli, wallet, shell, cluster, endpoint)
return [future.result() for future in cids_futures]
@reporter.step("Create container by spec {container_request}")
def _create_container_by_spec(
container_request: ContainerRequest,
wallet: WalletInfo,
shell: Shell,
cluster: Cluster,
endpoint: str,
) -> str:
return create_container(wallet, shell, endpoint, container_request.parsed_rule(cluster), wait_for_creation=False)
def _apply_ape_rules(cid: str, frostfs_cli: FrostfsCli, endpoint: str, ape_rules: list[ape.Rule]):
for ape_rule in ape_rules:
rule_str = ape_rule.as_string()
with reporter.step(f"Apply APE rule '{rule_str}' for container {cid}"):
frostfs_cli.ape_manager.add(
endpoint,
ape_rule.chain_id,
target_name=cid,
target_type="container",
rule=rule_str,
)

View file

@ -1,83 +0,0 @@
from dataclasses import dataclass
from functools import partial
import pytest
from frostfs_testlib.steps.cli.container import DEFAULT_PLACEMENT_RULE
from frostfs_testlib.storage.cluster import Cluster
from frostfs_testlib.storage.dataclasses import ape
APE_EVERYONE_ALLOW_ALL = [ape.Rule(ape.Verb.ALLOW, ape.ObjectOperations.WILDCARD_ALL)]
# In case if we need container operations
# ape.Rule(ape.Verb.ALLOW, ape.ContainerOperations.WILDCARD_ALL)]
APE_OWNER_ALLOW_ALL = [ape.Rule(ape.Verb.ALLOW, ape.ObjectOperations.WILDCARD_ALL, ape.Condition.by_role(ape.Role.OWNER))]
# In case if we need container operations
# ape.Rule(ape.Verb.ALLOW, ape.ContainerOperations.WILDCARD_ALL, ape.Condition.by_role(ape.Role.OWNER))]
@dataclass
class ContainerRequest:
policy: str = None
ape_rules: list[ape.Rule] = None
short_name: str | None = None
def __post_init__(self):
if self.ape_rules is None:
self.ape_rules = []
# For pytest instead of ids=[...] everywhere
self.__name__ = self.short_name
def parsed_rule(self, cluster: Cluster):
if self.policy is None:
return None
substitutions = {"%NODE_COUNT%": str(len(cluster.cluster_nodes))}
parsed_rule = self.policy
for sub, replacement in substitutions.items():
parsed_rule = parsed_rule.replace(sub, replacement)
return parsed_rule
def __repr__(self):
if self.short_name:
return self.short_name
spec_info: list[str] = []
if self.policy:
spec_info.append(f"policy='{self.policy}'")
if self.ape_rules:
ape_rules_list = ", ".join([f"'{rule.as_string()}'" for rule in self.ape_rules])
spec_info.append(f"ape_rules=[{ape_rules_list}]")
return f"({', '.join(spec_info)})"
class MultipleContainersRequest(list[ContainerRequest]):
def __init__(self, iterable=None):
"""Override initializer which can accept iterable"""
super(MultipleContainersRequest, self).__init__()
if iterable:
self.extend(iterable)
self.__set_name()
def __set_name(self):
self.__name__ = ", ".join([s.__name__ for s in self])
PUBLIC_WITH_POLICY = partial(ContainerRequest, ape_rules=APE_EVERYONE_ALLOW_ALL, short_name="Custom_policy_with_allow_all_ape_rule")
EVERYONE_ALLOW_ALL = ContainerRequest(policy=DEFAULT_PLACEMENT_RULE, ape_rules=APE_EVERYONE_ALLOW_ALL, short_name="Everyone_Allow_All")
OWNER_ALLOW_ALL = ContainerRequest(policy=DEFAULT_PLACEMENT_RULE, ape_rules=APE_OWNER_ALLOW_ALL, short_name="Owner_Allow_All")
PRIVATE = ContainerRequest(policy=DEFAULT_PLACEMENT_RULE, ape_rules=[], short_name="Private_No_APE")
def requires_container(container_request: None | ContainerRequest | list[ContainerRequest] = None) -> pytest.MarkDecorator:
if container_request is None:
container_request = EVERYONE_ALLOW_ALL
if not isinstance(container_request, list):
container_request = [container_request]
return pytest.mark.parametrize("container_request", container_request, indirect=True)

View file

@ -0,0 +1,23 @@
from dataclasses import dataclass
from frostfs_testlib.steps.cli.container import DEFAULT_PLACEMENT_RULE
from frostfs_testlib.storage.cluster import Cluster
@dataclass
class ContainerSpec:
rule: str = DEFAULT_PLACEMENT_RULE
basic_acl: str = None
allow_owner_via_ape: bool = False
def parsed_rule(self, cluster: Cluster):
if self.rule is None:
return None
substitutions = {"%NODE_COUNT%": str(len(cluster.cluster_nodes))}
parsed_rule = self.rule
for sub, replacement in substitutions.items():
parsed_rule = parsed_rule.replace(sub, replacement)
return parsed_rule

View file

@ -1,26 +0,0 @@
from frostfs_testlib.shell.interfaces import Shell
from frostfs_testlib.steps.cli.container import get_container
from frostfs_testlib.storage.dataclasses.storage_object_info import NodeNetmapInfo
from workspace.frostfs_testcases.pytest_tests.helpers.utility import placement_policy_from_container
def validate_object_policy(wallet: str, shell: Shell, placement_rule: str, cid: str, endpoint: str):
got_policy = placement_policy_from_container(get_container(wallet, cid, shell, endpoint, False))
assert got_policy.replace("'", "") == placement_rule.replace(
"'", ""
), f"Expected \n{placement_rule} and got policy \n{got_policy} are the same"
def get_netmap_param(netmap_info: list[NodeNetmapInfo]) -> dict:
dict_external = dict()
for node in netmap_info:
external_adress = node.external_address[0].split("/")[2]
dict_external[external_adress] = {
"country": node.country,
"country_code": node.country_code,
"Price": node.price,
"continent": node.continent,
"un_locode": node.un_locode,
"location": node.location,
}
return dict_external

View file

@ -0,0 +1,105 @@
import allure
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.resources.wellknown_acl import PRIVATE_ACL_F, PUBLIC_ACL_F, READONLY_ACL_F
from frostfs_testlib.shell import Shell
from frostfs_testlib.steps.cli.object import put_object_to_random_node
from frostfs_testlib.storage.cluster import Cluster
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from ....helpers.container_access import assert_full_access_to_container, assert_no_access_to_container, assert_read_only_container
from ....helpers.container_spec import ContainerSpec
@pytest.mark.nightly
@pytest.mark.sanity
@pytest.mark.acl
class TestACLBasic(ClusterTestBase):
@allure.title("Operations in public container available to everyone (obj_size={object_size})")
@pytest.mark.container(ContainerSpec(basic_acl=PUBLIC_ACL_F))
def test_basic_acl_public(
self,
default_wallet: WalletInfo,
other_wallet: WalletInfo,
client_shell: Shell,
container: str,
file_path: str,
cluster: Cluster,
):
"""
Test access to object operations in public container.
"""
for wallet, role in ((default_wallet, "owner"), (other_wallet, "others")):
with reporter.step("Put objects to container"):
# We create new objects for each wallet because assert_full_access_to_container
# deletes the object
owner_object_oid = put_object_to_random_node(
default_wallet,
file_path,
container,
shell=self.shell,
cluster=self.cluster,
attributes={"created": "owner"},
)
other_object_oid = put_object_to_random_node(
other_wallet,
file_path,
container,
shell=self.shell,
cluster=self.cluster,
attributes={"created": "other"},
)
with reporter.step(f"Check {role} has full access to public container"):
assert_full_access_to_container(wallet, container, owner_object_oid, file_path, client_shell, cluster)
assert_full_access_to_container(wallet, container, other_object_oid, file_path, client_shell, cluster)
@allure.title("Operations in private container only available to owner (obj_size={object_size})")
@pytest.mark.container(ContainerSpec(basic_acl=PRIVATE_ACL_F))
def test_basic_acl_private(
self,
default_wallet: WalletInfo,
other_wallet: WalletInfo,
client_shell: Shell,
container: str,
file_path: str,
cluster: Cluster,
):
"""
Test access to object operations in private container.
"""
with reporter.step("Put object to container"):
owner_object_oid = put_object_to_random_node(default_wallet, file_path, container, client_shell, cluster)
with reporter.step("Check no one except owner has access to operations with container"):
assert_no_access_to_container(other_wallet, container, owner_object_oid, file_path, client_shell, cluster)
with reporter.step("Check owner has full access to private container"):
assert_full_access_to_container(default_wallet, container, owner_object_oid, file_path, self.shell, cluster)
@allure.title("Read operations in readonly container available to others (obj_size={object_size})")
@pytest.mark.container(ContainerSpec(basic_acl=READONLY_ACL_F))
def test_basic_acl_readonly(
self,
default_wallet: WalletInfo,
other_wallet: WalletInfo,
client_shell: Shell,
container: str,
file_path: str,
cluster: Cluster,
):
"""
Test access to object operations in readonly container.
"""
with reporter.step("Put object to container"):
object_oid = put_object_to_random_node(default_wallet, file_path, container, client_shell, cluster)
with reporter.step("Check others has read-only access to operations with container"):
assert_read_only_container(other_wallet, container, object_oid, file_path, client_shell, cluster)
with reporter.step("Check owner has full access to public container"):
assert_full_access_to_container(default_wallet, container, object_oid, file_path, client_shell, cluster)

View file

@ -2,6 +2,7 @@ import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.resources.wellknown_acl import PUBLIC_ACL
from frostfs_testlib.steps.cli.object import put_object_to_random_node from frostfs_testlib.steps.cli.object import put_object_to_random_node
from frostfs_testlib.steps.node_management import drop_object from frostfs_testlib.steps.node_management import drop_object
from frostfs_testlib.storage.dataclasses import ape from frostfs_testlib.storage.dataclasses import ape
@ -17,7 +18,7 @@ from ....helpers.container_access import (
assert_full_access_to_container, assert_full_access_to_container,
assert_no_access_to_container, assert_no_access_to_container,
) )
from ....helpers.container_request import APE_EVERYONE_ALLOW_ALL, OWNER_ALLOW_ALL, ContainerRequest from ....helpers.container_spec import ContainerSpec
@pytest.fixture @pytest.fixture
@ -112,11 +113,7 @@ class TestApeContainer(ClusterTestBase):
assert_full_access_to_container(other_wallet_2, container, objects.pop(), file_path, self.shell, self.cluster) assert_full_access_to_container(other_wallet_2, container, objects.pop(), file_path, self.shell, self.cluster)
@allure.title("Replication works with APE deny rules on OWNER and OTHERS (obj_size={object_size})") @allure.title("Replication works with APE deny rules on OWNER and OTHERS (obj_size={object_size})")
@pytest.mark.parametrize( @pytest.mark.container(ContainerSpec(f"REP %NODE_COUNT% IN X CBF 1 SELECT %NODE_COUNT% FROM * AS X", PUBLIC_ACL))
"container_request",
[ContainerRequest(f"REP %NODE_COUNT% IN X CBF 1 SELECT %NODE_COUNT% FROM * AS X", APE_EVERYONE_ALLOW_ALL, "custom")],
indirect=True,
)
def test_replication_works_with_deny_rules( def test_replication_works_with_deny_rules(
self, self,
default_wallet: WalletInfo, default_wallet: WalletInfo,
@ -152,7 +149,6 @@ class TestApeContainer(ClusterTestBase):
wait_object_replication(container, oid, len(self.cluster.storage_nodes), self.shell, self.cluster.storage_nodes) wait_object_replication(container, oid, len(self.cluster.storage_nodes), self.shell, self.cluster.storage_nodes)
@allure.title("Deny operations via APE by role (role=ir, obj_size={object_size})") @allure.title("Deny operations via APE by role (role=ir, obj_size={object_size})")
@pytest.mark.parametrize("container_request", [OWNER_ALLOW_ALL], indirect=True)
def test_deny_operations_via_ape_by_role_ir( def test_deny_operations_via_ape_by_role_ir(
self, frostfs_cli: FrostfsCli, ir_wallet: WalletInfo, container: str, objects: list[str], rpc_endpoint: str, file_path: TestFile self, frostfs_cli: FrostfsCli, ir_wallet: WalletInfo, container: str, objects: list[str], rpc_endpoint: str, file_path: TestFile
): ):
@ -160,7 +156,7 @@ class TestApeContainer(ClusterTestBase):
ape.ObjectOperations.PUT: False, ape.ObjectOperations.PUT: False,
ape.ObjectOperations.GET: True, ape.ObjectOperations.GET: True,
ape.ObjectOperations.HEAD: True, ape.ObjectOperations.HEAD: True,
ape.ObjectOperations.GET_RANGE: True, ape.ObjectOperations.GET_RANGE: False,
ape.ObjectOperations.GET_RANGE_HASH: True, ape.ObjectOperations.GET_RANGE_HASH: True,
ape.ObjectOperations.SEARCH: True, ape.ObjectOperations.SEARCH: True,
ape.ObjectOperations.DELETE: False, ape.ObjectOperations.DELETE: False,
@ -189,7 +185,6 @@ class TestApeContainer(ClusterTestBase):
assert_access_to_container(default_ir_access, ir_wallet, container, objects[0], file_path, self.shell, self.cluster) assert_access_to_container(default_ir_access, ir_wallet, container, objects[0], file_path, self.shell, self.cluster)
@allure.title("Deny operations via APE by role (role=container, obj_size={object_size})") @allure.title("Deny operations via APE by role (role=container, obj_size={object_size})")
@pytest.mark.parametrize("container_request", [OWNER_ALLOW_ALL], indirect=True)
def test_deny_operations_via_ape_by_role_container( def test_deny_operations_via_ape_by_role_container(
self, self,
frostfs_cli: FrostfsCli, frostfs_cli: FrostfsCli,
@ -203,10 +198,10 @@ class TestApeContainer(ClusterTestBase):
ape.ObjectOperations.PUT: True, ape.ObjectOperations.PUT: True,
ape.ObjectOperations.GET: True, ape.ObjectOperations.GET: True,
ape.ObjectOperations.HEAD: True, ape.ObjectOperations.HEAD: True,
ape.ObjectOperations.GET_RANGE: True, ape.ObjectOperations.GET_RANGE: False,
ape.ObjectOperations.GET_RANGE_HASH: True, ape.ObjectOperations.GET_RANGE_HASH: True,
ape.ObjectOperations.SEARCH: True, ape.ObjectOperations.SEARCH: True,
ape.ObjectOperations.DELETE: True, ape.ObjectOperations.DELETE: False,
} }
with reporter.step("Assert CONTAINER wallet access in default state"): with reporter.step("Assert CONTAINER wallet access in default state"):
@ -221,7 +216,7 @@ class TestApeContainer(ClusterTestBase):
self.wait_for_blocks() self.wait_for_blocks()
with reporter.step("Assert CONTAINER wallet ignores APE rule"): with reporter.step("Assert CONTAINER wallet ignores APE rule"):
assert_access_to_container(access_matrix, container_node_wallet, container, objects[1], file_path, self.shell, self.cluster) assert_access_to_container(access_matrix, container_node_wallet, container, objects[0], file_path, self.shell, self.cluster)
with reporter.step("Remove APE rule"): with reporter.step("Remove APE rule"):
frostfs_cli.ape_manager.remove(rpc_endpoint, rule.chain_id, target_name=container, target_type="container") frostfs_cli.ape_manager.remove(rpc_endpoint, rule.chain_id, target_name=container, target_type="container")
@ -230,4 +225,4 @@ class TestApeContainer(ClusterTestBase):
self.wait_for_blocks() self.wait_for_blocks()
with reporter.step("Assert CONTAINER wallet access after rule was removed"): with reporter.step("Assert CONTAINER wallet access after rule was removed"):
assert_access_to_container(access_matrix, container_node_wallet, container, objects[2], file_path, self.shell, self.cluster) assert_access_to_container(access_matrix, container_node_wallet, container, objects[0], file_path, self.shell, self.cluster)

View file

@ -18,7 +18,7 @@ from ....helpers.container_access import (
assert_full_access_to_container, assert_full_access_to_container,
assert_no_access_to_container, assert_no_access_to_container,
) )
from ....helpers.container_request import OWNER_ALLOW_ALL from ....helpers.container_spec import ContainerSpec
from ....helpers.object_access import OBJECT_ACCESS_DENIED from ....helpers.object_access import OBJECT_ACCESS_DENIED
@ -241,7 +241,8 @@ class TestApeFilters(ClusterTestBase):
@allure.title("Operations with allow APE rule with resource filters (match_type={match_type}, obj_size={object_size})") @allure.title("Operations with allow APE rule with resource filters (match_type={match_type}, obj_size={object_size})")
@pytest.mark.parametrize("match_type", [ape.MatchType.EQUAL, ape.MatchType.NOT_EQUAL]) @pytest.mark.parametrize("match_type", [ape.MatchType.EQUAL, ape.MatchType.NOT_EQUAL])
@pytest.mark.parametrize("object_size, container_request", [("simple", OWNER_ALLOW_ALL)], indirect=True) @pytest.mark.parametrize("object_size", ["simple"], indirect=True)
@pytest.mark.container(ContainerSpec(basic_acl="0", allow_owner_via_ape=True))
def test_ape_allow_filters_object( def test_ape_allow_filters_object(
self, self,
frostfs_cli: FrostfsCli, frostfs_cli: FrostfsCli,
@ -341,7 +342,7 @@ class TestApeFilters(ClusterTestBase):
put_object_to_random_node(other_wallet, file_path, container, self.shell, self.cluster, bearer, attributes=allow_attribute) put_object_to_random_node(other_wallet, file_path, container, self.shell, self.cluster, bearer, attributes=allow_attribute)
@allure.title("PUT and GET object using bearer with objectID in filter (obj_size={object_size}, match_type=NOT_EQUAL)") @allure.title("PUT and GET object using bearer with objectID in filter (obj_size={object_size}, match_type=NOT_EQUAL)")
@pytest.mark.parametrize("container_request", [OWNER_ALLOW_ALL], indirect=True) @pytest.mark.container(ContainerSpec(basic_acl="0", allow_owner_via_ape=True))
def test_ape_filter_object_id_not_equals( def test_ape_filter_object_id_not_equals(
self, self,
frostfs_cli: FrostfsCli, frostfs_cli: FrostfsCli,
@ -369,7 +370,7 @@ class TestApeFilters(ClusterTestBase):
get_object_from_random_node(other_wallet, container, oid, self.shell, self.cluster, bearer) get_object_from_random_node(other_wallet, container, oid, self.shell, self.cluster, bearer)
@allure.title("PUT and GET object using bearer with objectID in filter (obj_size={object_size}, match_type=EQUAL)") @allure.title("PUT and GET object using bearer with objectID in filter (obj_size={object_size}, match_type=EQUAL)")
@pytest.mark.parametrize("container_request", [OWNER_ALLOW_ALL], indirect=True) @pytest.mark.container(ContainerSpec(basic_acl="0", allow_owner_via_ape=True))
def test_ape_filter_object_id_equals( def test_ape_filter_object_id_equals(
self, self,
frostfs_cli: FrostfsCli, frostfs_cli: FrostfsCli,

View file

@ -75,7 +75,7 @@ class TestApeBearer(ClusterTestBase):
temp_directory: str, temp_directory: str,
default_wallet: WalletInfo, default_wallet: WalletInfo,
other_wallet: WalletInfo, other_wallet: WalletInfo,
container: str, container: tuple[str, list[str], str],
objects: list[str], objects: list[str],
rpc_endpoint: str, rpc_endpoint: str,
file_path: TestFile, file_path: TestFile,
@ -114,23 +114,24 @@ class TestApeBearer(ClusterTestBase):
bt_access_map = { bt_access_map = {
ape.Role.OWNER: { ape.Role.OWNER: {
ape.ObjectOperations.PUT: True, ape.ObjectOperations.PUT: True,
ape.ObjectOperations.GET: False, ape.ObjectOperations.GET: True,
ape.ObjectOperations.HEAD: True, ape.ObjectOperations.HEAD: True,
ape.ObjectOperations.GET_RANGE: True, ape.ObjectOperations.GET_RANGE: True,
ape.ObjectOperations.GET_RANGE_HASH: False, ape.ObjectOperations.GET_RANGE_HASH: True,
ape.ObjectOperations.SEARCH: False, ape.ObjectOperations.SEARCH: True,
ape.ObjectOperations.DELETE: True, ape.ObjectOperations.DELETE: True,
}, },
# Bearer Token COMPLETLY overrides chains set for the specific target.
# Thus, any restictions or permissions should be explicitly defined in BT.
ape.Role.OTHERS: { ape.Role.OTHERS: {
ape.ObjectOperations.PUT: False, ape.ObjectOperations.PUT: True,
ape.ObjectOperations.GET: False, ape.ObjectOperations.GET: False,
ape.ObjectOperations.HEAD: False, ape.ObjectOperations.HEAD: True,
ape.ObjectOperations.GET_RANGE: False, ape.ObjectOperations.GET_RANGE: False,
ape.ObjectOperations.GET_RANGE_HASH: False, ape.ObjectOperations.GET_RANGE_HASH: False,
ape.ObjectOperations.SEARCH: False, # Although SEARCH is denied by the APE chain defined in Policy contract,
ape.ObjectOperations.DELETE: False, # Bearer Token COMPLETLY overrides chains set for the specific target.
# Thus, any restictions or permissions should be explicitly defined in BT.
ape.ObjectOperations.SEARCH: True,
ape.ObjectOperations.DELETE: True,
}, },
} }
@ -147,11 +148,9 @@ class TestApeBearer(ClusterTestBase):
# Operations that we will allow for each role with bearer token # Operations that we will allow for each role with bearer token
bearer_map = { bearer_map = {
ape.Role.OWNER: [ ape.Role.OWNER: [
ape.ObjectOperations.PUT,
ape.ObjectOperations.HEAD,
ape.ObjectOperations.GET_RANGE,
# Delete also requires PUT (to make tobstone) and HEAD (to get simple objects header)
ape.ObjectOperations.DELETE, ape.ObjectOperations.DELETE,
ape.ObjectOperations.PUT,
ape.ObjectOperations.GET_RANGE,
], ],
ape.Role.OTHERS: [ ape.Role.OTHERS: [
ape.ObjectOperations.GET, ape.ObjectOperations.GET,

View file

@ -1,14 +1,22 @@
import json import json
import time
import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.resources.common import MORPH_BLOCK_TIME
from frostfs_testlib.resources.wellknown_acl import PUBLIC_ACL
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
from frostfs_testlib.steps.cli.container import search_nodes_with_container from frostfs_testlib.steps.cli.container import create_container, search_nodes_with_container
from frostfs_testlib.steps.cli.object import put_object_to_random_node from frostfs_testlib.steps.cli.object import put_object_to_random_node
from frostfs_testlib.storage.cluster import Cluster, ClusterNode from frostfs_testlib.storage.cluster import Cluster, ClusterNode
from frostfs_testlib.storage.dataclasses import ape from frostfs_testlib.storage.dataclasses import ape
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.parallel import parallel from frostfs_testlib.testing.parallel import parallel
from frostfs_testlib.utils import datetime_utils
from ...helpers.container_spec import ContainerSpec
OBJECT_COUNT = 5 OBJECT_COUNT = 5
@ -40,6 +48,81 @@ def test_wallet(default_wallet: WalletInfo, other_wallet: WalletInfo, role: ape.
return role_to_wallet_map[role] return role_to_wallet_map[role]
@pytest.fixture
def container(
default_wallet: WalletInfo,
frostfs_cli: FrostfsCli,
client_shell: Shell,
cluster: Cluster,
request: pytest.FixtureRequest,
rpc_endpoint: str,
) -> str:
container_spec = _get_container_spec(request)
cid = _create_container_by_spec(default_wallet, client_shell, cluster, rpc_endpoint, container_spec)
if container_spec.allow_owner_via_ape:
_allow_owner_via_ape(frostfs_cli, cluster, cid)
return cid
def _create_container_by_spec(
default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster, rpc_endpoint: str, container_spec: ContainerSpec
) -> str:
# TODO: add container spec to step message
with reporter.step("Create container"):
cid = create_container(
default_wallet, client_shell, rpc_endpoint, basic_acl=container_spec.basic_acl, rule=container_spec.parsed_rule(cluster)
)
with reporter.step("Search nodes holding the container"):
container_holder_nodes = search_nodes_with_container(default_wallet, cid, client_shell, cluster.default_rpc_endpoint, cluster)
report_data = {node.id: node.host_ip for node in container_holder_nodes}
reporter.attach(json.dumps(report_data, indent=2), "container_nodes.json")
return cid
def _get_container_spec(request: pytest.FixtureRequest) -> ContainerSpec:
container_marker = request.node.get_closest_marker("container")
# let default container to be public at the moment
container_spec = ContainerSpec(basic_acl=PUBLIC_ACL)
if container_marker:
if len(container_marker.args) != 1:
raise RuntimeError(f"Something wrong with container marker: {container_marker}")
container_spec = container_marker.args[0]
if "param" in request.__dict__:
container_spec = request.param
if not container_spec:
raise RuntimeError(
f"""Container specification is empty.
Either add @pytest.mark.container(ContainerSpec(...)) or
@pytest.mark.parametrize(\"container\", [ContainerSpec(...)], indirect=True) decorator"""
)
return container_spec
def _allow_owner_via_ape(frostfs_cli: FrostfsCli, cluster: Cluster, container: str):
with reporter.step("Create allow APE rule for container owner"):
role_condition = ape.Condition.by_role(ape.Role.OWNER)
deny_rule = ape.Rule(ape.Verb.ALLOW, ape.ObjectOperations.WILDCARD_ALL, role_condition)
frostfs_cli.ape_manager.add(
cluster.default_rpc_endpoint,
deny_rule.chain_id,
target_name=container,
target_type="container",
rule=deny_rule.as_string(),
)
with reporter.step("Wait for one block"):
time.sleep(datetime_utils.parse_time(MORPH_BLOCK_TIME))
@pytest.fixture @pytest.fixture
def objects(container: str, default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster, file_path: str): def objects(container: str, default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster, file_path: str):
with reporter.step("Add test objects to container"): with reporter.step("Add test objects to container"):

View file

@ -1,52 +0,0 @@
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.cli import FrostfsCli
from frostfs_testlib.resources.cli import FROSTFS_CLI_EXEC
from frostfs_testlib.shell.interfaces import Shell
from frostfs_testlib.steps.cli.object import put_object
from frostfs_testlib.storage.cluster import Cluster
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.utils.file_utils import generate_file
@pytest.fixture(scope="session")
def frostfs_cli_on_first_node(cluster: Cluster) -> FrostfsCli:
node = cluster.cluster_nodes[0]
shell = node.host.get_shell()
return FrostfsCli(shell, FROSTFS_CLI_EXEC, node.storage_node.get_remote_wallet_config_path())
@pytest.fixture
def object_id(
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
cluster: Cluster,
client_shell: Shell,
container: str,
) -> str:
test_file = generate_file(simple_object_size.value)
with reporter.step("Allow PutObject on first node via local override"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowPutObject",
rule=f"allow Object.Put *",
)
with reporter.step("Put objects in container on the first node"):
object_id = put_object(default_wallet, test_file, container, client_shell, cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Remove PutObject local override from first node"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowPutObject",
)
return object_id

File diff suppressed because it is too large Load diff

View file

@ -1,212 +0,0 @@
from time import sleep
import allure
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.cli import FrostfsCli
from frostfs_testlib.resources.cli import FROSTFS_CLI_EXEC
from frostfs_testlib.resources.common import MORPH_BLOCK_TIME
from frostfs_testlib.resources.error_patterns import RULE_ACCESS_DENIED_CONTAINER
from frostfs_testlib.shell.interfaces import Shell
from frostfs_testlib.storage.cluster import Cluster, ClusterNode
from frostfs_testlib.storage.dataclasses.ape import Operations
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.parallel import parallel
from frostfs_testlib.testing.test_control import expect_not_raises
from frostfs_testlib.utils import datetime_utils
from ...helpers.container_request import APE_EVERYONE_ALLOW_ALL, ContainerRequest, MultipleContainersRequest
REP2 = ContainerRequest("REP 2", ape_rules=APE_EVERYONE_ALLOW_ALL, short_name="REP2_allow_all_ape")
def remove_local_overrides_on_node(node: ClusterNode):
target = "Chain ID"
shell: Shell = node.host.get_shell()
remote_config: str = node.storage_node.get_remote_wallet_config_path()
cli = FrostfsCli(shell=shell, frostfs_cli_exec_path=FROSTFS_CLI_EXEC, config_file=remote_config)
with reporter.step(f"Check local overrides on {node.storage_node.id} node"):
rules = cli.control.list_rules(
endpoint=node.storage_node.get_control_endpoint(), target_name="root", target_type="namespace"
).stdout
if target in rules:
with reporter.step("Delete rules"):
chain_ids = [i.split(" ")[2].strip() for i in rules.split("\n") if "Chain ID" in i]
for chain_id in chain_ids:
cli.control.remove_rule(
endpoint=node.storage_node.get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id=chain_id,
)
with reporter.step("Wait for one block"):
sleep(datetime_utils.parse_time(MORPH_BLOCK_TIME))
@pytest.fixture(scope="session")
def remove_local_ape_overrides(cluster: Cluster) -> None:
yield
with reporter.step("Check local overrides on nodes."):
parallel(remove_local_overrides_on_node, cluster.cluster_nodes)
@pytest.mark.ape
@pytest.mark.ape_local
@pytest.mark.ape_container
@pytest.mark.ape_namespace
class TestApeLocalOverrideContainer(ClusterTestBase):
@allure.title("LocalOverride: Deny to GetContainer in root tenant")
def test_local_override_deny_to_get_container_root(
self,
frostfs_cli_on_first_node: FrostfsCli,
frostfs_cli: FrostfsCli,
container: str,
remove_local_ape_overrides: None,
):
with reporter.step("Create a namespace rule for the first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerGet",
rule="deny Container.Get *",
)
with reporter.step("Check get the container property on the first node, expected denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_CONTAINER.format(operation=Operations.GET_CONTAINER)):
frostfs_cli.container.get(self.cluster.storage_nodes[0].get_rpc_endpoint(), container)
with reporter.step("Check get the container property on the second node, expected allow"):
with expect_not_raises():
frostfs_cli.container.get(self.cluster.storage_nodes[1].get_rpc_endpoint(), container)
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerGet",
)
with reporter.step("Check get the container property on the first node, expected allow"):
with expect_not_raises():
frostfs_cli.container.get(self.cluster.storage_nodes[0].get_rpc_endpoint(), container)
@allure.title("LocalOverride: Deny to PutContainer in root tenant")
def test_local_override_deny_to_put_container_root(
self,
frostfs_cli_on_first_node: FrostfsCli,
frostfs_cli: FrostfsCli,
remove_local_ape_overrides: None,
):
with reporter.step("Create a namespace rule for the first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerPut",
rule="deny Container.Put *",
)
with reporter.step("Check create container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_CONTAINER.format(operation=Operations.PUT_CONTAINER)):
frostfs_cli.container.create(
rpc_endpoint=self.cluster.storage_nodes[0].get_rpc_endpoint(),
policy="REP 1",
)
with reporter.step("Check create a container on the second node, expected allow"):
with expect_not_raises():
frostfs_cli.container.create(
rpc_endpoint=self.cluster.storage_nodes[1].get_rpc_endpoint(),
policy="REP 1",
)
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerPut",
)
with reporter.step("Check create a container on the first node, expected allow"):
with expect_not_raises():
frostfs_cli.container.create(
rpc_endpoint=self.cluster.storage_nodes[0].get_rpc_endpoint(),
policy="REP 1",
)
@allure.title("LocalOverride: Deny to ListContainer in root tenant")
def test_local_override_deny_to_list_container_root(
self,
frostfs_cli_on_first_node: FrostfsCli,
frostfs_cli: FrostfsCli,
remove_local_ape_overrides: None,
):
with reporter.step("Create a namespace rule for the first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerList",
rule="deny Container.List *",
)
with reporter.step("Check list the container properties on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_CONTAINER.format(operation=Operations.LIST_CONTAINER)):
frostfs_cli.container.list(rpc_endpoint=self.cluster.storage_nodes[0].get_rpc_endpoint(), ttl=1)
with reporter.step("Check list the container properties on the second node, expected allow"):
with expect_not_raises():
frostfs_cli.container.list(rpc_endpoint=self.cluster.storage_nodes[1].get_rpc_endpoint(), ttl=1)
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerList",
)
with reporter.step("Check display a list of containers on the first node, expected allow"):
with expect_not_raises():
frostfs_cli.container.list(rpc_endpoint=self.cluster.storage_nodes[0].get_rpc_endpoint(), ttl=1)
@allure.title("LocalOverride: Deny to DeleteContainer in root tenant")
@pytest.mark.parametrize("multiple_containers_request", [MultipleContainersRequest([REP2, REP2])], indirect=True)
def test_local_override_deny_to_delete_container_root(
self,
frostfs_cli_on_first_node: FrostfsCli,
frostfs_cli: FrostfsCli,
remove_local_ape_overrides: None,
containers: list[str],
):
with reporter.step("Create a namespace rule for the first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerDelete",
rule="deny Container.Delete *",
)
with reporter.step("Check delete first container from the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_CONTAINER.format(operation=Operations.DELETE_CONTAINER)):
frostfs_cli.container.delete(self.cluster.storage_nodes[0].get_rpc_endpoint(), containers[0], ttl=1)
with reporter.step("Check delete a second container from the second node, expected allow"):
with expect_not_raises():
frostfs_cli.container.delete(self.cluster.storage_nodes[1].get_rpc_endpoint(), containers[1], ttl=1)
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="namespace",
target_name="root",
chain_id="denyContainerDelete",
)
with reporter.step("Check delete first container from the first node, expected allow"):
with expect_not_raises():
frostfs_cli.container.delete(self.cluster.storage_nodes[0].get_rpc_endpoint(), containers[0], ttl=1)

View file

@ -1,254 +0,0 @@
import allure
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.cli import FrostfsCli
from frostfs_testlib.resources.error_patterns import NO_RULE_FOUND_OBJECT
from frostfs_testlib.steps.cli.object import delete_object, get_object, get_range, get_range_hash, head_object, put_object, search_object
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import expect_not_raises
from frostfs_testlib.utils.file_utils import generate_file
from ...helpers.container_request import ContainerRequest
REP1_MSK = ContainerRequest("REP 1 IN MOW CBF 1 SELECT 1 FROM MSK AS MOW FILTER SubDivCode EQ MOW AS MSK", short_name="REP1_MSK_no_ape")
@pytest.mark.ape
@pytest.mark.ape_local
@pytest.mark.ape_object
@pytest.mark.ape_allow
@pytest.mark.parametrize("container_request", [REP1_MSK], indirect=True)
class TestApeLocalOverrideAllow(ClusterTestBase):
@allure.title("LocalOverride: Allow to GetObject in root tenant")
def test_local_override_allow_to_get_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
object_id: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowGetObject",
rule=f"allow Object.Get *",
)
with reporter.step("Check get object in container on the first node, expected allow"):
with expect_not_raises():
get_object(default_wallet, container, object_id, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get object in container on the second node, epxected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
get_object(default_wallet, container, object_id, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowGetObject",
)
@allure.title("LocalOverride: Allow to PutObject in root tenant")
def test_local_override_allow_to_put_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowPutObject",
rule=f"allow Object.Put *",
)
with reporter.step("Check put object in container on the first node, expected allow"):
with expect_not_raises():
put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get object in container on the second node, epxected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowPutObject",
)
@allure.title("LocalOverride: Allow to HeadObject in root tenant")
def test_local_override_allow_to_head_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
object_id: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowHeadObject",
rule=f"allow Object.Head *",
)
with reporter.step("Check head object in container on the first node, expected allow"):
with expect_not_raises():
head_object(default_wallet, container, object_id, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check head object in container on the second node, expected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
head_object(default_wallet, container, object_id, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowHeadObject",
)
@allure.title("LocalOverride: Allow to SearchObject in root tenant")
def test_local_override_allow_to_search_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowSearchObject",
rule=f"allow Object.Search *",
)
with reporter.step("Check search object in container on the first node, expected allow"):
with expect_not_raises():
search_object(default_wallet, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check search object from container on the second node, expected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
search_object(default_wallet, container, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowSearchObject",
)
@allure.title("LocalOverride: Allow to RangeObject in root tenant")
def test_local_override_allow_to_range_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
object_id: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowRangeObject",
rule=f"allow Object.Range *",
)
with reporter.step("Check get range object in container on the first node, expected allow"):
with expect_not_raises():
get_range(default_wallet, container, object_id, "0:10", self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check range object in container on the second node. expected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
get_range(default_wallet, container, object_id, "0:10", self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowRangeObject",
)
@allure.title("LocalOverride: Allow to HashObject in root tenant")
def test_local_override_allow_to_hash_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
object_id: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowHashObject",
rule=f"allow Object.Hash *",
)
with reporter.step("Check get range hash object in container on the first node, expected allow"):
with expect_not_raises():
get_range_hash(default_wallet, container, object_id, "0:10", self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get range hash object in container on the second node, expected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
get_range_hash(default_wallet, container, object_id, "0:10", self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowHashObject",
)
@allure.title("LocalOverride: Allow to DeleteObject in root tenant")
def test_local_override_allow_to_delete_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
object_id: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowDeleteObject",
rule=f"allow Object.Head Object.Delete *",
)
with reporter.step("Check delete object from container on the second node, expected access denied error"):
with pytest.raises(RuntimeError, match=NO_RULE_FOUND_OBJECT):
delete_object(default_wallet, container, object_id, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Check delete object in container on the first node, expected allow"):
with expect_not_raises():
delete_object(default_wallet, container, object_id, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="allowDeleteObject",
)

View file

@ -1,333 +0,0 @@
import allure
import pytest
from frostfs_testlib.cli import FrostfsCli
from frostfs_testlib.reporter import get_reporter
from frostfs_testlib.resources.error_patterns import OBJECT_ACCESS_DENIED, RULE_ACCESS_DENIED_OBJECT
from frostfs_testlib.steps.cli.object import delete_object, get_object, get_range, get_range_hash, head_object, put_object, search_object
from frostfs_testlib.storage.dataclasses.ape import Operations
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import expect_not_raises
from frostfs_testlib.utils.file_utils import generate_file
from ...helpers.container_request import APE_EVERYONE_ALLOW_ALL, ContainerRequest
reporter = get_reporter()
REP2 = ContainerRequest("REP 2", ape_rules=APE_EVERYONE_ALLOW_ALL, short_name="REP2_allow_all_ape")
@pytest.mark.ape
@pytest.mark.ape_local
@pytest.mark.ape_object
@pytest.mark.ape_deny
class TestApeLocalOverrideDeny(ClusterTestBase):
@allure.title("LocalOverride: Deny to GetObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_get_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyGetObject",
rule=f"deny Object.Get /{container}/*",
)
with reporter.step("Put object in container on the first node"):
oid = put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_OBJECT):
get_object(default_wallet, container, oid, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get object from container on the second node, expected allow"):
with expect_not_raises():
get_object(default_wallet, container, oid, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyGetObject",
)
with reporter.step("Check get object in container on the first node, expected allow"):
with expect_not_raises():
get_object(default_wallet, container, oid, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
@allure.title("LocalOverride: Deny to PutObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_put_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyPutObject",
rule=f"deny Object.Put /{container}/*",
)
with reporter.step("Check put object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=OBJECT_ACCESS_DENIED):
put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check put object from container on the second node, expected allow"):
with expect_not_raises():
put_object(
default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint(), copies_number=3
)
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyPutObject",
)
with reporter.step("Check get object in container on the first node, expected allow"):
with expect_not_raises():
put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
@allure.title("LocalOverride: Deny to HeadObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_head_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyHeadObject",
rule=f"deny Object.Head /{container}/*",
)
with reporter.step("Put object in container on the first node"):
oid = put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check head object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_OBJECT):
head_object(default_wallet, container, oid, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check head object from container on the second node, expected allow"):
with expect_not_raises():
head_object(default_wallet, container, oid, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyHeadObject",
)
with reporter.step("Check head object in container on the first node, expected allow"):
with expect_not_raises():
head_object(default_wallet, container, oid, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
@allure.title("LocalOverride: Deny to SearchObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_search_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
container: str,
):
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denySearchObject",
rule=f"deny Object.Search /{container}/*",
)
with reporter.step("Check search object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_OBJECT.format(operation=Operations.SEARCH_OBJECT)):
search_object(default_wallet, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check search object from container on the second node, expected allow"):
with expect_not_raises():
search_object(default_wallet, container, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denySearchObject",
)
with reporter.step("Check search object in container on the first node, expected allow"):
with expect_not_raises():
search_object(default_wallet, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
@allure.title("LocalOverride: Deny to RangeObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_range_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyRangeObject",
rule=f"deny Object.Range /{container}/*",
)
with reporter.step("Put object in container on the first node"):
oid = put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check range object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_OBJECT.format(operation=Operations.RANGE_OBJECT)):
get_range(default_wallet, container, oid, "0:10", self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get range object from container on the second node, expected allow"):
with expect_not_raises():
get_range(default_wallet, container, oid, "0:10", self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyRangeObject",
)
with reporter.step("Check get range object in container on the first node, expected allow"):
with expect_not_raises():
get_range(default_wallet, container, oid, "0:10", self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
@allure.title("LocalOverride: Deny to HashObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_hash_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyHashObject",
rule=f"deny Object.Hash /{container}/*",
)
with reporter.step("Put object in container on the first node"):
oid = put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get range hash object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_OBJECT.format(operation=Operations.HASH_OBJECT)):
get_range_hash(default_wallet, container, oid, "0:10", self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check get range hash object from container on the second node, expected allow"):
with expect_not_raises():
get_range_hash(default_wallet, container, oid, "0:10", self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyHashObject",
)
with reporter.step("Check get range hash object in container on the first node, expected allow"):
with expect_not_raises():
get_range_hash(default_wallet, container, oid, "0:10", self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
@allure.title("LocalOverride: Deny to DeleteObject in root tenant")
@pytest.mark.parametrize("container_request", [REP2], indirect=True)
def test_local_override_deny_to_delete_object_root(
self,
default_wallet: WalletInfo,
frostfs_cli_on_first_node: FrostfsCli,
simple_object_size: ObjectSize,
container: str,
):
test_file = generate_file(simple_object_size.value)
with reporter.step("Create local override on first node"):
frostfs_cli_on_first_node.control.add_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyDeleteObject",
rule=f"deny Object.Delete /{container}/*",
)
with reporter.step("Put objects in container on the first node"):
oid_1 = put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
oid_2 = put_object(default_wallet, test_file, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Search object in container on the first node"):
search_object_in_container_1 = search_object(
default_wallet, container, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint()
)
assert oid_1 in search_object_in_container_1, f"Object {oid_1} was not found"
assert oid_2 in search_object_in_container_1, f"Object {oid_2} was not found"
with reporter.step("Search object from container on the second node"):
search_object_in_container_2 = search_object(
default_wallet, container, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint()
)
assert oid_1 in search_object_in_container_2, f"Object {oid_1} was not found"
assert oid_2 in search_object_in_container_2, f"Object {oid_2} was not found"
with reporter.step("Check delete object from container on the first node, expected access denied error"):
with pytest.raises(RuntimeError, match=RULE_ACCESS_DENIED_OBJECT):
delete_object(default_wallet, container, oid_1, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())
with reporter.step("Check delete object from container on the second node, expected allow"):
with expect_not_raises():
delete_object(default_wallet, container, oid_2, self.shell, self.cluster.storage_nodes[1].get_rpc_endpoint())
with reporter.step("Delete a rule"):
frostfs_cli_on_first_node.control.remove_rule(
endpoint=self.cluster.storage_nodes[0].get_control_endpoint(),
target_type="container",
target_name=container,
chain_id="denyDeleteObject",
)
with reporter.step("Check delete object in container on the first node, expected allow"):
with expect_not_raises():
delete_object(default_wallet, container, oid_1, self.shell, self.cluster.storage_nodes[0].get_rpc_endpoint())

View file

@ -1,5 +1,7 @@
import logging import logging
import os
import random import random
import shutil
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
@ -18,7 +20,6 @@ from frostfs_testlib.s3.interfaces import BucketContainerResolver
from frostfs_testlib.shell import LocalShell, Shell from frostfs_testlib.shell import LocalShell, Shell
from frostfs_testlib.steps.cli.container import DEFAULT_EC_PLACEMENT_RULE, DEFAULT_PLACEMENT_RULE, FROSTFS_CLI_EXEC from frostfs_testlib.steps.cli.container import DEFAULT_EC_PLACEMENT_RULE, DEFAULT_PLACEMENT_RULE, FROSTFS_CLI_EXEC
from frostfs_testlib.steps.cli.object import get_netmap_netinfo from frostfs_testlib.steps.cli.object import get_netmap_netinfo
from frostfs_testlib.steps.epoch import ensure_fresh_epoch
from frostfs_testlib.steps.s3 import s3_helper from frostfs_testlib.steps.s3 import s3_helper
from frostfs_testlib.storage.cluster import Cluster, ClusterNode from frostfs_testlib.storage.cluster import Cluster, ClusterNode
from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController
@ -30,12 +31,10 @@ from frostfs_testlib.storage.grpc_operations.client_wrappers import CliClientWra
from frostfs_testlib.storage.grpc_operations.interfaces import GrpcClientWrapper from frostfs_testlib.storage.grpc_operations.interfaces import GrpcClientWrapper
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.parallel import parallel from frostfs_testlib.testing.parallel import parallel
from frostfs_testlib.testing.test_control import cached_fixture, run_optionally, wait_for_success from frostfs_testlib.testing.test_control import run_optionally, wait_for_success
from frostfs_testlib.utils import env_utils, string_utils, version_utils from frostfs_testlib.utils import env_utils, string_utils, version_utils
from frostfs_testlib.utils.file_utils import TestFile, generate_file from frostfs_testlib.utils.file_utils import TestFile, generate_file
from ..helpers.container_creation import create_container_with_ape, create_containers_with_ape
from ..helpers.container_request import EVERYONE_ALLOW_ALL, ContainerRequest, MultipleContainersRequest
from ..resources.common import TEST_CYCLES_COUNT from ..resources.common import TEST_CYCLES_COUNT
logger = logging.getLogger("NeoLogger") logger = logging.getLogger("NeoLogger")
@ -142,16 +141,15 @@ def require_multiple_interfaces(cluster: Cluster):
interfaces = cluster.cluster_nodes[0].host.config.interfaces interfaces = cluster.cluster_nodes[0].host.config.interfaces
if "internal1" not in interfaces or "data1" not in interfaces: if "internal1" not in interfaces or "data1" not in interfaces:
pytest.skip("This test requires multiple internal and data interfaces") pytest.skip("This test requires multiple internal and data interfaces")
return yield
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@cached_fixture(optionals.OPTIONAL_CACHE_FIXTURES)
def max_object_size(cluster: Cluster, client_shell: Shell) -> int: def max_object_size(cluster: Cluster, client_shell: Shell) -> int:
storage_node = cluster.storage_nodes[0] storage_node = cluster.storage_nodes[0]
wallet = WalletInfo.from_node(storage_node) wallet = WalletInfo.from_node(storage_node)
net_info = get_netmap_netinfo(wallet=wallet, endpoint=storage_node.get_rpc_endpoint(), shell=client_shell) net_info = get_netmap_netinfo(wallet=wallet, endpoint=storage_node.get_rpc_endpoint(), shell=client_shell)
return net_info["maximum_object_size"] yield net_info["maximum_object_size"]
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@ -160,6 +158,11 @@ def simple_object_size(max_object_size: int) -> ObjectSize:
return ObjectSize("simple", size) return ObjectSize("simple", size)
@pytest.fixture()
def file_path(object_size: ObjectSize) -> TestFile:
return generate_file(object_size.value)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def complex_object_size(max_object_size: int) -> ObjectSize: def complex_object_size(max_object_size: int) -> ObjectSize:
size = max_object_size * int(COMPLEX_OBJECT_CHUNKS_COUNT) + int(COMPLEX_OBJECT_TAIL_SIZE) size = max_object_size * int(COMPLEX_OBJECT_CHUNKS_COUNT) + int(COMPLEX_OBJECT_TAIL_SIZE)
@ -178,22 +181,6 @@ def object_size(simple_object_size: ObjectSize, complex_object_size: ObjectSize,
return complex_object_size return complex_object_size
@pytest.fixture()
def test_file(object_size: ObjectSize) -> TestFile:
return generate_file(object_size.value)
@pytest.fixture(scope="module")
def test_file_module(object_size: ObjectSize) -> TestFile:
return generate_file(object_size.value)
# Deprecated. Please migrate all to test_file
@pytest.fixture()
def file_path(test_file: TestFile) -> TestFile:
return test_file
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def rep_placement_policy() -> PlacementPolicy: def rep_placement_policy() -> PlacementPolicy:
return PlacementPolicy("rep", DEFAULT_PLACEMENT_RULE) return PlacementPolicy("rep", DEFAULT_PLACEMENT_RULE)
@ -224,10 +211,8 @@ def placement_policy(
) -> PlacementPolicy: ) -> PlacementPolicy:
if request.param == "rep": if request.param == "rep":
return rep_placement_policy return rep_placement_policy
elif request.param == "ec":
return ec_placement_policy
return request.param return ec_placement_policy
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@ -433,7 +418,6 @@ def readiness_on_node(cluster_node: ClusterNode):
@reporter.step("Prepare default user with wallet") @reporter.step("Prepare default user with wallet")
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@cached_fixture(optionals.OPTIONAL_CACHE_FIXTURES)
def default_user(credentials_provider: CredentialsProvider, cluster: Cluster) -> User: def default_user(credentials_provider: CredentialsProvider, cluster: Cluster) -> User:
user = User(string_utils.unique_name("user-")) user = User(string_utils.unique_name("user-"))
node = cluster.cluster_nodes[0] node = cluster.cluster_nodes[0]
@ -450,7 +434,6 @@ def default_wallet(default_user: User) -> WalletInfo:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@cached_fixture(optionals.OPTIONAL_CACHE_FIXTURES)
def wallets_pool(credentials_provider: CredentialsProvider, cluster: Cluster) -> list[WalletInfo]: def wallets_pool(credentials_provider: CredentialsProvider, cluster: Cluster) -> list[WalletInfo]:
users = [User(string_utils.unique_name("user-")) for _ in range(WALLTETS_IN_POOL)] users = [User(string_utils.unique_name("user-")) for _ in range(WALLTETS_IN_POOL)]
parallel(credentials_provider.GRPC.provide, users, cluster_node=cluster.cluster_nodes[0]) parallel(credentials_provider.GRPC.provide, users, cluster_node=cluster.cluster_nodes[0])
@ -486,71 +469,3 @@ def bucket_container_resolver(node_under_test: ClusterNode) -> BucketContainerRe
resolver_cls = plugins.load_plugin("frostfs.testlib.bucket_cid_resolver", node_under_test.host.config.product) resolver_cls = plugins.load_plugin("frostfs.testlib.bucket_cid_resolver", node_under_test.host.config.product)
resolver: BucketContainerResolver = resolver_cls() resolver: BucketContainerResolver = resolver_cls()
return resolver return resolver
@pytest.fixture(scope="session", params=[pytest.param(EVERYONE_ALLOW_ALL)])
def container_request(request: pytest.FixtureRequest) -> ContainerRequest:
if "param" in request.__dict__:
return request.param
container_marker = request.node.get_closest_marker("container")
# let default container to be public at the moment
container_request = EVERYONE_ALLOW_ALL
if container_marker:
if len(container_marker.args) != 1:
raise RuntimeError(f"Something wrong with container marker: {container_marker}")
container_request = container_marker.args[0]
if not container_request:
raise RuntimeError(
f"""Container specification is empty.
Add @pytest.mark.parametrize("container_request", [ContainerRequest(...)], indirect=True) decorator."""
)
return container_request
@pytest.fixture(scope="session")
def multiple_containers_request(request: pytest.FixtureRequest) -> ContainerRequest:
if "param" in request.__dict__:
return request.param
raise RuntimeError(
f"""Container specification is empty.
Add @pytest.mark.parametrize("container_requests", [[ContainerRequest(...), ..., ContainerRequest(...)]], indirect=True) decorator."""
)
@pytest.fixture
def container(
default_wallet: WalletInfo,
frostfs_cli: FrostfsCli,
client_shell: Shell,
cluster: Cluster,
rpc_endpoint: str,
container_request: ContainerRequest,
) -> str:
return create_container_with_ape(container_request, frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint)
@pytest.fixture
def containers(
default_wallet: WalletInfo,
frostfs_cli: FrostfsCli,
client_shell: Shell,
cluster: Cluster,
rpc_endpoint: str,
multiple_containers_request: MultipleContainersRequest,
) -> list[str]:
return create_containers_with_ape(frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint, multiple_containers_request)
@pytest.fixture()
def new_epoch(client_shell: Shell, cluster: Cluster) -> int:
return ensure_fresh_epoch(client_shell, cluster)
@pytest.fixture(scope="module")
def new_epoch_module_scope(client_shell: Shell, cluster: Cluster) -> int:
return ensure_fresh_epoch(client_shell, cluster)

View file

@ -20,23 +20,35 @@ from ...helpers.utility import placement_policy_from_container
@pytest.mark.sanity @pytest.mark.sanity
@pytest.mark.container @pytest.mark.container
class TestContainer(ClusterTestBase): class TestContainer(ClusterTestBase):
PLACEMENT_RULE = "REP 2 IN X CBF 1 SELECT 2 FROM * AS X"
@allure.title("Create container (name={name})") @allure.title("Create container (name={name})")
@pytest.mark.parametrize("name", ["", "test-container"], ids=["No name", "Set particular name"]) @pytest.mark.parametrize("name", ["", "test-container"], ids=["No name", "Set particular name"])
@pytest.mark.smoke @pytest.mark.smoke
def test_container_creation(self, default_wallet: WalletInfo, name: str, rpc_endpoint: str): def test_container_creation(self, default_wallet: WalletInfo, name: str):
wallet = default_wallet wallet = default_wallet
cid = create_container(wallet, self.shell, rpc_endpoint, self.PLACEMENT_RULE, name=name) placement_rule = "REP 2 IN X CBF 1 SELECT 2 FROM * AS X"
cid = create_container(
wallet,
rule=placement_rule,
name=name,
shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint,
)
containers = list_containers(wallet, self.shell, rpc_endpoint) containers = list_containers(wallet, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint)
assert cid in containers, f"Expected container {cid} in containers: {containers}" assert cid in containers, f"Expected container {cid} in containers: {containers}"
container_info: str = get_container(wallet, cid, self.shell, rpc_endpoint, False) container_info: str = get_container(
wallet,
cid,
json_mode=False,
shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint,
)
container_info = container_info.casefold() # To ignore case when comparing with expected values container_info = container_info.casefold() # To ignore case when comparing with expected values
info_to_check = { info_to_check = {
f"basic ACL: {PRIVATE_ACL_F} (private)",
f"owner ID: {wallet.get_address_from_json(0)}", f"owner ID: {wallet.get_address_from_json(0)}",
f"CID: {cid}", f"CID: {cid}",
} }
@ -44,7 +56,7 @@ class TestContainer(ClusterTestBase):
info_to_check.add(f"Name={name}") info_to_check.add(f"Name={name}")
with reporter.step("Check container has correct information"): with reporter.step("Check container has correct information"):
expected_policy = self.PLACEMENT_RULE.casefold() expected_policy = placement_rule.casefold()
actual_policy = placement_policy_from_container(container_info) actual_policy = placement_policy_from_container(container_info)
assert actual_policy == expected_policy, f"Expected policy\n{expected_policy} but got policy\n{actual_policy}" assert actual_policy == expected_policy, f"Expected policy\n{expected_policy} but got policy\n{actual_policy}"
@ -53,42 +65,50 @@ class TestContainer(ClusterTestBase):
assert expected_info in container_info, f"Expected {expected_info} in container info:\n{container_info}" assert expected_info in container_info, f"Expected {expected_info} in container info:\n{container_info}"
with reporter.step("Delete container and check it was deleted"): with reporter.step("Delete container and check it was deleted"):
# Force to skip frostfs-cli verifictions before delete. delete_container(
# Because no APE rules assigned to container, those verifications will fail due to APE requests denial. wallet,
delete_container(wallet, cid, self.shell, rpc_endpoint, force=True, await_mode=True) cid,
shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint,
await_mode=True,
)
self.tick_epoch() self.tick_epoch()
wait_for_container_deletion(wallet, cid, self.shell, rpc_endpoint) wait_for_container_deletion(wallet, cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint)
@allure.title("Delete container without force (name={name})")
@pytest.mark.smoke
def test_container_deletion_no_force(self, container: str, default_wallet: WalletInfo, rpc_endpoint: str):
with reporter.step("Delete container and check it was deleted"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=True)
self.tick_epoch()
wait_for_container_deletion(default_wallet, container, self.shell, rpc_endpoint)
@allure.title("Parallel container creation and deletion") @allure.title("Parallel container creation and deletion")
def test_container_creation_deletion_parallel(self, default_wallet: WalletInfo, rpc_endpoint: str): def test_container_creation_deletion_parallel(self, default_wallet: WalletInfo):
containers_count = 3 containers_count = 3
wallet = default_wallet wallet = default_wallet
placement_rule = "REP 2 IN X CBF 1 SELECT 2 FROM * AS X"
iteration_count = 10 iteration_count = 10
for _ in range(iteration_count): for iteration in range(iteration_count):
cids: list[str] = [] cids: list[str] = []
with reporter.step(f"Create {containers_count} containers"): with reporter.step(f"Create {containers_count} containers"):
for _ in range(containers_count): for _ in range(containers_count):
cids.append( cids.append(
create_container(wallet, self.shell, rpc_endpoint, self.PLACEMENT_RULE, await_mode=False, wait_for_creation=False) create_container(
wallet,
rule=placement_rule,
await_mode=False,
shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint,
wait_for_creation=False,
)
) )
with reporter.step("Wait for containers occur in container list"): with reporter.step("Wait for containers occur in container list"):
for cid in cids: for cid in cids:
wait_for_container_creation(wallet, cid, self.shell, rpc_endpoint, sleep_interval=containers_count) wait_for_container_creation(
wallet,
cid,
sleep_interval=containers_count,
shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint,
)
with reporter.step("Delete containers and check they were deleted"): with reporter.step("Delete containers and check they were deleted"):
for cid in cids: for cid in cids:
# Force to skip frostfs-cli verifictions before delete. delete_container(wallet, cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, await_mode=True)
# Because no APE rules assigned to container, those verifications will fail due to APE requests denial. containers_list = list_containers(wallet, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint)
delete_container(wallet, cid, self.shell, rpc_endpoint, force=True, await_mode=True)
containers_list = list_containers(wallet, self.shell, rpc_endpoint)
assert cid not in containers_list, "Container not deleted" assert cid not in containers_list, "Container not deleted"

File diff suppressed because it is too large Load diff

View file

@ -1,640 +0,0 @@
import itertools
import allure
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.shell.interfaces import Shell
from frostfs_testlib.steps.cli.container import delete_container
from frostfs_testlib.steps.cli.object import delete_object, put_object_to_random_node
from frostfs_testlib.steps.node_management import get_netmap_snapshot
from frostfs_testlib.steps.storage_policy import get_nodes_with_object
from frostfs_testlib.storage.cluster import Cluster
from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController
from frostfs_testlib.storage.controllers.state_managers.config_state_manager import ConfigStateManager
from frostfs_testlib.storage.dataclasses.frostfs_services import StorageNode
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing import parallel
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import wait_for_success
from frostfs_testlib.utils.cli_utils import parse_netmap_output
from frostfs_testlib.utils.file_utils import generate_file
from ...helpers.container_creation import create_container_with_ape
from ...helpers.container_request import PUBLIC_WITH_POLICY, ContainerRequest
from ...helpers.policy_validation import get_netmap_param, validate_object_policy
@pytest.mark.weekly
@pytest.mark.policy
@pytest.mark.policy_price
class TestPolicyWithPrice(ClusterTestBase):
@wait_for_success(1200, 60, title="Wait for full field price on node", expected_result=True)
def await_for_price_attribute_on_nodes(self):
netmap = parse_netmap_output(get_netmap_snapshot(node=self.cluster.storage_nodes[0], shell=self.shell))
netmap = get_netmap_param(netmap)
for node in self.cluster.storage_nodes:
node_address = node.get_rpc_endpoint().split(":")[0]
if netmap[node_address]["Price"] is None:
return False
return True
@pytest.fixture(scope="module")
def fill_field_price(self, cluster: Cluster, cluster_state_controller_session: ClusterStateController):
prices = ["15", "10", "65", "55"]
config_manager = cluster_state_controller_session.manager(ConfigStateManager)
parallel(
config_manager.set_on_node,
cluster.cluster_nodes,
StorageNode,
itertools.cycle([{"node:attribute_5": f"Price:{price}"} for price in prices]),
)
cluster_state_controller_session.wait_after_storage_startup()
self.tick_epoch()
self.await_for_price_attribute_on_nodes()
yield
cluster_state_controller_session.manager(ConfigStateManager).revert_all()
@pytest.fixture
def container(
self,
default_wallet: WalletInfo,
frostfs_cli: FrostfsCli,
client_shell: Shell,
cluster: Cluster,
rpc_endpoint: str,
container_request: ContainerRequest,
# In these set of tests containers should be created after the fill_field_price fixture
fill_field_price,
) -> str:
return create_container_with_ape(container_request, frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint)
@allure.title("Policy with SELECT and FILTER results with 25% of available nodes")
@pytest.mark.parametrize(
"container_request",
[PUBLIC_WITH_POLICY("REP 1 IN Nodes25 SELECT 1 FROM LE10 AS Nodes25 FILTER Price LE 10 AS LE10")],
indirect=True,
)
def test_policy_with_select_and_filter_results_with_25_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and FILTER results with 25% of available nodes.
"""
placement_params = {"Price": 10}
file_path = generate_file(simple_object_size.value)
expected_copies = 1
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
node_address = resulting_copies[0].get_rpc_endpoint().split(":")[0]
with reporter.step(f"Check the node is selected with price <= {placement_params['Price']}"):
assert (
int(netmap[node_address]["Price"]) <= placement_params["Price"]
), f"The node is selected with the wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with select and complex filter results with 25% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"REP 1 IN Nodes25 SELECT 1 FROM BET0AND10 AS Nodes25 FILTER Price LE 10 AS LE10 FILTER Price GT 0 AS GT0 FILTER @LE10 AND @GT0 AS BET0AND10"
)
],
indirect=True,
)
def test_policy_with_select_and_complex_filter_results_with_25_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and Complex FILTER results with 25% of available nodes.
"""
placement_params = {"Price": [10, 0]}
file_path = generate_file(simple_object_size.value)
expected_copies = 1
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check the node is selected with price between 1 and 10"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
int(netmap[node_address]["Price"]) > placement_params["Price"][1]
and int(netmap[node_address]["Price"]) <= placement_params["Price"][0]
), f"The node is selected with the wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with Multi SELECTs and FILTERs results with 25% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"UNIQUE REP 1 IN One REP 1 IN One CBF 1 SELECT 1 FROM MINMAX AS One FILTER Price LT 15 AS LT15 FILTER Price GT 55 AS GT55 FILTER @LT15 OR @GT55 AS MINMAX"
)
],
indirect=True,
)
def test_policy_with_multi_selects_and_filters_results_with_25_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with Multi SELECTs and FILTERs results with 25% of available nodes.
"""
placement_params = {"Price": [15, 55]}
file_path = generate_file(simple_object_size.value)
expected_copies = 2
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check two nodes are selected with max and min prices"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
int(netmap[node_address]["Price"]) > placement_params["Price"][1]
or int(netmap[node_address]["Price"]) < placement_params["Price"][0]
), f"The node is selected with the wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with SELECT and FILTER results with 50% of available nodes")
@pytest.mark.parametrize(
"container_request",
[PUBLIC_WITH_POLICY("REP 2 IN HALF CBF 1 SELECT 2 FROM GT15 AS HALF FILTER Price GT 15 AS GT15")],
indirect=True,
)
def test_policy_with_select_and_filter_results_with_50_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and FILTER results with 50% of available nodes.
"""
placement_params = {"Price": 15}
file_path = generate_file(simple_object_size.value)
expected_copies = 2
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check two nodes are selected with price > {placement_params['Price']}"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
int(netmap[node_address]["Price"]) > placement_params["Price"]
), f"The node is selected with the wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with SELECT and Complex FILTER results with 50% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"REP 2 IN HALF CBF 2 SELECT 2 FROM GE15 AS HALF FILTER CountryCode NE RU AS NOTRU FILTER @NOTRU AND Price GE 15 AS GE15"
)
],
indirect=True,
)
def test_policy_with_select_and_complex_filter_results_with_50_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and Complex FILTER results with 50% of available nodes.
"""
placement_params = {"Price": 15, "country_code": "RU"}
file_path = generate_file(simple_object_size.value)
expected_copies = 2
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check two nodes are selected not with country code '{placement_params['country_code']}'"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
not netmap[node_address]["country_code"] == placement_params["country_code"]
or not netmap[node_address]["country_code"] == placement_params["country_code"]
and int(netmap[node_address]["Price"]) >= placement_params["Price"]
), f"The node is selected with the wrong price or country code. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with Multi SELECTs and FILTERs results with 50% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"REP 2 IN FH REP 1 IN SH CBF 2 SELECT 2 FROM LE55 AS FH SELECT 2 FROM GE15 AS SH FILTER 'UN-LOCODE' EQ 'RU LED' OR 'UN-LOCODE' EQ 'RU MOW' AS RU FILTER NOT (@RU) AS NOTRU FILTER @NOTRU AND Price GE 15 AS GE15 FILTER @RU AND Price LE 55 AS LE55"
)
],
indirect=True,
)
def test_policy_with_multi_selects_and_filters_results_with_50_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with Multi SELECTs and FILTERs results with 50% of available nodes.
"""
placement_params = {"un_locode": ["RU LED", "RU MOW"], "Price": [15, 55]}
file_path = generate_file(simple_object_size.value)
expected_copies = 3
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check all nodes are selected"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
netmap[node_address]["un_locode"] in placement_params["un_locode"]
or not netmap[node_address]["un_locode"] == placement_params["un_locode"][1]
or (
not netmap[node_address]["un_locode"] == placement_params["un_locode"][1]
and int(netmap[node_address]["Price"]) >= placement_params["Price"][0]
)
or (
netmap[node_address]["un_locode"] == placement_params["un_locode"][1]
and int(netmap[node_address]["Price"]) <= placement_params["Price"][1]
)
), f"The node is selected with the wrong price or un_locode. Expected {placement_params} and got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with SELECT and FILTER results with 75% of available nodes")
@pytest.mark.parametrize(
"container_request",
[PUBLIC_WITH_POLICY("REP 2 IN NODES75 SELECT 2 FROM LT65 AS NODES75 FILTER Price LT 65 AS LT65")],
indirect=True,
)
def test_policy_with_select_and_filter_results_with_75_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and FILTER results with 75% of available nodes.
"""
placement_params = {"Price": 65}
file_path = generate_file(simple_object_size.value)
expected_copies = 2
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check two nodes are selected with price < {placement_params['Price']}"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
int(netmap[node_address]["Price"]) < placement_params["Price"]
), f"The node is selected with the wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with SELECT and Complex FILTER results with 75% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"REP 2 IN NODES75 SELECT 2 FROM LT65 AS NODES75 FILTER Continent NE America AS NOAM FILTER @NOAM AND Price LT 65 AS LT65"
)
],
indirect=True,
)
def test_policy_with_select_and_complex_filter_results_with_75_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and Complex FILTER results with 75% of available nodes.
"""
placement_params = {"Price": 65, "continent": "America"}
file_path = generate_file(simple_object_size.value)
expected_copies = 2
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check three nodes are selected not from {placement_params['continent']}"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
int(netmap[node_address]["Price"]) < placement_params["Price"]
and not netmap[node_address]["continent"] == placement_params["continent"]
) or (
not netmap[node_address]["continent"] == placement_params["continent"]
), f"The node is selected with the wrong price or continent. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with Multi SELECTs and FILTERs results with 75% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"REP 3 IN EXPNSV REP 3 IN CHEAP SELECT 3 FROM GT10 AS EXPNSV SELECT 3 FROM LT65 AS CHEAP FILTER NOT (Continent EQ America) AS NOAM FILTER @NOAM AND Price LT 65 AS LT65 FILTER @NOAM AND Price GT 10 AS GT10"
)
],
indirect=True,
)
def test_policy_with_multi_selects_and_filters_results_with_75_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with Multi SELECTs and FILTERs results with 75% of available nodes.
"""
placement_params = {"Price": [65, 10], "continent": "America"}
file_path = generate_file(simple_object_size.value)
expected_copies = 4
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check all nodes are selected"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (
(
int(netmap[node_address]["Price"]) > placement_params["Price"][1]
and not netmap[node_address]["continent"] == placement_params["continent"]
)
or (
int(netmap[node_address]["Price"]) < placement_params["Price"][0]
and not netmap[node_address]["continent"] == placement_params["continent"]
)
or not (netmap[node_address]["continent"] == placement_params["continent"])
), f"The node is selected with the wrong price or continent. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with SELECT and FILTER results with 100% of available nodes")
@pytest.mark.parametrize(
"container_request", [PUBLIC_WITH_POLICY("REP 1 IN All SELECT 4 FROM AllNodes AS All FILTER Price GE 0 AS AllNodes")], indirect=True
)
def test_policy_with_select_and_filter_results_with_100_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with SELECT and FILTER results with 100% of available nodes.
"""
placement_params = {"Price": 0}
file_path = generate_file(simple_object_size.value)
expected_copies = 1
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
node_address = resulting_copies[0].get_rpc_endpoint().split(":")[0]
with reporter.step(f"Check the node is selected with price >= {placement_params['Price']}"):
assert (
int(netmap[node_address]["Price"]) >= placement_params["Price"]
), f"The node is selected with the wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)
@allure.title("Policy with Multi SELECTs and FILTERs results with 100% of available nodes")
@pytest.mark.parametrize(
"container_request",
[
PUBLIC_WITH_POLICY(
"REP 4 IN AllOne REP 4 IN AllTwo CBF 4 SELECT 2 FROM GEZero AS AllOne SELECT 2 FROM AllCountries AS AllTwo FILTER Country EQ Russia OR Country EQ Sweden OR Country EQ Finland AS AllCountries FILTER Price GE 0 AS GEZero"
)
],
indirect=True,
)
def test_policy_with_multi_selects_and_filters_results_with_100_of_available_nodes(
self,
default_wallet: WalletInfo,
rpc_endpoint: str,
simple_object_size: ObjectSize,
container: str,
container_request: ContainerRequest,
):
"""
This test checks object's copies based on container's placement policy with Multi SELECTs and FILTERs results with 100% of available nodes.
"""
placement_params = {"country": ["Russia", "Sweden", "Finland"], "Price": 0}
file_path = generate_file(simple_object_size.value)
expected_copies = 4
with reporter.step(f"Check container policy"):
validate_object_policy(default_wallet, self.shell, container_request.policy, container, rpc_endpoint)
with reporter.step(f"Put object in container"):
oid = put_object_to_random_node(default_wallet, file_path, container, self.shell, self.cluster)
with reporter.step(f"Check object expected copies"):
resulting_copies = get_nodes_with_object(container, oid, shell=self.shell, nodes=self.cluster.storage_nodes)
assert len(resulting_copies) == expected_copies, f"Expected {expected_copies} copies, got {len(resulting_copies)}"
with reporter.step(f"Check the object appearance"):
netmap = parse_netmap_output(get_netmap_snapshot(node=resulting_copies[0], shell=self.shell))
netmap = get_netmap_param(netmap)
with reporter.step(f"Check all node are selected"):
for node in resulting_copies:
node_address = node.get_rpc_endpoint().split(":")[0]
assert (netmap[node_address]["country"] in placement_params["country"]) or (
int(netmap[node_address]["Price"]) >= placement_params["Price"]
), f"The node is selected from the wrong country or with wrong price. Got {netmap[node_address]}"
with reporter.step(f"Delete the object from the container"):
delete_object(default_wallet, container, oid, self.shell, rpc_endpoint)
with reporter.step(f"Delete the container"):
delete_container(default_wallet, container, self.shell, rpc_endpoint, await_mode=False)

View file

@ -364,7 +364,7 @@ class TestMaintenanceMode(ClusterTestBase):
cluster_state_controller: ClusterStateController, cluster_state_controller: ClusterStateController,
restore_node_status: list[ClusterNode], restore_node_status: list[ClusterNode],
): ):
with reporter.step("Create container and put object"): with reporter.step("Create container and create\put object"):
cid = create_container( cid = create_container(
wallet=default_wallet, wallet=default_wallet,
shell=self.shell, shell=self.shell,

View file

@ -1,9 +1,10 @@
import math import math
import allure import allure
from frostfs_testlib.testing.parallel import parallel
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.steps.cli.container import delete_container, search_nodes_with_container, wait_for_container_deletion from frostfs_testlib.steps.cli.container import create_container, delete_container, search_nodes_with_container, wait_for_container_deletion
from frostfs_testlib.steps.cli.object import delete_object, head_object, put_object_to_random_node from frostfs_testlib.steps.cli.object import delete_object, head_object, put_object_to_random_node
from frostfs_testlib.steps.metrics import calc_metrics_count_from_stdout, check_metrics_counter, get_metrics_value from frostfs_testlib.steps.metrics import calc_metrics_count_from_stdout, check_metrics_counter, get_metrics_value
from frostfs_testlib.steps.storage_policy import get_nodes_with_object from frostfs_testlib.steps.storage_policy import get_nodes_with_object
@ -11,21 +12,19 @@ from frostfs_testlib.storage.cluster import Cluster, ClusterNode
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.parallel import parallel from frostfs_testlib.utils.file_utils import generate_file
from frostfs_testlib.utils.file_utils import TestFile, generate_file
from ...helpers.container_request import PUBLIC_WITH_POLICY, ContainerRequest, requires_container
from ...helpers.utility import are_numbers_similar from ...helpers.utility import are_numbers_similar
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.metrics @pytest.mark.container
class TestContainerMetrics(ClusterTestBase): class TestContainerMetrics(ClusterTestBase):
@reporter.step("Put object to container: {cid}") @reporter.step("Put object to container: {cid}")
def put_object_parallel(self, file_path: str, wallet: WalletInfo, cid: str): def put_object_parallel(self, file_path: str, wallet: WalletInfo, cid: str):
oid = put_object_to_random_node(wallet, file_path, cid, self.shell, self.cluster) oid = put_object_to_random_node(wallet, file_path, cid, self.shell, self.cluster)
return oid return oid
@reporter.step("Get metrics value from node") @reporter.step("Get metrics value from node")
def get_metrics_search_by_greps_parallel(self, node: ClusterNode, **greps): def get_metrics_search_by_greps_parallel(self, node: ClusterNode, **greps):
try: try:
@ -34,183 +33,182 @@ class TestContainerMetrics(ClusterTestBase):
except Exception as e: except Exception as e:
return None return None
@allure.title("Container metrics (obj_size={object_size}, policy={container_request})") @allure.title("Container metrics (obj_size={object_size},policy={policy})")
@pytest.mark.parametrize( @pytest.mark.parametrize("placement_policy, policy", [("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", "REP"), ("EC 1.1 CBF 1", "EC")])
"container_request, copies",
[
(PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP"), 2),
(PUBLIC_WITH_POLICY("EC 1.1 CBF 1", short_name="EC"), 1),
],
indirect=["container_request"],
)
def test_container_metrics( def test_container_metrics(
self, self,
object_size: ObjectSize, object_size: ObjectSize,
max_object_size: int, max_object_size: int,
default_wallet: WalletInfo, default_wallet: WalletInfo,
cluster: Cluster, cluster: Cluster,
copies: int, placement_policy: str,
container: str, policy: str,
test_file: TestFile,
container_request: ContainerRequest,
): ):
file_path = generate_file(object_size.value)
copies = 2 if policy == "REP" else 1
object_chunks = 1 object_chunks = 1
link_object = 0 link_object = 0
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, cluster.default_rpc_endpoint, placement_policy)
if object_size.value > max_object_size: if object_size.value > max_object_size:
object_chunks = math.ceil(object_size.value / max_object_size) object_chunks = math.ceil(object_size.value / max_object_size)
link_object = len(search_nodes_with_container(default_wallet, container, self.shell, cluster.default_rpc_endpoint, cluster)) link_object = len(search_nodes_with_container(default_wallet, cid, self.shell, cluster.default_rpc_endpoint, cluster))
with reporter.step("Put object to random node"): with reporter.step("Put object to random node"):
oid = put_object_to_random_node(default_wallet, test_file.path, container, self.shell, cluster) oid = put_object_to_random_node(
wallet=default_wallet,
path=file_path,
cid=cid,
shell=self.shell,
cluster=cluster,
)
with reporter.step("Get object nodes"): with reporter.step("Get object nodes"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, cluster.storage_nodes)
object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes] object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes]
with reporter.step("Check metric appears in node where the object is located"): with reporter.step("Check metric appears in node where the object is located"):
count_metrics = (object_chunks * copies) + link_object count_metrics = (object_chunks * copies) + link_object
if container_request.short_name == "EC": if policy == "EC":
count_metrics = (object_chunks * 2) + link_object count_metrics = (object_chunks * 2) + link_object
check_metrics_counter(object_nodes, counter_exp=count_metrics, command="container_objects_total", cid=container, type="phy") check_metrics_counter(object_nodes, counter_exp=count_metrics, command="container_objects_total", cid=cid, type="phy")
check_metrics_counter(object_nodes, counter_exp=count_metrics, command="container_objects_total", cid=container, type="logic") check_metrics_counter(object_nodes, counter_exp=count_metrics, command="container_objects_total", cid=cid, type="logic")
check_metrics_counter(object_nodes, counter_exp=copies, command="container_objects_total", cid=container, type="user") check_metrics_counter(object_nodes, counter_exp=copies, command="container_objects_total", cid=cid, type="user")
with reporter.step("Delete file, wait until gc remove object"): with reporter.step("Delete file, wait until gc remove object"):
delete_object(default_wallet, container, oid, self.shell, cluster.default_rpc_endpoint) delete_object(default_wallet, cid, oid, self.shell, cluster.default_rpc_endpoint)
with reporter.step(f"Check container metrics 'the counter should equal {len(object_nodes)}' in object nodes"): with reporter.step(f"Check container metrics 'the counter should equal {len(object_nodes)}' in object nodes"):
check_metrics_counter(object_nodes, counter_exp=len(object_nodes), command="container_objects_total", cid=container, type="phy") check_metrics_counter(object_nodes, counter_exp=len(object_nodes), command="container_objects_total", cid=cid, type="phy")
check_metrics_counter( check_metrics_counter(object_nodes, counter_exp=len(object_nodes), command="container_objects_total", cid=cid, type="logic")
object_nodes, counter_exp=len(object_nodes), command="container_objects_total", cid=container, type="logic" check_metrics_counter(object_nodes, counter_exp=0, command="container_objects_total", cid=cid, type="user")
)
check_metrics_counter(object_nodes, counter_exp=0, command="container_objects_total", cid=container, type="user")
with reporter.step("Check metrics(Phy, Logic, User) in each nodes"): with reporter.step("Check metrics(Phy, Logic, User) in each nodes"):
# Phy and Logic metrics are x2, because in rule 'CBF 2 SELECT 2 FROM', cbf2*sel2=4 # Phy and Logic metrics are 4, because in rule 'CBF 2 SELECT 2 FROM', cbf2*sel2=4
expect_metrics = copies * 2 expect_metrics = 4 if policy == "REP" else 2
check_metrics_counter(cluster.cluster_nodes, counter_exp=expect_metrics, command="container_objects_total", cid=cid, type="phy")
check_metrics_counter( check_metrics_counter(
cluster.cluster_nodes, counter_exp=expect_metrics, command="container_objects_total", cid=container, type="phy" cluster.cluster_nodes, counter_exp=expect_metrics, command="container_objects_total", cid=cid, type="logic"
) )
check_metrics_counter( check_metrics_counter(cluster.cluster_nodes, counter_exp=0, command="container_objects_total", cid=cid, type="user")
cluster.cluster_nodes, counter_exp=expect_metrics, command="container_objects_total", cid=container, type="logic"
)
check_metrics_counter(cluster.cluster_nodes, counter_exp=0, command="container_objects_total", cid=container, type="user")
@allure.title("Container size metrics (obj_size={object_size}, policy={container_request})") @allure.title("Container size metrics (obj_size={object_size},policy={policy})")
@requires_container( @pytest.mark.parametrize("placement_policy, policy", [("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", "REP"), ("EC 1.1 CBF 1", "EC")])
[PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP"), PUBLIC_WITH_POLICY("EC 1.1 CBF 1", short_name="EC")]
)
def test_container_size_metrics( def test_container_size_metrics(
self, self,
object_size: ObjectSize, object_size: ObjectSize,
default_wallet: WalletInfo, default_wallet: WalletInfo,
test_file: TestFile, placement_policy: str,
container: str, policy: str,
): ):
file_path = generate_file(object_size.value)
with reporter.step(f"Create container with policy {policy}"):
cid = create_container(default_wallet, self.shell, self.cluster.default_rpc_endpoint, placement_policy)
with reporter.step("Put object to random node"): with reporter.step("Put object to random node"):
oid = put_object_to_random_node(default_wallet, test_file.path, container, self.shell, self.cluster) oid = put_object_to_random_node(
wallet=default_wallet,
path=file_path,
cid=cid,
shell=self.shell,
cluster=self.cluster,
)
with reporter.step("Get object nodes"): with reporter.step("Get object nodes"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, self.cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, self.cluster.storage_nodes)
object_nodes = [ object_nodes = [
cluster_node for cluster_node in self.cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes cluster_node for cluster_node in self.cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes
] ]
with reporter.step("Check metric appears in all node where the object is located"): with reporter.step("Check metric appears in all node where the object is located"):
act_metric = sum( act_metric = sum(
[get_metrics_value(node, command="frostfs_node_engine_container_size_bytes", cid=container) for node in object_nodes] [get_metrics_value(node, command="frostfs_node_engine_container_size_bytes", cid=cid) for node in object_nodes]
) )
assert (act_metric // 2) == object_size.value assert (act_metric // 2) == object_size.value
with reporter.step("Delete file, wait until gc remove object"): with reporter.step("Delete file, wait until gc remove object"):
id_tombstone = delete_object(default_wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) id_tombstone = delete_object(default_wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
tombstone = head_object(default_wallet, container, id_tombstone, self.shell, self.cluster.default_rpc_endpoint) tombstone = head_object(default_wallet, cid, id_tombstone, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step(f"Check container size metrics"): with reporter.step(f"Check container size metrics"):
act_metric = get_metrics_value(object_nodes[0], command="frostfs_node_engine_container_size_bytes", cid=container) act_metric = get_metrics_value(object_nodes[0], command="frostfs_node_engine_container_size_bytes", cid=cid)
assert act_metric == int(tombstone["header"]["payloadLength"]) assert act_metric == int(tombstone["header"]["payloadLength"])
@allure.title("Container size metrics put {objects_count} objects (obj_size={object_size})") @allure.title("Container size metrics put {objects_count} objects (obj_size={object_size})")
@pytest.mark.parametrize("objects_count", [5, 10, 20]) @pytest.mark.parametrize("objects_count", [5, 10, 20])
@requires_container
def test_container_size_metrics_more_objects( def test_container_size_metrics_more_objects(
self, object_size: ObjectSize, default_wallet: WalletInfo, objects_count: int, container: str self,
object_size: ObjectSize,
default_wallet: WalletInfo,
objects_count: int
): ):
with reporter.step(f"Create container"):
cid = create_container(default_wallet, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step(f"Put {objects_count} objects"): with reporter.step(f"Put {objects_count} objects"):
files_path = [generate_file(object_size.value) for _ in range(objects_count)] files_path = [generate_file(object_size.value) for _ in range(objects_count)]
futures = parallel(self.put_object_parallel, files_path, wallet=default_wallet, cid=container) futures = parallel(self.put_object_parallel, files_path, wallet=default_wallet, cid=cid)
oids = [future.result() for future in futures] oids = [future.result() for future in futures]
with reporter.step("Check metric appears in all nodes"): with reporter.step("Check metric appears in all nodes"):
metric_values = [ metric_values = [get_metrics_value(node, command="frostfs_node_engine_container_size_bytes", cid=cid) for node in self.cluster.cluster_nodes]
get_metrics_value(node, command="frostfs_node_engine_container_size_bytes", cid=container) actual_value = sum(metric_values) // 2 # for policy REP 2, value divide by 2
for node in self.cluster.cluster_nodes
]
actual_value = sum(metric_values) // 2 # for policy REP 2, value divide by 2
expected_value = object_size.value * objects_count expected_value = object_size.value * objects_count
assert are_numbers_similar( assert are_numbers_similar(actual_value, expected_value, tolerance_percentage=2), "metric container size bytes value not correct"
actual_value, expected_value, tolerance_percentage=2
), "metric container size bytes value not correct"
with reporter.step("Delete file, wait until gc remove object"): with reporter.step("Delete file, wait until gc remove object"):
tombstones_size = 0 tombstones_size = 0
for oid in oids: for oid in oids:
tombstone_id = delete_object(default_wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) tombstone_id = delete_object(default_wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
tombstone = head_object(default_wallet, container, tombstone_id, self.shell, self.cluster.default_rpc_endpoint) tombstone = head_object(default_wallet, cid, tombstone_id, self.shell, self.cluster.default_rpc_endpoint)
tombstones_size += int(tombstone["header"]["payloadLength"]) tombstones_size += int(tombstone["header"]["payloadLength"])
with reporter.step(f"Check container size metrics, 'should be positive in all nodes'"): with reporter.step(f"Check container size metrics, 'should be positive in all nodes'"):
futures = parallel( futures = parallel(get_metrics_value, self.cluster.cluster_nodes, command="frostfs_node_engine_container_size_bytes", cid=cid)
get_metrics_value, self.cluster.cluster_nodes, command="frostfs_node_engine_container_size_bytes", cid=container
)
metrics_value_nodes = [future.result() for future in futures] metrics_value_nodes = [future.result() for future in futures]
for act_metric in metrics_value_nodes: for act_metric in metrics_value_nodes:
assert act_metric >= 0, "Metrics value is negative" assert act_metric >= 0, "Metrics value is negative"
assert sum(metrics_value_nodes) // len(self.cluster.cluster_nodes) == tombstones_size, "tomstone size of objects not correct" assert sum(metrics_value_nodes) // len(self.cluster.cluster_nodes) == tombstones_size, "tomstone size of objects not correct"
@allure.title("Container metrics (policy={container_request})")
@pytest.mark.parametrize( @allure.title("Container metrics (policy={policy})")
"container_request, copies", @pytest.mark.parametrize("placement_policy, policy", [("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", "REP"), ("EC 1.1 CBF 1", "EC")])
[
(PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP"), 2),
(PUBLIC_WITH_POLICY("EC 1.1 CBF 1", short_name="EC"), 1),
],
indirect=["container_request"],
)
def test_container_metrics_delete_complex_objects( def test_container_metrics_delete_complex_objects(
self, self,
complex_object_size: ObjectSize, complex_object_size: ObjectSize,
default_wallet: WalletInfo, default_wallet: WalletInfo,
cluster: Cluster, cluster: Cluster,
copies: int, placement_policy: str,
container: str, policy: str
container_request: ContainerRequest,
): ):
copies = 2 if policy == "REP" else 1
objects_count = 2 objects_count = 2
metric_name = "frostfs_node_engine_container_objects_total" metric_name = "frostfs_node_engine_container_objects_total"
with reporter.step(f"Create container"):
cid = create_container(default_wallet, self.shell, cluster.default_rpc_endpoint, rule=placement_policy)
with reporter.step(f"Put {objects_count} objects"): with reporter.step(f"Put {objects_count} objects"):
files_path = [generate_file(complex_object_size.value) for _ in range(objects_count)] files_path = [generate_file(complex_object_size.value) for _ in range(objects_count)]
futures = parallel(self.put_object_parallel, files_path, wallet=default_wallet, cid=container) futures = parallel(self.put_object_parallel, files_path, wallet=default_wallet, cid=cid)
oids = [future.result() for future in futures] oids = [future.result() for future in futures]
with reporter.step(f"Check metrics value in each nodes, should be {objects_count} for 'user'"): with reporter.step(f"Check metrics value in each nodes, should be {objects_count} for 'user'"):
check_metrics_counter( check_metrics_counter(cluster.cluster_nodes, counter_exp=objects_count * copies, command=metric_name, cid=cid, type="user")
cluster.cluster_nodes, counter_exp=objects_count * copies, command=metric_name, cid=container, type="user"
)
with reporter.step("Delete objects and container"): with reporter.step("Delete objects and container"):
for oid in oids: for oid in oids:
delete_object(default_wallet, container, oid, self.shell, cluster.default_rpc_endpoint) delete_object(default_wallet, cid, oid, self.shell, cluster.default_rpc_endpoint)
delete_container(default_wallet, container, self.shell, cluster.default_rpc_endpoint) delete_container(default_wallet, cid, self.shell, cluster.default_rpc_endpoint)
with reporter.step("Tick epoch and check container was deleted"): with reporter.step("Tick epoch and check container was deleted"):
self.tick_epoch() self.tick_epoch()
wait_for_container_deletion(default_wallet, container, shell=self.shell, endpoint=cluster.default_rpc_endpoint) wait_for_container_deletion(default_wallet, cid, shell=self.shell, endpoint=cluster.default_rpc_endpoint)
with reporter.step(f"Check metrics value in each nodes, should not be show any result"): with reporter.step(f"Check metrics value in each nodes, should not be show any result"):
futures = parallel(self.get_metrics_search_by_greps_parallel, cluster.cluster_nodes, command=metric_name, cid=container) futures = parallel(self.get_metrics_search_by_greps_parallel, cluster.cluster_nodes, command=metric_name, cid=cid)
metrics_results = [future.result() for future in futures if future.result() is not None] metrics_results = [future.result() for future in futures if future.result() is not None]
assert len(metrics_results) == 0, f"Metrics value is not empty in Prometheus, actual value in nodes: {metrics_results}" assert len(metrics_results) == 0, f"Metrics value is not empty in Prometheus, actual value in nodes: {metrics_results}"

View file

@ -1,9 +1,11 @@
import random
import re import re
import allure import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.steps.cli.object import delete_object, put_object_to_random_node from frostfs_testlib.steps.cli.container import create_container
from frostfs_testlib.steps.cli.object import delete_object, put_object, put_object_to_random_node
from frostfs_testlib.steps.metrics import check_metrics_counter, get_metrics_value from frostfs_testlib.steps.metrics import check_metrics_counter, get_metrics_value
from frostfs_testlib.steps.storage_policy import get_nodes_with_object from frostfs_testlib.steps.storage_policy import get_nodes_with_object
from frostfs_testlib.storage.cluster import Cluster, ClusterNode from frostfs_testlib.storage.cluster import Cluster, ClusterNode
@ -13,11 +15,8 @@ from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import wait_for_success from frostfs_testlib.testing.test_control import wait_for_success
from frostfs_testlib.utils.file_utils import generate_file from frostfs_testlib.utils.file_utils import generate_file
from ...helpers.container_request import PUBLIC_WITH_POLICY, requires_container
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.metrics
class TestGarbageCollectorMetrics(ClusterTestBase): class TestGarbageCollectorMetrics(ClusterTestBase):
@wait_for_success(interval=10) @wait_for_success(interval=10)
def check_metrics_in_node(self, cluster_node: ClusterNode, counter_exp: int, **metrics_greps: str): def check_metrics_in_node(self, cluster_node: ClusterNode, counter_exp: int, **metrics_greps: str):
@ -35,11 +34,9 @@ class TestGarbageCollectorMetrics(ClusterTestBase):
return sum(map(int, result)) return sum(map(int, result))
@allure.title("Garbage collector expire_at object") @allure.title("Garbage collector expire_at object")
@requires_container(PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP 2")) def test_garbage_collector_metrics_expire_at_object(self, simple_object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster):
def test_garbage_collector_metrics_expire_at_object(
self, simple_object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster, container: str
):
file_path = generate_file(simple_object_size.value) file_path = generate_file(simple_object_size.value)
placement_policy = "REP 2 IN X CBF 2 SELECT 2 FROM * AS X"
metrics_step = 1 metrics_step = 1
with reporter.step("Get current garbage collector metrics for each nodes"): with reporter.step("Get current garbage collector metrics for each nodes"):
@ -47,19 +44,22 @@ class TestGarbageCollectorMetrics(ClusterTestBase):
for node in cluster.cluster_nodes: for node in cluster.cluster_nodes:
metrics_counter[node] = get_metrics_value(node, command="frostfs_node_garbage_collector_marked_for_removal_objects_total") metrics_counter[node] = get_metrics_value(node, command="frostfs_node_garbage_collector_marked_for_removal_objects_total")
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, cluster.default_rpc_endpoint, placement_policy)
with reporter.step("Put object to random node with expire_at"): with reporter.step("Put object to random node with expire_at"):
current_epoch = self.get_epoch() current_epoch = self.get_epoch()
oid = put_object_to_random_node( oid = put_object_to_random_node(
default_wallet, default_wallet,
file_path, file_path,
container, cid,
self.shell, self.shell,
cluster, cluster,
expire_at=current_epoch + 1, expire_at=current_epoch + 1,
) )
with reporter.step("Get object nodes"): with reporter.step("Get object nodes"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, cluster.storage_nodes)
object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes] object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes]
with reporter.step("Tick Epoch"): with reporter.step("Tick Epoch"):
@ -77,11 +77,9 @@ class TestGarbageCollectorMetrics(ClusterTestBase):
) )
@allure.title("Garbage collector delete object") @allure.title("Garbage collector delete object")
@requires_container(PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP 2")) def test_garbage_collector_metrics_deleted_objects(self, simple_object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster):
def test_garbage_collector_metrics_deleted_objects(
self, simple_object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster, container: str
):
file_path = generate_file(simple_object_size.value) file_path = generate_file(simple_object_size.value)
placement_policy = "REP 2 IN X CBF 2 SELECT 2 FROM * AS X"
metrics_step = 1 metrics_step = 1
with reporter.step("Get current garbage collector metrics for each nodes"): with reporter.step("Get current garbage collector metrics for each nodes"):
@ -89,21 +87,24 @@ class TestGarbageCollectorMetrics(ClusterTestBase):
for node in cluster.cluster_nodes: for node in cluster.cluster_nodes:
metrics_counter[node] = get_metrics_value(node, command="frostfs_node_garbage_collector_deleted_objects_total") metrics_counter[node] = get_metrics_value(node, command="frostfs_node_garbage_collector_deleted_objects_total")
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, node.storage_node.get_rpc_endpoint(), placement_policy)
with reporter.step("Put object to random node"): with reporter.step("Put object to random node"):
oid = put_object_to_random_node( oid = put_object_to_random_node(
default_wallet, default_wallet,
file_path, file_path,
container, cid,
self.shell, self.shell,
cluster, cluster,
) )
with reporter.step("Get object nodes"): with reporter.step("Get object nodes"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, cluster.storage_nodes)
object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes] object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes]
with reporter.step("Delete file, wait until gc remove object"): with reporter.step("Delete file, wait until gc remove object"):
delete_object(default_wallet, container, oid, self.shell, node.storage_node.get_rpc_endpoint()) delete_object(default_wallet, cid, oid, self.shell, node.storage_node.get_rpc_endpoint())
with reporter.step(f"Check garbage collector metrics 'the counter should increase by {metrics_step}'"): with reporter.step(f"Check garbage collector metrics 'the counter should increase by {metrics_step}'"):
for node in object_nodes: for node in object_nodes:

View file

@ -19,7 +19,6 @@ from frostfs_testlib.utils.file_utils import generate_file
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.metrics
class TestGRPCMetrics(ClusterTestBase): class TestGRPCMetrics(ClusterTestBase):
@pytest.fixture @pytest.fixture
def disable_policer(self, cluster_state_controller: ClusterStateController): def disable_policer(self, cluster_state_controller: ClusterStateController):
@ -85,18 +84,22 @@ class TestGRPCMetrics(ClusterTestBase):
@allure.title("GRPC metrics object operations") @allure.title("GRPC metrics object operations")
def test_grpc_metrics_object_operations( def test_grpc_metrics_object_operations(
self, simple_object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster, container: str, disable_policer self, simple_object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster, disable_policer
): ):
file_path = generate_file(simple_object_size.value) file_path = generate_file(simple_object_size.value)
placement_policy = "REP 2 IN X CBF 1 SELECT 4 FROM * AS X"
with reporter.step("Select random node"): with reporter.step("Select random node"):
node = random.choice(cluster.cluster_nodes) node = random.choice(cluster.cluster_nodes)
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, node.storage_node.get_rpc_endpoint(), placement_policy)
with reporter.step("Get current gRPC metrics for method 'Put'"): with reporter.step("Get current gRPC metrics for method 'Put'"):
metrics_counter_put = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Put") metrics_counter_put = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Put")
with reporter.step("Put object to selected node"): with reporter.step("Put object to selected node"):
oid = put_object(default_wallet, file_path, container, self.shell, node.storage_node.get_rpc_endpoint()) oid = put_object(default_wallet, file_path, cid, self.shell, node.storage_node.get_rpc_endpoint())
with reporter.step(f"Check gRPC metrics method 'Put', 'the counter should increase by 1'"): with reporter.step(f"Check gRPC metrics method 'Put', 'the counter should increase by 1'"):
metrics_counter_put += 1 metrics_counter_put += 1
@ -112,7 +115,7 @@ class TestGRPCMetrics(ClusterTestBase):
metrics_counter_get = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Get") metrics_counter_get = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Get")
with reporter.step(f"Get object"): with reporter.step(f"Get object"):
get_object(default_wallet, container, oid, self.shell, node.storage_node.get_rpc_endpoint()) get_object(default_wallet, cid, oid, self.shell, node.storage_node.get_rpc_endpoint())
with reporter.step(f"Check gRPC metrics method=Get, 'the counter should increase by 1'"): with reporter.step(f"Check gRPC metrics method=Get, 'the counter should increase by 1'"):
metrics_counter_get += 1 metrics_counter_get += 1
@ -128,7 +131,7 @@ class TestGRPCMetrics(ClusterTestBase):
metrics_counter_search = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Search") metrics_counter_search = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Search")
with reporter.step(f"Search object"): with reporter.step(f"Search object"):
search_object(default_wallet, container, self.shell, node.storage_node.get_rpc_endpoint()) search_object(default_wallet, cid, self.shell, node.storage_node.get_rpc_endpoint())
with reporter.step(f"Check gRPC metrics method=Search, 'the counter should increase by 1'"): with reporter.step(f"Check gRPC metrics method=Search, 'the counter should increase by 1'"):
metrics_counter_search += 1 metrics_counter_search += 1
@ -144,7 +147,7 @@ class TestGRPCMetrics(ClusterTestBase):
metrics_counter_head = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Head") metrics_counter_head = get_metrics_value(node, command="grpc_server_handled_total", service="ObjectService", method="Head")
with reporter.step(f"Head object"): with reporter.step(f"Head object"):
head_object(default_wallet, container, oid, self.shell, node.storage_node.get_rpc_endpoint()) head_object(default_wallet, cid, oid, self.shell, node.storage_node.get_rpc_endpoint())
with reporter.step(f"Check gRPC metrics method=Head, 'the counter should increase by 1'"): with reporter.step(f"Check gRPC metrics method=Head, 'the counter should increase by 1'"):
metrics_counter_head += 1 metrics_counter_head += 1

View file

@ -6,7 +6,7 @@ import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.steps.metrics import get_metrics_value from frostfs_testlib.steps.metrics import get_metrics_value
from frostfs_testlib.storage.cluster import ClusterNode from frostfs_testlib.storage.cluster import Cluster, ClusterNode
from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController
from frostfs_testlib.storage.controllers.state_managers.config_state_manager import ConfigStateManager from frostfs_testlib.storage.controllers.state_managers.config_state_manager import ConfigStateManager
from frostfs_testlib.storage.dataclasses.frostfs_services import StorageNode from frostfs_testlib.storage.dataclasses.frostfs_services import StorageNode
@ -15,7 +15,6 @@ from frostfs_testlib.testing.test_control import wait_for_success
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.metrics
class TestLogsMetrics(ClusterTestBase): class TestLogsMetrics(ClusterTestBase):
@pytest.fixture @pytest.fixture
def revert_all(self, cluster_state_controller: ClusterStateController): def revert_all(self, cluster_state_controller: ClusterStateController):

View file

@ -4,73 +4,73 @@ import re
import allure import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.steps.cli.container import delete_container, search_nodes_with_container from frostfs_testlib.steps.cli.container import create_container, delete_container, search_nodes_with_container
from frostfs_testlib.steps.cli.object import delete_object, lock_object, put_object, put_object_to_random_node from frostfs_testlib.steps.cli.object import delete_object, lock_object, put_object, put_object_to_random_node
from frostfs_testlib.steps.metrics import check_metrics_counter, get_metrics_value from frostfs_testlib.steps.metrics import check_metrics_counter, get_metrics_value
from frostfs_testlib.steps.storage_policy import get_nodes_with_object from frostfs_testlib.steps.storage_policy import get_nodes_with_object
from frostfs_testlib.storage.cluster import Cluster, ClusterNode from frostfs_testlib.storage.cluster import Cluster, ClusterNode
from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.utils.file_utils import TestFile from frostfs_testlib.utils.file_utils import generate_file
from ...helpers.container_request import PUBLIC_WITH_POLICY, ContainerRequest, requires_container
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.metrics
class TestObjectMetrics(ClusterTestBase): class TestObjectMetrics(ClusterTestBase):
@allure.title("Object metrics of removed container (obj_size={object_size})") @allure.title("Object metrics of removed container (obj_size={object_size})")
@requires_container(PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X")) def test_object_metrics_removed_container(self, object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster):
def test_object_metrics_removed_container(self, default_wallet: WalletInfo, cluster: Cluster, container: str, test_file: TestFile): file_path = generate_file(object_size.value)
placement_policy = "REP 2 IN X CBF 2 SELECT 2 FROM * AS X"
copies = 2
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, cluster.default_rpc_endpoint, placement_policy)
with reporter.step("Put object to random node"): with reporter.step("Put object to random node"):
oid = put_object_to_random_node(default_wallet, test_file.path, container, self.shell, cluster) oid = put_object_to_random_node(default_wallet, file_path, cid, self.shell, cluster)
with reporter.step("Check metric appears in node where the object is located"): with reporter.step("Check metric appears in node where the object is located"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, cluster.storage_nodes)
object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes] object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes]
check_metrics_counter( check_metrics_counter(
object_nodes, object_nodes,
counter_exp=2, counter_exp=copies,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
with reporter.step("Delete container"): with reporter.step("Delete container"):
delete_container(default_wallet, container, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint) delete_container(default_wallet, cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint)
with reporter.step("Tick Epoch"): with reporter.step("Tick Epoch"):
self.tick_epochs(epochs_to_tick=2, wait_block=2) self.tick_epochs(epochs_to_tick=2, wait_block=2)
with reporter.step("Check metrics of removed containers doesn't appear in the storage node"): with reporter.step("Check metrics of removed containers doesn't appear in the storage node"):
check_metrics_counter( check_metrics_counter(object_nodes, counter_exp=0, command="frostfs_node_engine_container_objects_total", cid=cid, type="user")
object_nodes, counter_exp=0, command="frostfs_node_engine_container_objects_total", cid=container, type="user" check_metrics_counter(object_nodes, counter_exp=0, command="frostfs_node_engine_container_size_byte", cid=cid)
)
check_metrics_counter(object_nodes, counter_exp=0, command="frostfs_node_engine_container_size_byte", cid=container)
for node in object_nodes: for node in object_nodes:
all_metrics = node.metrics.storage.get_metrics_search_by_greps(command="frostfs_node_engine_container_size_byte") all_metrics = node.metrics.storage.get_metrics_search_by_greps(command="frostfs_node_engine_container_size_byte")
assert container not in all_metrics.stdout, "metrics of removed containers shouldn't appear in the storage node" assert cid not in all_metrics.stdout, "metrics of removed containers shouldn't appear in the storage node"
@allure.title("Object metrics, locked object (obj_size={object_size}, policy={container_request})") @allure.title("Object metrics, locked object (obj_size={object_size}, policy={placement_policy})")
@requires_container( @pytest.mark.parametrize("placement_policy", ["REP 1 IN X CBF 1 SELECT 1 FROM * AS X", "REP 2 IN X CBF 2 SELECT 2 FROM * AS X"])
[
PUBLIC_WITH_POLICY("REP 1 IN X CBF 1 SELECT 1 FROM * AS X", short_name="REP 1"),
PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP 2"),
]
)
def test_object_metrics_blocked_object( def test_object_metrics_blocked_object(
self, default_wallet: WalletInfo, cluster: Cluster, container: str, container_request: ContainerRequest, test_file: TestFile self, object_size: ObjectSize, default_wallet: WalletInfo, cluster: Cluster, placement_policy: str
): ):
metric_step = int(re.search(r"REP\s(\d+)", container_request.policy).group(1)) file_path = generate_file(object_size.value)
metric_step = int(re.search(r"REP\s(\d+)", placement_policy).group(1))
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, cluster.default_rpc_endpoint, placement_policy)
with reporter.step("Search container nodes"): with reporter.step("Search container nodes"):
container_nodes = search_nodes_with_container( container_nodes = search_nodes_with_container(
wallet=default_wallet, wallet=default_wallet,
cid=container, cid=cid,
shell=self.shell, shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint, endpoint=self.cluster.default_rpc_endpoint,
cluster=cluster, cluster=cluster,
@ -82,7 +82,7 @@ class TestObjectMetrics(ClusterTestBase):
objects_metric_counter += get_metrics_value(node, command="frostfs_node_engine_objects_total", type="user") objects_metric_counter += get_metrics_value(node, command="frostfs_node_engine_objects_total", type="user")
with reporter.step("Put object to container node"): with reporter.step("Put object to container node"):
oid = put_object(default_wallet, test_file.path, container, self.shell, container_nodes[0].storage_node.get_rpc_endpoint()) oid = put_object(default_wallet, file_path, cid, self.shell, container_nodes[0].storage_node.get_rpc_endpoint())
with reporter.step(f"Check metric user 'the counter should increase by {metric_step}'"): with reporter.step(f"Check metric user 'the counter should increase by {metric_step}'"):
objects_metric_counter += metric_step objects_metric_counter += metric_step
@ -96,12 +96,12 @@ class TestObjectMetrics(ClusterTestBase):
container_nodes, container_nodes,
counter_exp=metric_step, counter_exp=metric_step,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
with reporter.step("Delete object"): with reporter.step("Delete object"):
delete_object(default_wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) delete_object(default_wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step(f"Check metric user 'the counter should decrease by {metric_step}'"): with reporter.step(f"Check metric user 'the counter should decrease by {metric_step}'"):
objects_metric_counter -= metric_step objects_metric_counter -= metric_step
@ -115,16 +115,16 @@ class TestObjectMetrics(ClusterTestBase):
container_nodes, container_nodes,
counter_exp=0, counter_exp=0,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
with reporter.step("Put object and lock it to next epoch"): with reporter.step("Put object and lock it to next epoch"):
oid = put_object(default_wallet, test_file.path, container, self.shell, container_nodes[0].storage_node.get_rpc_endpoint()) oid = put_object(default_wallet, file_path, cid, self.shell, container_nodes[0].storage_node.get_rpc_endpoint())
current_epoch = self.get_epoch() current_epoch = self.get_epoch()
lock_object( lock_object(
default_wallet, default_wallet,
container, cid,
oid, oid,
self.shell, self.shell,
container_nodes[0].storage_node.get_rpc_endpoint(), container_nodes[0].storage_node.get_rpc_endpoint(),
@ -143,7 +143,7 @@ class TestObjectMetrics(ClusterTestBase):
container_nodes, container_nodes,
counter_exp=metric_step, counter_exp=metric_step,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
@ -157,7 +157,7 @@ class TestObjectMetrics(ClusterTestBase):
) )
with reporter.step("Delete object"): with reporter.step("Delete object"):
delete_object(default_wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) delete_object(default_wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step(f"Check metric user 'the counter should decrease by {metric_step}'"): with reporter.step(f"Check metric user 'the counter should decrease by {metric_step}'"):
objects_metric_counter -= metric_step objects_metric_counter -= metric_step
@ -171,7 +171,7 @@ class TestObjectMetrics(ClusterTestBase):
container_nodes, container_nodes,
counter_exp=0, counter_exp=0,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
@ -179,8 +179,8 @@ class TestObjectMetrics(ClusterTestBase):
current_epoch = self.get_epoch() current_epoch = self.get_epoch()
oid = put_object( oid = put_object(
default_wallet, default_wallet,
test_file.path, file_path,
container, cid,
self.shell, self.shell,
container_nodes[0].storage_node.get_rpc_endpoint(), container_nodes[0].storage_node.get_rpc_endpoint(),
expire_at=current_epoch + 1, expire_at=current_epoch + 1,
@ -198,7 +198,7 @@ class TestObjectMetrics(ClusterTestBase):
container_nodes, container_nodes,
counter_exp=metric_step, counter_exp=metric_step,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
@ -217,28 +217,31 @@ class TestObjectMetrics(ClusterTestBase):
container_nodes, container_nodes,
counter_exp=0, counter_exp=0,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
cid=container, cid=cid,
type="user", type="user",
) )
@allure.title("Object metrics, stop the node (obj_size={object_size})") @allure.title("Object metrics, stop the node (obj_size={object_size})")
@requires_container(PUBLIC_WITH_POLICY("REP 2 IN X CBF 2 SELECT 2 FROM * AS X", short_name="REP 2"))
def test_object_metrics_stop_node( def test_object_metrics_stop_node(
self, self,
object_size: ObjectSize,
default_wallet: WalletInfo, default_wallet: WalletInfo,
cluster_state_controller: ClusterStateController, cluster_state_controller: ClusterStateController,
container: str,
test_file: TestFile,
): ):
placement_policy = "REP 2 IN X CBF 2 SELECT 2 FROM * AS X"
file_path = generate_file(object_size.value)
copies = 2 copies = 2
with reporter.step(f"Create container with policy {placement_policy}"):
cid = create_container(default_wallet, self.shell, self.cluster.default_rpc_endpoint, placement_policy)
with reporter.step(f"Check object metrics in container 'should be zero'"): with reporter.step(f"Check object metrics in container 'should be zero'"):
check_metrics_counter( check_metrics_counter(
self.cluster.cluster_nodes, self.cluster.cluster_nodes,
counter_exp=0, counter_exp=0,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
type="user", type="user",
cid=container, cid=cid,
) )
with reporter.step("Get current metrics for each nodes"): with reporter.step("Get current metrics for each nodes"):
@ -247,10 +250,10 @@ class TestObjectMetrics(ClusterTestBase):
objects_metric_counter[node] = get_metrics_value(node, command="frostfs_node_engine_objects_total", type="user") objects_metric_counter[node] = get_metrics_value(node, command="frostfs_node_engine_objects_total", type="user")
with reporter.step("Put object"): with reporter.step("Put object"):
oid = put_object(default_wallet, test_file.path, container, self.shell, self.cluster.default_rpc_endpoint) oid = put_object(default_wallet, file_path, cid, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step("Get object nodes"): with reporter.step("Get object nodes"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, self.cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, self.cluster.storage_nodes)
object_nodes = [ object_nodes = [
cluster_node for cluster_node in self.cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes cluster_node for cluster_node in self.cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes
] ]
@ -263,7 +266,7 @@ class TestObjectMetrics(ClusterTestBase):
counter_exp=copies, counter_exp=copies,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
type="user", type="user",
cid=container, cid=cid,
) )
with reporter.step(f"Select node to stop"): with reporter.step(f"Select node to stop"):
@ -287,5 +290,5 @@ class TestObjectMetrics(ClusterTestBase):
counter_exp=copies, counter_exp=copies,
command="frostfs_node_engine_container_objects_total", command="frostfs_node_engine_container_objects_total",
type="user", type="user",
cid=container, cid=cid,
) )

View file

@ -5,6 +5,8 @@ import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.resources.error_patterns import OBJECT_NOT_FOUND from frostfs_testlib.resources.error_patterns import OBJECT_NOT_FOUND
from frostfs_testlib.resources.wellknown_acl import EACL_PUBLIC_READ_WRITE
from frostfs_testlib.steps.cli.container import create_container
from frostfs_testlib.steps.cli.object import get_object, put_object from frostfs_testlib.steps.cli.object import get_object, put_object
from frostfs_testlib.steps.metrics import check_metrics_counter from frostfs_testlib.steps.metrics import check_metrics_counter
from frostfs_testlib.steps.node_management import node_shard_list, node_shard_set_mode from frostfs_testlib.steps.node_management import node_shard_list, node_shard_set_mode
@ -16,11 +18,8 @@ from frostfs_testlib.testing import parallel, wait_for_success
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.utils.file_utils import generate_file from frostfs_testlib.utils.file_utils import generate_file
from ...helpers.container_request import PUBLIC_WITH_POLICY, requires_container
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.metrics
class TestShardMetrics(ClusterTestBase): class TestShardMetrics(ClusterTestBase):
@pytest.fixture() @pytest.fixture()
@allure.title("Get two shards for set mode") @allure.title("Get two shards for set mode")
@ -128,22 +127,28 @@ class TestShardMetrics(ClusterTestBase):
) )
@allure.title("Metric for error count on shard") @allure.title("Metric for error count on shard")
@requires_container(PUBLIC_WITH_POLICY("REP 1 CBF 1", short_name="REP 1")) def test_shard_metrics_error_count(self, max_object_size: int, default_wallet: WalletInfo, cluster: Cluster, revert_all_shards_mode):
def test_shard_metrics_error_count(
self, max_object_size: int, default_wallet: WalletInfo, cluster: Cluster, container: str, revert_all_shards_mode
):
file_path = generate_file(round(max_object_size * 0.8)) file_path = generate_file(round(max_object_size * 0.8))
with reporter.step(f"Create container"):
cid = create_container(
wallet=default_wallet,
shell=self.shell,
endpoint=cluster.default_rpc_endpoint,
rule="REP 1 CBF 1",
basic_acl=EACL_PUBLIC_READ_WRITE,
)
with reporter.step("Put object"): with reporter.step("Put object"):
oid = put_object(default_wallet, file_path, container, self.shell, cluster.default_rpc_endpoint) oid = put_object(default_wallet, file_path, cid, self.shell, cluster.default_rpc_endpoint)
with reporter.step("Get object nodes"): with reporter.step("Get object nodes"):
object_storage_nodes = get_nodes_with_object(container, oid, self.shell, cluster.storage_nodes) object_storage_nodes = get_nodes_with_object(cid, oid, self.shell, cluster.storage_nodes)
object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes] object_nodes = [cluster_node for cluster_node in cluster.cluster_nodes if cluster_node.storage_node in object_storage_nodes]
node = random.choice(object_nodes) node = random.choice(object_nodes)
with reporter.step("Search object in system."): with reporter.step("Search object in system."):
object_path, object_name = self.get_object_path_and_name_file(oid, container, node) object_path, object_name = self.get_object_path_and_name_file(oid, cid, node)
with reporter.step("Block read file"): with reporter.step("Block read file"):
node.host.get_shell().exec(f"chmod a-r {object_path}/{object_name}") node.host.get_shell().exec(f"chmod a-r {object_path}/{object_name}")
@ -152,7 +157,7 @@ class TestShardMetrics(ClusterTestBase):
with pytest.raises(RuntimeError, match=OBJECT_NOT_FOUND): with pytest.raises(RuntimeError, match=OBJECT_NOT_FOUND):
get_object( get_object(
wallet=default_wallet, wallet=default_wallet,
cid=container, cid=cid,
oid=oid, oid=oid,
shell=self.shell, shell=self.shell,
endpoint=node.storage_node.get_rpc_endpoint(), endpoint=node.storage_node.get_rpc_endpoint(),

View file

@ -5,7 +5,6 @@ import sys
import allure import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.resources.error_patterns import ( from frostfs_testlib.resources.error_patterns import (
INVALID_LENGTH_SPECIFIER, INVALID_LENGTH_SPECIFIER,
INVALID_OFFSET_SPECIFIER, INVALID_OFFSET_SPECIFIER,
@ -15,7 +14,7 @@ from frostfs_testlib.resources.error_patterns import (
OUT_OF_RANGE, OUT_OF_RANGE,
) )
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
from frostfs_testlib.steps.cli.container import DEFAULT_EC_PLACEMENT_RULE, DEFAULT_PLACEMENT_RULE, search_nodes_with_container from frostfs_testlib.steps.cli.container import create_container, search_nodes_with_container
from frostfs_testlib.steps.cli.object import ( from frostfs_testlib.steps.cli.object import (
get_object_from_random_node, get_object_from_random_node,
get_range, get_range,
@ -34,16 +33,12 @@ from frostfs_testlib.storage.dataclasses.policy import PlacementPolicy
from frostfs_testlib.storage.dataclasses.storage_object_info import StorageObjectInfo from frostfs_testlib.storage.dataclasses.storage_object_info import StorageObjectInfo
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.utils.file_utils import TestFile, get_file_content, get_file_hash from frostfs_testlib.utils.file_utils import generate_file, get_file_content, get_file_hash
from ...helpers.container_creation import create_container_with_ape
from ...helpers.container_request import APE_EVERYONE_ALLOW_ALL, ContainerRequest
logger = logging.getLogger("NeoLogger") logger = logging.getLogger("NeoLogger")
CLEANUP_TIMEOUT = 10 CLEANUP_TIMEOUT = 10
COMMON_ATTRIBUTE = {"common_key": "common_value"} COMMON_ATTRIBUTE = {"common_key": "common_value"}
COMMON_CONTAINER_RULE = "REP 1 IN X CBF 1 SELECT 1 FROM * AS X"
# Will upload object for each attribute set # Will upload object for each attribute set
OBJECT_ATTRIBUTES = [ OBJECT_ATTRIBUTES = [
None, None,
@ -85,7 +80,7 @@ def generate_ranges(storage_object: StorageObjectInfo, max_object_size: int, she
for offset, length in file_ranges: for offset, length in file_ranges:
range_length = random.randint(RANGE_MIN_LEN, RANGE_MAX_LEN) range_length = random.randint(RANGE_MIN_LEN, RANGE_MAX_LEN)
range_start = random.randint(offset, offset + length - 1) range_start = random.randint(offset, offset + length)
file_ranges_to_test.append((range_start, min(range_length, storage_object.size - range_start))) file_ranges_to_test.append((range_start, min(range_length, storage_object.size - range_start)))
@ -95,15 +90,12 @@ def generate_ranges(storage_object: StorageObjectInfo, max_object_size: int, she
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def common_container( def common_container(default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster) -> str:
frostfs_cli: FrostfsCli, rule = "REP 1 IN X CBF 1 SELECT 1 FROM * AS X"
default_wallet: WalletInfo, with reporter.step(f"Create container with {rule} and put object"):
client_shell: Shell, cid = create_container(default_wallet, client_shell, cluster.default_rpc_endpoint, rule)
cluster: Cluster,
rpc_endpoint: str, return cid
) -> str:
container_request = ContainerRequest(COMMON_CONTAINER_RULE, APE_EVERYONE_ALLOW_ALL)
return create_container_with_ape(container_request, frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -125,35 +117,33 @@ def storage_objects(
client_shell: Shell, client_shell: Shell,
cluster: Cluster, cluster: Cluster,
object_size: ObjectSize, object_size: ObjectSize,
test_file_module: TestFile,
frostfs_cli: FrostfsCli,
placement_policy: PlacementPolicy, placement_policy: PlacementPolicy,
rpc_endpoint: str,
) -> list[StorageObjectInfo]: ) -> list[StorageObjectInfo]:
cid = create_container_with_ape( wallet = default_wallet
ContainerRequest(placement_policy.value, APE_EVERYONE_ALLOW_ALL), frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint # Separate containers for complex/simple objects to avoid side-effects
) cid = create_container(wallet, shell=client_shell, rule=placement_policy.value, endpoint=cluster.default_rpc_endpoint)
file_path = generate_file(object_size.value)
file_hash = get_file_hash(file_path)
with reporter.step("Generate file"):
file_hash = get_file_hash(test_file_module.path)
storage_objects = [] storage_objects = []
with reporter.step("Put objects"): with reporter.step("Put objects"):
# We need to upload objects multiple times with different attributes # We need to upload objects multiple times with different attributes
for attributes in OBJECT_ATTRIBUTES: for attributes in OBJECT_ATTRIBUTES:
storage_object_id = put_object_to_random_node( storage_object_id = put_object_to_random_node(
default_wallet, wallet=wallet,
test_file_module.path, path=file_path,
cid, cid=cid,
client_shell, shell=client_shell,
cluster, cluster=cluster,
attributes=attributes, attributes=attributes,
) )
storage_object = StorageObjectInfo(cid, storage_object_id) storage_object = StorageObjectInfo(cid, storage_object_id)
storage_object.size = object_size.value storage_object.size = object_size.value
storage_object.wallet = default_wallet storage_object.wallet = wallet
storage_object.file_path = test_file_module.path storage_object.file_path = file_path
storage_object.file_hash = file_hash storage_object.file_hash = file_hash
storage_object.attributes = attributes storage_object.attributes = attributes
@ -251,26 +241,21 @@ class TestObjectApi(ClusterTestBase):
) )
self.check_header_is_presented(head_info, storage_object_2.attributes) self.check_header_is_presented(head_info, storage_object_2.attributes)
@allure.title("Head deleted object with --raw arg (obj_size={object_size}, policy={container_request})") @allure.title("Head deleted object with --raw arg (obj_size={object_size}, policy={placement_policy})")
@pytest.mark.parametrize( def test_object_head_raw(self, default_wallet: str, object_size: ObjectSize, placement_policy: PlacementPolicy):
"container_request", with reporter.step("Create container"):
[ cid = create_container(default_wallet, self.shell, self.cluster.default_rpc_endpoint, placement_policy.value)
ContainerRequest(DEFAULT_PLACEMENT_RULE, APE_EVERYONE_ALLOW_ALL, "rep"),
ContainerRequest(DEFAULT_EC_PLACEMENT_RULE, APE_EVERYONE_ALLOW_ALL, "ec"),
],
indirect=True,
ids=["rep", "ec"],
)
def test_object_head_raw(self, default_wallet: str, container: str, test_file: TestFile):
with reporter.step("Upload object"): with reporter.step("Upload object"):
oid = put_object_to_random_node(default_wallet, test_file.path, container, self.shell, self.cluster) file_path = generate_file(object_size.value)
oid = put_object_to_random_node(default_wallet, file_path, cid, self.shell, self.cluster)
with reporter.step("Delete object"): with reporter.step("Delete object"):
delete_object(default_wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) delete_object(default_wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step("Call object head --raw and expect error"): with reporter.step("Call object head --raw and expect error"):
with pytest.raises(Exception, match=OBJECT_ALREADY_REMOVED): with pytest.raises(Exception, match=OBJECT_ALREADY_REMOVED):
head_object(default_wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint, is_raw=True) head_object(default_wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint, is_raw=True)
@allure.title("Search objects by native API (obj_size={object_size}, policy={placement_policy})") @allure.title("Search objects by native API (obj_size={object_size}, policy={placement_policy})")
def test_search_object_api(self, storage_objects: list[StorageObjectInfo]): def test_search_object_api(self, storage_objects: list[StorageObjectInfo]):
@ -314,50 +299,54 @@ class TestObjectApi(ClusterTestBase):
assert sorted(expected_oids) == sorted(result) assert sorted(expected_oids) == sorted(result)
@allure.title("Search objects with removed items (obj_size={object_size})") @allure.title("Search objects with removed items (obj_size={object_size})")
def test_object_search_should_return_tombstone_items( def test_object_search_should_return_tombstone_items(self, default_wallet: WalletInfo, object_size: ObjectSize):
self,
default_wallet: WalletInfo,
container: str,
object_size: ObjectSize,
rpc_endpoint: str,
test_file: TestFile,
):
""" """
Validate object search with removed items Validate object search with removed items
""" """
with reporter.step("Put object to container"): wallet = default_wallet
cid = create_container(wallet, self.shell, self.cluster.default_rpc_endpoint)
with reporter.step("Upload file"):
file_path = generate_file(object_size.value)
file_hash = get_file_hash(file_path)
storage_object = StorageObjectInfo( storage_object = StorageObjectInfo(
cid=container, cid=cid,
oid=put_object_to_random_node(default_wallet, test_file.path, container, self.shell, self.cluster), oid=put_object_to_random_node(wallet, file_path, cid, self.shell, self.cluster),
size=object_size.value, size=object_size.value,
wallet=default_wallet, wallet=wallet,
file_path=test_file.path, file_path=file_path,
file_hash=get_file_hash(test_file.path), file_hash=file_hash,
) )
with reporter.step("Search object"): with reporter.step("Search object"):
# Root Search object should return root object oid # Root Search object should return root object oid
result = search_object(default_wallet, container, self.shell, rpc_endpoint, root=True) result = search_object(wallet, cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, root=True)
objects_before_deletion = len(result) assert result == [storage_object.oid]
assert storage_object.oid in result
with reporter.step("Delete object"): with reporter.step("Delete file"):
delete_objects([storage_object], self.shell, self.cluster) delete_objects([storage_object], self.shell, self.cluster)
with reporter.step("Search deleted object with --root"): with reporter.step("Search deleted object with --root"):
# Root Search object should return nothing # Root Search object should return nothing
result = search_object(default_wallet, container, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, root=True) result = search_object(wallet, cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, root=True)
assert len(result) == 0 assert len(result) == 0
with reporter.step("Search deleted object with --phy should return only tombstones"): with reporter.step("Search deleted object with --phy should return only tombstones"):
# Physical Search object should return only tombstones # Physical Search object should return only tombstones
result = search_object(default_wallet, container, self.shell, rpc_endpoint, phy=True) result = search_object(wallet, cid, shell=self.shell, endpoint=self.cluster.default_rpc_endpoint, phy=True)
assert storage_object.tombstone in result, "Search result should contain tombstone of removed object" assert storage_object.tombstone in result, "Search result should contain tombstone of removed object"
assert storage_object.oid not in result, "Search result should not contain ObjectId of removed object" assert storage_object.oid not in result, "Search result should not contain ObjectId of removed object"
for tombstone_oid in result: for tombstone_oid in result:
head_info = head_object(default_wallet, container, tombstone_oid, self.shell, rpc_endpoint) header = head_object(
object_type = head_info["header"]["objectType"] wallet,
cid,
tombstone_oid,
shell=self.shell,
endpoint=self.cluster.default_rpc_endpoint,
)["header"]
object_type = header["objectType"]
assert object_type == "TOMBSTONE", f"Object wasn't deleted properly. Found object {tombstone_oid} with type {object_type}" assert object_type == "TOMBSTONE", f"Object wasn't deleted properly. Found object {tombstone_oid} with type {object_type}"
@allure.title("Get range hash by native API (obj_size={object_size}, policy={placement_policy})") @allure.title("Get range hash by native API (obj_size={object_size}, policy={placement_policy})")

View file

@ -2,12 +2,14 @@ import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli import FrostfsCli from frostfs_testlib.cli import FrostfsCli
from frostfs_testlib.resources.wellknown_acl import PUBLIC_ACL
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
from frostfs_testlib.steps.cli.container import ( from frostfs_testlib.steps.cli.container import (
REP_2_FOR_3_NODES_PLACEMENT_RULE, REP_2_FOR_3_NODES_PLACEMENT_RULE,
SINGLE_PLACEMENT_RULE, SINGLE_PLACEMENT_RULE,
StorageContainer, StorageContainer,
StorageContainerInfo, StorageContainerInfo,
create_container,
) )
from frostfs_testlib.steps.cli.object import delete_object, get_object from frostfs_testlib.steps.cli.object import delete_object, get_object
from frostfs_testlib.steps.storage_object import StorageObjectInfo from frostfs_testlib.steps.storage_object import StorageObjectInfo
@ -21,18 +23,13 @@ from pytest import FixtureRequest
from ...helpers.bearer_token import create_bearer_token from ...helpers.bearer_token import create_bearer_token
from ...helpers.container_access import assert_full_access_to_container from ...helpers.container_access import assert_full_access_to_container
from ...helpers.container_creation import create_container_with_ape
from ...helpers.container_request import ContainerRequest
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@allure.title("Create user container for bearer token usage") @allure.title("Create user container for bearer token usage")
def user_container( def user_container(default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster, request: FixtureRequest) -> StorageContainer:
frostfs_cli: FrostfsCli, default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster, rpc_endpoint: str, request: FixtureRequest rule = request.param if "param" in request.__dict__ else SINGLE_PLACEMENT_RULE
) -> StorageContainer: container_id = create_container(default_wallet, client_shell, cluster.default_rpc_endpoint, rule, PUBLIC_ACL)
policy = request.param if "param" in request.__dict__ else SINGLE_PLACEMENT_RULE
container_request = ContainerRequest(policy)
container_id = create_container_with_ape(container_request, frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint)
# Deliberately using s3gate wallet here to test bearer token # Deliberately using s3gate wallet here to test bearer token
s3_gate_wallet = WalletInfo.from_node(cluster.s3_gates[0]) s3_gate_wallet = WalletInfo.from_node(cluster.s3_gates[0])
@ -68,7 +65,6 @@ def storage_objects(
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.bearer @pytest.mark.bearer
@pytest.mark.ape @pytest.mark.ape
@pytest.mark.grpc_api
class TestObjectApiWithBearerToken(ClusterTestBase): class TestObjectApiWithBearerToken(ClusterTestBase):
@allure.title("Object can be deleted from any node using s3gate wallet with bearer token (obj_size={object_size})") @allure.title("Object can be deleted from any node using s3gate wallet with bearer token (obj_size={object_size})")
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -1,861 +0,0 @@
import allure
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.storage.cluster import ClusterNode
from frostfs_testlib.storage.constants import PlacementRule
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.policy import PlacementPolicy
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.storage.grpc_operations.interfaces import GrpcClientWrapper
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.utils.file_utils import generate_file, generate_file_with_content, get_file_content, get_file_hash
from workspace.frostfs_testcases.pytest_tests.helpers.container_creation import create_container_with_ape
from workspace.frostfs_testcases.pytest_tests.helpers.container_request import APE_EVERYONE_ALLOW_ALL, ContainerRequest
OPERATIONS_TO_REDUCE = 100
DEFAULT_FILE_SIZE = 10
EMPTY_FILE_SIZE = 0
@pytest.mark.nightly
@pytest.mark.grpc_api
class TestObjectApiPatch(ClusterTestBase):
@allure.title("[Class] Create container with all operations allowed for owner")
@pytest.fixture(scope="class")
def container(self, placement_policy: PlacementPolicy, frostfs_cli: FrostfsCli, default_wallet: WalletInfo) -> str:
container_request = ContainerRequest(placement_policy.value, APE_EVERYONE_ALLOW_ALL)
return create_container_with_ape(
container_request,
frostfs_cli,
default_wallet,
self.shell,
self.cluster,
self.cluster.default_rpc_endpoint,
)
@pytest.fixture(scope="function")
def original_object(self, grpc_client: GrpcClientWrapper, container: str, object_size: ObjectSize) -> str:
file = generate_file(object_size.value)
with reporter.step("Put object"):
return grpc_client.object.put(file, container, self.cluster.default_rpc_endpoint)
@pytest.fixture(scope="function", params=[pytest.param(DEFAULT_FILE_SIZE, id="default_file_size")])
def sized_object(self, grpc_client: GrpcClientWrapper, container: str, request: pytest.FixtureRequest) -> str:
size = request.param
file = generate_file(size)
with reporter.step(f"Put object of {size} bytes"):
return grpc_client.object.put(file, container, self.cluster.default_rpc_endpoint)
@pytest.fixture(scope="class")
def container_nodes(self, container: str, grpc_client: GrpcClientWrapper) -> list[ClusterNode]:
return grpc_client.container.nodes(self.cluster.default_rpc_endpoint, container, self.cluster)
@pytest.fixture(scope="class")
def non_container_nodes(self, container_nodes: list[ClusterNode]) -> list[ClusterNode]:
return list(set(self.cluster.cluster_nodes) - set(container_nodes))
def _get_bytes_relative_to_object(self, value: int | str, object_size: int = None, part_size: int = None) -> int:
if type(value) is int:
return value
if "part" not in value and "object" not in value:
return int(value)
if object_size is not None:
value = value.replace("object", str(object_size))
if part_size is not None:
value = value.replace("part", str(part_size))
return int(eval(value))
def _get_range_relative_to_object(
self, rng: str, object_size: int = None, part_size: int = None, int_values: bool = False
) -> str | int:
offset, length = rng.split(":")
offset = self._get_bytes_relative_to_object(offset, object_size, part_size)
length = self._get_bytes_relative_to_object(length, object_size, part_size)
return (offset, length) if int_values else f"{offset}:{length}"
@allure.title("Patch simple object payload (range={offset}:{length}, payload_size={payload_size}, policy={placement_policy})")
@pytest.mark.parametrize("object_size", ["simple"], indirect=True)
@pytest.mark.parametrize(
"offset, length, payload_size",
[
# String "object" denotes size of object.
(0, 10, 10),
(0, 50, 0),
(500, 0, 5),
("object", 0, 30),
(0, "object", "object"),
(0, "object", "object+100"),
(0, "object", "object-100"),
("object-100", 100, 200),
# TODO: Empty payload is temporarily not supported for EC policy
# (0, "object", 0),
],
)
def test_patch_simple_object(
self,
grpc_client: GrpcClientWrapper,
container: str,
original_object: str,
object_size: ObjectSize,
offset: int | str,
length: int | str,
payload_size: int | str,
):
relative_offset = self._get_bytes_relative_to_object(offset, object_size.value)
relative_length = self._get_bytes_relative_to_object(length, object_size.value)
relative_size = self._get_bytes_relative_to_object(payload_size, object_size.value)
patch_payload = generate_file(relative_size)
patch_range = f"{relative_offset}:{relative_length}"
with reporter.step("Patch simple object"):
patched_oid = grpc_client.object.patch(
container,
original_object,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
assert patched_oid != original_object, "Patched object's OID must be different from original one"
with reporter.step("Head patched object and make sure it changes size"):
patched_info: dict = grpc_client.object.head(container, patched_oid, self.cluster.default_rpc_endpoint)
expected_payload_length = object_size.value + relative_size - relative_length
patched_payload_length = int(patched_info["header"]["payloadLength"])
assert (
patched_payload_length == expected_payload_length
), f"Size of object does not match expected size: {patched_payload_length}"
@allure.title("Patch complex object payload (range={offset}:{length}, payload_size={payload_size}, policy={placement_policy})")
@pytest.mark.parametrize("object_size", ["complex"], indirect=True)
@pytest.mark.parametrize(
"offset, length, payload_size",
[
# Strings "object" and "part" denote size of object and its part, respectively.
("part", 100, 50),
("object-part", "part", "part"),
(0, "part", "part+100"),
("part*2", "part", "part-200"),
("part-1", "part", "part-100"),
("part+1", "part-1", "part+100"),
# TODO: Empty payload is temporarily not supported for EC policy
# ("part", "part", 0),
],
)
def test_patch_complex_object(
self,
grpc_client: GrpcClientWrapper,
container: str,
original_object: str,
object_size: ObjectSize,
max_object_size: int,
offset: int | str,
length: int | str,
payload_size: int | str,
):
relative_offset = self._get_bytes_relative_to_object(offset, object_size.value, max_object_size)
relative_length = self._get_bytes_relative_to_object(length, object_size.value, max_object_size)
relative_size = self._get_bytes_relative_to_object(payload_size, object_size.value, max_object_size)
patch_payload = generate_file(relative_size)
patch_range = f"{relative_offset}:{relative_length}"
with reporter.step("Patch complex object"):
patched_oid = grpc_client.object.patch(
container,
original_object,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
assert patched_oid != original_object, "Patched object's OID must be different from original one"
with reporter.step("Head patched object and make sure it changes size"):
patched_info: dict = grpc_client.object.head(container, patched_oid, self.cluster.default_rpc_endpoint)
expected_payload_length = object_size.value + relative_size - relative_length
patched_payload_length = int(patched_info["header"]["payloadLength"])
assert (
patched_payload_length == expected_payload_length
), f"Size of object does not match expected size: {patched_payload_length}"
@allure.title("Patch simple object with complex one and vice versa (policy={placement_policy})")
def test_replace_simple_object_with_complex_one_and_rollback(
self,
grpc_client: GrpcClientWrapper,
container: str,
simple_object_size: ObjectSize,
complex_object_size: ObjectSize,
):
simple_file = generate_file(simple_object_size.value)
complex_file = generate_file(complex_object_size.value)
with reporter.step("Put simple object"):
simple_oid = grpc_client.object.put(simple_file, container, self.cluster.default_rpc_endpoint)
with reporter.step("Completely replace simple object with complex one"):
patched_oid = grpc_client.object.patch(
container,
simple_oid,
self.cluster.default_rpc_endpoint,
ranges=[f"0:{simple_object_size.value}"],
payloads=[complex_file.path],
timeout="200s",
)
assert simple_oid != patched_oid, "Patched object's OID must be different from simple one"
with reporter.step("Get patched object and make sure it is identical to complex one"):
patched_file = grpc_client.object.get(container, patched_oid, self.cluster.default_rpc_endpoint)
assert get_file_hash(patched_file) == get_file_hash(complex_file), "Patched object is not identical to complex one"
complex_oid = patched_oid
with reporter.step("Completely replace complex object with simple one"):
patched_oid = grpc_client.object.patch(
container,
complex_oid,
self.cluster.default_rpc_endpoint,
ranges=[f"0:{complex_object_size.value}"],
payloads=[simple_file.path],
timeout="200s",
)
assert patched_oid != complex_oid, "Patched object's OID must be different from complex one"
with reporter.step("Get patched object and make sure it is identical to simple one"):
patched_file = grpc_client.object.get(container, patched_oid, self.cluster.default_rpc_endpoint)
assert get_file_hash(patched_file) == get_file_hash(simple_file), "Patched object is not identical to simple one"
# TODO: Empty payload is temporarily not supported for EC policy
@allure.title("Reduce object payload to zero length with iterative patching")
@pytest.mark.parametrize("placement_policy", ["rep"], indirect=True)
@pytest.mark.parametrize("sized_object", [OPERATIONS_TO_REDUCE], indirect=True)
def test_iterative_reduction_object_payload_by_patching_rep(self, grpc_client: GrpcClientWrapper, container: str, sized_object: str):
oid = sized_object
previous_oids = {oid}
patch_payload = generate_file(EMPTY_FILE_SIZE)
with reporter.step(f"Iterative patch {OPERATIONS_TO_REDUCE} times by 1 byte"):
for i in range(OPERATIONS_TO_REDUCE):
with reporter.step(f"Patch {i + 1}"):
oid = grpc_client.object.patch(
container,
oid,
self.cluster.default_rpc_endpoint,
ranges=["0:1"],
payloads=[patch_payload],
timeout="200s",
)
assert oid not in previous_oids, f"New OID expected, previous ones: {previous_oids}"
previous_oids.add(oid)
with reporter.step("Head object and make sure its size is 0"):
patched_info: dict = grpc_client.object.head(container, oid, self.cluster.default_rpc_endpoint)
payload_length = int(patched_info["header"]["payloadLength"])
assert payload_length == 0, f"Expected file size 0 bytes, received: {payload_length}"
@allure.title(
"[NEGATIVE] Non-zero length patch cannot be applied to end of object (object_size={object_size}, policy={placement_policy})"
)
def test_patch_end_of_object_with_non_zero_length(
self, grpc_client: GrpcClientWrapper, container: str, original_object: str, object_size: ObjectSize
):
patch_payload = generate_file(DEFAULT_FILE_SIZE)
patch_range = f"{object_size.value}:1"
with reporter.step("Try to patch object and wait for an exception"):
with pytest.raises(Exception, match="out of range"):
grpc_client.object.patch(
container,
original_object,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
@allure.title(
"[NEGATIVE] Patch with out of range offset cannot be applied "
"(offset={offset}, object_size={object_size}, policy={placement_policy})"
)
@pytest.mark.parametrize("offset", [-1, "object+1"])
def test_patch_with_out_of_range_offset(
self,
grpc_client: GrpcClientWrapper,
container: str,
original_object: str,
object_size: ObjectSize,
offset: int | str,
):
patch_payload = generate_file(DEFAULT_FILE_SIZE)
offset = self._get_bytes_relative_to_object(offset, object_size.value)
patch_range = f"{offset}:5"
with reporter.step("Try to patch object and wait for an exception"):
# Invalid syntax for offset '-1'
with pytest.raises(Exception, match="patch offset exceeds object size|invalid syntax"):
grpc_client.object.patch(
container,
original_object,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
@allure.title("Patch an object with content (range={offset}:{length}, content='{payload_content}', policy={placement_policy})")
@pytest.mark.parametrize(
"offset, length, payload_content",
[(0, 0, "prefix"), (17, 0, "suffix"), (10, 7, "QWERTYU"), (0, 3, "somereplaceandinsert"), (0, 5, "xyz")],
ids=["prefix", "suffix", "replace", "replace_insert", "replace_remove"],
)
def test_patch_object_with_content(
self,
grpc_client: GrpcClientWrapper,
container: str,
offset: int,
length: int,
payload_content: str,
):
ORIGINAL_CONTENT = "0123456789qwertyu"
original_file = generate_file_with_content(len(ORIGINAL_CONTENT), content=ORIGINAL_CONTENT)
patch_payload = generate_file_with_content(len(payload_content), content=payload_content)
patch_range = f"{offset}:{length}"
expected_content = ORIGINAL_CONTENT[:offset] + payload_content + ORIGINAL_CONTENT[offset + length :]
with reporter.step("Put object with content"):
original_oid = grpc_client.object.put(original_file, container, self.cluster.default_rpc_endpoint)
with reporter.step("Patch object with content"):
patched_oid = grpc_client.object.patch(
container,
original_oid,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
assert patched_oid != original_oid, "Patched object's OID must be different from original one"
with reporter.step("Get patched object and compare its content with expected one"):
patched_file = grpc_client.object.get(container, patched_oid, self.cluster.default_rpc_endpoint)
patched_content = get_file_content(patched_file)
assert patched_content == expected_content, f"Content of patched object does not match expected content: {patched_content}"
@allure.title(
"Patch object with multiple ranges and payloads "
"(ranges={ranges}, payload_sizes={payload_sizes}, object_size={object_size}, policy={placement_policy})"
)
@pytest.mark.parametrize(
"ranges, payload_sizes",
[
# String "object" denotes size of object.
[["0:0", "0:0"], [5, 10]],
[["0:10", "10:20", "30:100", "130:0"], [10, 50, 100, 30]],
[["100:100", "0:1", "500:200"], [100, 1, 400]],
[["0:object-1", "object-1:1"], ["object-1", 1]],
],
ids=["insert_insert", "order_ranges", "disorder_ranges", "replace_object"],
)
def test_patch_object_with_multiple_ranges_and_payloads(
self,
grpc_client: GrpcClientWrapper,
container: str,
original_object: str,
object_size: ObjectSize,
ranges: list[str],
payload_sizes: list[int | str],
):
patch_sizes = [self._get_bytes_relative_to_object(size, object_size.value) for size in payload_sizes]
patch_payloads = [generate_file(size) for size in patch_sizes]
patch_ranges = [self._get_range_relative_to_object(rng, object_size.value) for rng in ranges]
expected_payload_length = object_size.value
for i, _ in enumerate(patch_ranges):
_, length = self._get_range_relative_to_object(patch_ranges[i], object_size.value, int_values=True)
expected_payload_length += patch_sizes[i] - length
with reporter.step("Patch object with multiple ranges and payloads"):
patched_oid = grpc_client.object.patch(
container,
original_object,
self.cluster.default_rpc_endpoint,
patch_ranges,
patch_payloads,
timeout="200s",
)
assert patched_oid != original_object, "Patched object's OID must be different from original one"
with reporter.step("Head patched object and make sure it changes size"):
patched_info: dict = grpc_client.object.head(container, patched_oid, self.cluster.default_rpc_endpoint)
patched_payload_length = int(patched_info["header"]["payloadLength"])
assert (
patched_payload_length == expected_payload_length
), f"Size of object does not match expected size: {patched_payload_length}"
@allure.title(
"Patch object with content with multiple ranges and payloads "
"(ranges={patch_ranges}, contents={payload_contents}, policy={placement_policy})"
)
@pytest.mark.parametrize(
"patch_ranges, payload_contents, expected_content",
[
# String "object" denotes size of object.
[
["0:0", "0:0", "object:0"],
["first_prefix_", "second_prefix_", "_postfix"],
"first_prefix_second_prefix_test_patch_object_with content with multiple ranges and payloads_postfix",
],
[
["10:5", "15:3", "18:10", "28:2", "30:3"],
["patch", " > ", "is", " < ", "okay"],
"test_patchpatch > is < okayth multiple ranges and payloads",
],
],
ids=["prefix_postfix", "replace"],
)
def test_patch_object_with_content_with_multiple_ranges_and_payloads(
self,
grpc_client: GrpcClientWrapper,
container: str,
patch_ranges: list[str],
payload_contents: list[str],
expected_content: str,
):
ORIGINAL_CONTENT = "test_patch_object_with content with multiple ranges and payloads"
ORIGINAL_FILE_SIZE = len(ORIGINAL_CONTENT)
original_file = generate_file_with_content(ORIGINAL_FILE_SIZE, content=ORIGINAL_CONTENT)
patch_payloads = [generate_file_with_content(len(content), content=content) for content in payload_contents]
patch_ranges = [self._get_range_relative_to_object(rng, ORIGINAL_FILE_SIZE) for rng in patch_ranges]
with reporter.step("Put object with content"):
original_oid = grpc_client.object.put(original_file, container, self.cluster.default_rpc_endpoint)
with reporter.step("Patch object with multiple ranges and payloads"):
patched_oid = grpc_client.object.patch(
container,
original_oid,
self.cluster.default_rpc_endpoint,
ranges=patch_ranges,
payloads=patch_payloads,
timeout="200s",
)
assert patched_oid != original_oid, "Patched object's OID must be different from original one"
with reporter.step("Get patched object and compare its content with expected one"):
patched_file = grpc_client.object.get(container, patched_oid, self.cluster.default_rpc_endpoint)
patched_content = get_file_content(patched_file)
assert patched_content == expected_content, f"Content of patched object does not match expected content: {patched_content}"
@allure.title("[NEGATIVE] Patch cannot be applied with range collisions (ranges={ranges}, policy={placement_policy})")
@pytest.mark.parametrize(
"ranges",
# String "object" denotes size of object.
[["0:1", "1:2", "2:3"], ["0:100", "10:50"], ["0:object", "object-1:1"]],
ids=["left_overlap", "full_overlap", "object_overlap"],
)
def test_patch_object_with_multiple_range_collisions(
self,
grpc_client: GrpcClientWrapper,
container: str,
sized_object: str,
ranges: list[str],
):
payload_file = generate_file(DEFAULT_FILE_SIZE)
patch_payloads = [payload_file for _ in ranges]
patch_ranges = [self._get_range_relative_to_object(rng, DEFAULT_FILE_SIZE) for rng in ranges]
with reporter.step("Try to patch object with invalid range and catch exception"):
with pytest.raises(Exception, match="invalid patch offset order"):
grpc_client.object.patch(
container,
sized_object,
self.cluster.default_rpc_endpoint,
patch_ranges,
patch_payloads,
timeout="200s",
)
@allure.title(
"[NEGATIVE] Patch cannot be applied if ranges and payloads do not match "
"(ranges={ranges}, payloads={payload_count}, policy={placement_policy})"
)
@pytest.mark.parametrize(
"ranges, payload_count",
[[["0:1", "5:5", "20:11"], 1], [["22:1"], 5], [[], 3], [["50:10", "90:20"], 0]],
ids=["more_ranges", "more_payloads", "no_ranges", "no_payloads"],
)
def test_patch_with_ranges_do_not_match_payloads(
self,
grpc_client: GrpcClientWrapper,
container: str,
sized_object: str,
ranges: list[str],
payload_count: int,
):
payload_file = generate_file(DEFAULT_FILE_SIZE)
patch_payloads = [payload_file for _ in range(payload_count)]
with reporter.step("Try patch object with mismatched ranges and payloads and catch exception"):
with pytest.raises(Exception, match="number of ranges and payloads are not equal"):
grpc_client.object.patch(container, sized_object, self.cluster.default_rpc_endpoint, ranges, patch_payloads, timeout="200s")
@allure.title("[NEGATIVE] Patch cannot be applied with non-existent payload (policy={placement_policy})")
def test_patch_with_non_existent_payload(self, grpc_client: GrpcClientWrapper, container: str, sized_object: str):
with reporter.step("Try patch object with non-existent payload and catch exception"):
with pytest.raises(Exception, match="no such file or directory"):
grpc_client.object.patch(
container,
sized_object,
self.cluster.default_rpc_endpoint,
ranges=["20:300"],
payloads=["non_existent_file_path"],
timeout="200s",
)
@allure.title("[NEGATIVE] Patch cannot be applied to deleted object (object_size={object_size}, policy={placement_policy})")
def test_patch_deleted_object(self, grpc_client: GrpcClientWrapper, container: str, original_object: str):
patch_range = "0:10"
patch_payload = generate_file(DEFAULT_FILE_SIZE)
with reporter.step("Delete object"):
grpc_client.object.delete(container, original_object, self.cluster.default_rpc_endpoint)
with reporter.step("Try patch deleted object and catch exception"):
with pytest.raises(Exception, match="object already removed"):
grpc_client.object.patch(
container,
original_object,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
@allure.title("[NEGATIVE] Patch cannot be applied to tombstone object (object_size={object_size}, policy={placement_policy})")
def test_patch_tombstone_object(self, grpc_client: GrpcClientWrapper, container: str, original_object: str):
patch_range = "0:10"
patch_payload = generate_file(DEFAULT_FILE_SIZE)
with reporter.step("Delete object"):
tombstone_oid = grpc_client.object.delete(container, original_object, self.cluster.default_rpc_endpoint)
with reporter.step("Try patch tombstone object and catch exception"):
with pytest.raises(Exception, match="non-regular object can't be patched"):
grpc_client.object.patch(
container,
tombstone_oid,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
@allure.title("[NEGATIVE] Patch cannot be applied to locked object (object_size={object_size}, policy={placement_policy})")
def test_patch_locked_object(self, grpc_client: GrpcClientWrapper, container: str, original_object: str):
patch_range = "0:10"
patch_payload = generate_file(DEFAULT_FILE_SIZE)
current_epoch = self.get_epoch()
with reporter.step("Lock object"):
locked_oid = grpc_client.object.lock(
container,
original_object,
self.cluster.default_rpc_endpoint,
expire_at=current_epoch + 1,
)
with reporter.step("Try patch locked object and catch exception"):
with pytest.raises(Exception, match="non-regular object can't be patched"):
grpc_client.object.patch(
container,
locked_oid,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
@allure.title("[NEGATIVE] Patch cannot be applied to linked object (policy={placement_policy})")
@pytest.mark.parametrize("object_size", ["complex"], indirect=True)
def test_patch_link_object(self, grpc_client: GrpcClientWrapper, container: str, original_object: str):
patch_range = "0:10"
patch_payload = generate_file(DEFAULT_FILE_SIZE)
with reporter.step("Get link of complex object"):
object_info: dict = grpc_client.object.head(
container,
original_object,
self.cluster.default_rpc_endpoint,
is_raw=True,
)
link_oid = object_info["link"]
with reporter.step("Try patch link object and catch exception"):
with pytest.raises(Exception, match="linking object can't be patched"):
grpc_client.object.patch(
container,
link_oid,
self.cluster.default_rpc_endpoint,
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
@allure.title("[NEGATIVE] Patch cannot be applied to part of complex object (policy={placement_policy})")
@pytest.mark.parametrize("placement_policy", ["rep"], indirect=True)
@pytest.mark.parametrize("object_size", ["complex"], indirect=True)
def test_patch_part_of_complex_object_rep(self, grpc_client: GrpcClientWrapper, container: str, original_object: str):
with reporter.step("Get parts of complex object"):
parts = grpc_client.object.parts(container, original_object, self.cluster.cluster_nodes[0])
assert parts, f"Expected list of OIDs of object parts: {parts}"
part_oid = parts[0]
with reporter.step("Try patch part of complex object and catch exception"):
with pytest.raises(Exception, match="complex object parts can't be patched"):
grpc_client.object.patch(
container,
part_oid,
self.cluster.default_rpc_endpoint,
new_attrs="some_key=some_value",
timeout="200s",
)
@allure.title("[NEGATIVE] Patch cannot be applied to EC chunk (object_size={object_size}, policy={placement_policy})")
@pytest.mark.parametrize("placement_policy", ["ec"], indirect=True)
def test_patch_ec_chunk(self, grpc_client: GrpcClientWrapper, container: str, original_object: str):
with reporter.step("Get chunks of object"):
chunks = grpc_client.object.chunks.get_all(self.cluster.default_rpc_endpoint, container, original_object)
assert chunks, f"Expected object chunks, but they are not there: {chunks}"
with reporter.step("Try patch chunk of object and catch exception"):
with pytest.raises(Exception, match="complex object parts can't be patched"):
grpc_client.object.patch(
container,
chunks[0].object_id,
self.cluster.default_rpc_endpoint,
new_attrs="some_key=some_value",
timeout="200s",
)
@allure.title("Patch object attributes (ranges={ranges}, new_attrs={new_attrs}, replace={replace}, policy={placement_policy})")
@pytest.mark.parametrize("replace", [True, False])
@pytest.mark.parametrize("new_attrs", [{"key_1": "val_1"}, {"key_1": "20", "key_2": "false", "FileName": "new-object"}])
@pytest.mark.parametrize("ranges", [[], ["0:10", "20:50"]])
def test_patch_object_attributes(
self,
grpc_client: GrpcClientWrapper,
container: str,
simple_object_size: ObjectSize,
new_attrs: dict,
replace: bool,
ranges: list[str],
):
simple_file = generate_file(simple_object_size.value)
payload_file = generate_file(DEFAULT_FILE_SIZE)
patch_payloads = [payload_file for _ in ranges]
patch_attrs = ",".join(f"{k}={v}" for k, v in new_attrs.items())
with reporter.step("Put simple object" + " with attributes" if replace else ""):
original_oid = grpc_client.object.put(
simple_file,
container,
self.cluster.default_rpc_endpoint,
attributes={"key_1": "1", "key_2": "true", "key_3": "val_3"} if replace else None,
)
with reporter.step("Get simple object attributes"):
original_info: dict = grpc_client.object.head(container, original_oid, self.cluster.default_rpc_endpoint)
original_attrs: dict = original_info["header"]["attributes"]
expected_attrs = {}
if not replace:
expected_attrs.update(original_attrs)
expected_attrs.update(new_attrs)
with reporter.step("Patch simple object attributes" + " with replace" if replace else ""):
patched_oid = grpc_client.object.patch(
container,
original_oid,
self.cluster.default_rpc_endpoint,
ranges=ranges,
payloads=patch_payloads,
new_attrs=patch_attrs,
replace_attrs=replace,
timeout="200s",
)
assert patched_oid != original_oid, "Patched object's OID must be different from original one"
with reporter.step("Get patched object attributes and make sure they are as expected"):
patched_info: dict = grpc_client.object.head(container, patched_oid, self.cluster.default_rpc_endpoint)
patched_attrs: dict = patched_info["header"]["attributes"]
assert (
patched_attrs == expected_attrs
), f"Attributes of patched object do not match expected ones\nPatched attrs: {patched_attrs}\nExpected attrs: {expected_attrs}"
@allure.title("Patch an object via container nodes (object_size={object_size}, policy={placement_policy})")
@pytest.mark.parametrize(
"placement_policy",
[
PlacementPolicy("rep1select2", PlacementRule.REP_1_FOR_2_NODES_PLACEMENT_RULE),
PlacementPolicy("ec1.1select2", PlacementRule.EC_1_1_FOR_2_NODES_PLACEMENT_RULE),
],
ids=["rep1select2", "ec1.1select2"],
indirect=True,
)
def test_patch_via_container_node(
self,
grpc_client: GrpcClientWrapper,
container: str,
container_nodes: list[ClusterNode],
object_size: ObjectSize,
):
original_file = generate_file(object_size.value)
patch_payload = generate_file(DEFAULT_FILE_SIZE)
patch_range = "0:50"
with reporter.step("Put object via container node"):
original_oid = grpc_client.object.put(
original_file,
container,
container_nodes[0].storage_node.get_rpc_endpoint(),
)
with reporter.step("Patch object payload via container node"):
payload_patched_oid = grpc_client.object.patch(
container,
original_oid,
container_nodes[1].storage_node.get_rpc_endpoint(),
ranges=[patch_range],
payloads=[patch_payload],
timeout="200s",
)
assert payload_patched_oid != original_oid, "Patched object's OID must be different from original one"
with reporter.step("Head patched object via container node and make sure it changes size"):
patched_info: dict = grpc_client.object.head(
container,
payload_patched_oid,
container_nodes[0].storage_node.get_rpc_endpoint(),
)
expected_payload_length = object_size.value + DEFAULT_FILE_SIZE - int(patch_range.split(":")[1])
patched_payload_length = int(patched_info["header"]["payloadLength"])
assert (
patched_payload_length == expected_payload_length
), f"Size of object does not match expected size: {patched_payload_length}"
replace = True
new_attrs = {"FileName": "new-object-name"}
patch_attrs = ",".join(f"{k}={v}" for k, v in new_attrs.items())
with reporter.step("Get original object attributes via container node"):
original_info: dict = grpc_client.object.head(
container,
original_oid,
container_nodes[1].storage_node.get_rpc_endpoint(),
)
original_attrs: dict = original_info["header"]["attributes"]
expected_attrs = {}
if not replace:
expected_attrs.update(original_attrs)
expected_attrs.update(new_attrs)
with reporter.step("Patch previous patched object attributes via container node"):
attrs_patched_oid = grpc_client.object.patch(
container,
payload_patched_oid,
container_nodes[0].storage_node.get_rpc_endpoint(),
new_attrs=patch_attrs,
replace_attrs=replace,
timeout="200s",
)
assert attrs_patched_oid != payload_patched_oid, "Patched object's OID must be different from previous patched one"
@allure.title("Patch an object via non container node (object_size={object_size}, policy={placement_policy})")
@pytest.mark.parametrize(
"placement_policy",
[
PlacementPolicy("rep1select2", PlacementRule.REP_1_FOR_2_NODES_PLACEMENT_RULE),
PlacementPolicy("ec1.1select2", PlacementRule.EC_1_1_FOR_2_NODES_PLACEMENT_RULE),
],
ids=["rep1select2", "ec1.1select2"],
indirect=True,
)
def test_patch_via_non_container_node(
self,
grpc_client: GrpcClientWrapper,
container: str,
non_container_nodes: list[ClusterNode],
object_size: ObjectSize,
):
original_file = generate_file(object_size.value)
patch_payload = generate_file(DEFAULT_FILE_SIZE)
patch_range = "0:50"
with reporter.step("Put object via non container node"):
original_oid = grpc_client.object.put(
original_file,
container,
non_container_nodes[0].storage_node.get_rpc_endpoint(),
timeout="300s",
)
with reporter.step("Patch object payload via non container node"):
payload_patched_oid = grpc_client.object.patch(
container,
original_oid,
non_container_nodes[1].storage_node.get_rpc_endpoint(),
ranges=[patch_range],
payloads=[patch_payload],
timeout="300s",
)
assert payload_patched_oid != original_oid, "Patched object's OID must be different from original one"
with reporter.step("Head patched object via non container node and make sure it changes size"):
patched_info: dict = grpc_client.object.head(
container,
payload_patched_oid,
non_container_nodes[0].storage_node.get_rpc_endpoint(),
)
expected_payload_length = object_size.value + DEFAULT_FILE_SIZE - int(patch_range.split(":")[1])
patched_payload_length = int(patched_info["header"]["payloadLength"])
assert (
patched_payload_length == expected_payload_length
), f"Size of object does not match expected size: {patched_payload_length}"
replace = True
new_attrs = {"FileName": "new-object-name"}
patch_attrs = ",".join(f"{k}={v}" for k, v in new_attrs.items())
with reporter.step("Get original object attributes via non container node"):
original_info: dict = grpc_client.object.head(
container,
original_oid,
non_container_nodes[1].storage_node.get_rpc_endpoint(),
)
original_attrs: dict = original_info["header"]["attributes"]
expected_attrs = {}
if not replace:
expected_attrs.update(original_attrs)
expected_attrs.update(new_attrs)
with reporter.step("Patch previous patched object attributes via non container node"):
attrs_patched_oid = grpc_client.object.patch(
container,
payload_patched_oid,
non_container_nodes[0].storage_node.get_rpc_endpoint(),
new_attrs=patch_attrs,
replace_attrs=replace,
timeout="300s",
)
assert attrs_patched_oid != payload_patched_oid, "Patched object's OID must be different from previous patched one"

View file

@ -4,12 +4,13 @@ import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.resources.error_patterns import OBJECT_NOT_FOUND from frostfs_testlib.resources.error_patterns import OBJECT_NOT_FOUND
from frostfs_testlib.steps.cli.container import create_container
from frostfs_testlib.steps.cli.object import get_object_from_random_node, head_object, put_object_to_random_node from frostfs_testlib.steps.cli.object import get_object_from_random_node, head_object, put_object_to_random_node
from frostfs_testlib.steps.epoch import get_epoch from frostfs_testlib.steps.epoch import get_epoch
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import expect_not_raises from frostfs_testlib.utils.file_utils import generate_file, get_file_hash
from frostfs_testlib.utils.file_utils import TestFile
from ...helpers.utility import wait_for_gc_pass_on_storage_nodes from ...helpers.utility import wait_for_gc_pass_on_storage_nodes
@ -21,30 +22,35 @@ logger = logging.getLogger("NeoLogger")
@pytest.mark.grpc_api @pytest.mark.grpc_api
class TestObjectApiLifetime(ClusterTestBase): class TestObjectApiLifetime(ClusterTestBase):
@allure.title("Object is removed when lifetime expired (obj_size={object_size})") @allure.title("Object is removed when lifetime expired (obj_size={object_size})")
def test_object_api_lifetime(self, container: str, test_file: TestFile, default_wallet: WalletInfo): def test_object_api_lifetime(self, default_wallet: WalletInfo, object_size: ObjectSize):
""" """
Test object deleted after expiration epoch. Test object deleted after expiration epoch.
""" """
wallet = default_wallet wallet = default_wallet
endpoint = self.cluster.default_rpc_endpoint
cid = create_container(wallet, self.shell, endpoint)
file_path = generate_file(object_size.value)
file_hash = get_file_hash(file_path)
epoch = get_epoch(self.shell, self.cluster) epoch = get_epoch(self.shell, self.cluster)
oid = put_object_to_random_node(wallet, test_file.path, container, self.shell, self.cluster, expire_at=epoch + 1) oid = put_object_to_random_node(wallet, file_path, cid, self.shell, self.cluster, expire_at=epoch + 1)
with expect_not_raises(): got_file = get_object_from_random_node(wallet, cid, oid, self.shell, self.cluster)
head_object(wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) assert get_file_hash(got_file) == file_hash
with reporter.step("Tick two epochs"): with reporter.step("Tick two epochs"):
self.tick_epochs(2) for _ in range(2):
self.tick_epoch()
# Wait for GC, because object with expiration is counted as alive until GC removes it # Wait for GC, because object with expiration is counted as alive until GC removes it
wait_for_gc_pass_on_storage_nodes() wait_for_gc_pass_on_storage_nodes()
with reporter.step("Check object deleted because it expires on epoch"): with reporter.step("Check object deleted because it expires on epoch"):
with pytest.raises(Exception, match=OBJECT_NOT_FOUND): with pytest.raises(Exception, match=OBJECT_NOT_FOUND):
head_object(wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) head_object(wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
with pytest.raises(Exception, match=OBJECT_NOT_FOUND): with pytest.raises(Exception, match=OBJECT_NOT_FOUND):
get_object_from_random_node(wallet, container, oid, self.shell, self.cluster) get_object_from_random_node(wallet, cid, oid, self.shell, self.cluster)
with reporter.step("Tick additional epoch"): with reporter.step("Tick additional epoch"):
self.tick_epoch() self.tick_epoch()
@ -53,6 +59,6 @@ class TestObjectApiLifetime(ClusterTestBase):
with reporter.step("Check object deleted because it expires on previous epoch"): with reporter.step("Check object deleted because it expires on previous epoch"):
with pytest.raises(Exception, match=OBJECT_NOT_FOUND): with pytest.raises(Exception, match=OBJECT_NOT_FOUND):
head_object(wallet, container, oid, self.shell, self.cluster.default_rpc_endpoint) head_object(wallet, cid, oid, self.shell, self.cluster.default_rpc_endpoint)
with pytest.raises(Exception, match=OBJECT_NOT_FOUND): with pytest.raises(Exception, match=OBJECT_NOT_FOUND):
get_object_from_random_node(wallet, container, oid, self.shell, self.cluster) get_object_from_random_node(wallet, cid, oid, self.shell, self.cluster)

View file

@ -1,23 +1,25 @@
import logging import logging
import re
import allure import allure
import pytest import pytest
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli from frostfs_testlib.credentials.interfaces import CredentialsProvider, User
from frostfs_testlib.resources.common import STORAGE_GC_TIME from frostfs_testlib.resources.common import STORAGE_GC_TIME
from frostfs_testlib.resources.error_patterns import ( from frostfs_testlib.resources.error_patterns import (
LIFETIME_REQUIRED, LIFETIME_REQUIRED,
LOCK_NON_REGULAR_OBJECT, LOCK_NON_REGULAR_OBJECT,
LOCK_OBJECT_EXPIRATION, LOCK_OBJECT_EXPIRATION,
LOCK_OBJECT_REMOVAL, LOCK_OBJECT_REMOVAL,
OBJECT_ALREADY_REMOVED,
OBJECT_IS_LOCKED, OBJECT_IS_LOCKED,
OBJECT_NOT_FOUND, OBJECT_NOT_FOUND,
) )
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
from frostfs_testlib.steps.cli.container import StorageContainer, StorageContainerInfo from frostfs_testlib.steps.cli.container import StorageContainer, StorageContainerInfo, create_container
from frostfs_testlib.steps.cli.object import delete_object, head_object, lock_object from frostfs_testlib.steps.cli.object import delete_object, head_object, lock_object
from frostfs_testlib.steps.complex_object_actions import get_link_object, get_storage_object_chunks from frostfs_testlib.steps.complex_object_actions import get_link_object, get_storage_object_chunks
from frostfs_testlib.steps.epoch import ensure_fresh_epoch from frostfs_testlib.steps.epoch import ensure_fresh_epoch, get_epoch, tick_epoch
from frostfs_testlib.steps.node_management import drop_object from frostfs_testlib.steps.node_management import drop_object
from frostfs_testlib.steps.storage_object import delete_objects from frostfs_testlib.steps.storage_object import delete_objects
from frostfs_testlib.steps.storage_policy import get_nodes_with_object from frostfs_testlib.steps.storage_policy import get_nodes_with_object
@ -27,10 +29,8 @@ from frostfs_testlib.storage.dataclasses.storage_object_info import LockObjectIn
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import expect_not_raises, wait_for_success from frostfs_testlib.testing.test_control import expect_not_raises, wait_for_success
from frostfs_testlib.utils import datetime_utils from frostfs_testlib.utils import datetime_utils, string_utils
from ...helpers.container_creation import create_container_with_ape
from ...helpers.container_request import EVERYONE_ALLOW_ALL
from ...helpers.utility import wait_for_gc_pass_on_storage_nodes from ...helpers.utility import wait_for_gc_pass_on_storage_nodes
logger = logging.getLogger("NeoLogger") logger = logging.getLogger("NeoLogger")
@ -40,15 +40,25 @@ FIXTURE_OBJECT_LIFETIME = 10
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def user_container( def user_wallet(credentials_provider: CredentialsProvider, cluster: Cluster) -> WalletInfo:
frostfs_cli: FrostfsCli, default_wallet: WalletInfo, client_shell: Shell, cluster: Cluster, rpc_endpoint: str with reporter.step("Create user wallet with container"):
) -> StorageContainer: user = User(string_utils.unique_name("user-"))
cid = create_container_with_ape(EVERYONE_ALLOW_ALL, frostfs_cli, default_wallet, client_shell, cluster, rpc_endpoint) return credentials_provider.GRPC.provide(user, cluster.cluster_nodes[0])
return StorageContainer(StorageContainerInfo(cid, default_wallet), client_shell, cluster)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def locked_storage_object(user_container: StorageContainer, client_shell: Shell, cluster: Cluster, object_size: ObjectSize): def user_container(user_wallet: WalletInfo, client_shell: Shell, cluster: Cluster):
container_id = create_container(user_wallet, shell=client_shell, endpoint=cluster.default_rpc_endpoint)
return StorageContainer(StorageContainerInfo(container_id, user_wallet), client_shell, cluster)
@pytest.fixture(scope="module")
def locked_storage_object(
user_container: StorageContainer,
client_shell: Shell,
cluster: Cluster,
object_size: ObjectSize,
):
""" """
Intention of this fixture is to provide storage object which is NOT expected to be deleted during test act phase Intention of this fixture is to provide storage object which is NOT expected to be deleted during test act phase
""" """
@ -67,22 +77,56 @@ def locked_storage_object(user_container: StorageContainer, client_shell: Shell,
) )
storage_object.locks = [LockObjectInfo(storage_object.cid, lock_object_id, FIXTURE_LOCK_LIFETIME, expiration_epoch)] storage_object.locks = [LockObjectInfo(storage_object.cid, lock_object_id, FIXTURE_LOCK_LIFETIME, expiration_epoch)]
return storage_object yield storage_object
with reporter.step("Delete created locked object"):
current_epoch = get_epoch(client_shell, cluster)
epoch_diff = expiration_epoch - current_epoch + 1
if epoch_diff > 0:
with reporter.step(f"Tick {epoch_diff} epochs"):
for _ in range(epoch_diff):
tick_epoch(client_shell, cluster)
try:
delete_object(
storage_object.wallet,
storage_object.cid,
storage_object.oid,
client_shell,
cluster.default_rpc_endpoint,
)
except Exception as ex:
ex_message = str(ex)
# It's okay if object already removed
if not re.search(OBJECT_NOT_FOUND, ex_message) and not re.search(OBJECT_ALREADY_REMOVED, ex_message):
raise ex
logger.debug(ex_message)
@wait_for_success(datetime_utils.parse_time(STORAGE_GC_TIME)) @wait_for_success(datetime_utils.parse_time(STORAGE_GC_TIME))
def check_object_not_found(wallet: WalletInfo, cid: str, oid: str, shell: Shell, rpc_endpoint: str): def check_object_not_found(wallet: WalletInfo, cid: str, oid: str, shell: Shell, rpc_endpoint: str):
with pytest.raises(Exception, match=OBJECT_NOT_FOUND): with pytest.raises(Exception, match=OBJECT_NOT_FOUND):
head_object(wallet, cid, oid, shell, rpc_endpoint) head_object(
wallet,
cid,
oid,
shell,
rpc_endpoint,
)
def verify_object_available(wallet: WalletInfo, cid: str, oid: str, shell: Shell, rpc_endpoint: str): def verify_object_available(wallet: WalletInfo, cid: str, oid: str, shell: Shell, rpc_endpoint: str):
with expect_not_raises(): with expect_not_raises():
head_object(wallet, cid, oid, shell, rpc_endpoint) head_object(
wallet,
cid,
oid,
shell,
rpc_endpoint,
)
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.grpc_api
@pytest.mark.grpc_object_lock @pytest.mark.grpc_object_lock
class TestObjectLockWithGrpc(ClusterTestBase): class TestObjectLockWithGrpc(ClusterTestBase):
@pytest.fixture() @pytest.fixture()

View file

@ -1,62 +0,0 @@
import allure
import pytest
from frostfs_testlib import reporter
from frostfs_testlib.resources.common import EXPIRATION_EPOCH_ATTRIBUTE
from frostfs_testlib.resources.error_patterns import OBJECT_NOT_FOUND
from frostfs_testlib.storage.controllers.cluster_state_controller import ClusterStateController
from frostfs_testlib.storage.controllers.state_managers.config_state_manager import ConfigStateManager
from frostfs_testlib.storage.dataclasses.frostfs_services import StorageNode
from frostfs_testlib.storage.grpc_operations.interfaces import GrpcClientWrapper
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.utils.file_utils import TestFile
@pytest.mark.nightly
@pytest.mark.grpc_api
class TestObjectTombstone(ClusterTestBase):
@pytest.fixture()
@allure.title("Change tombstone lifetime")
def tombstone_lifetime(self, cluster_state_controller: ClusterStateController, request: pytest.FixtureRequest):
config_manager = cluster_state_controller.manager(ConfigStateManager)
config_manager.set_on_all_nodes(StorageNode, {"object:delete:tombstone_lifetime": request.param}, True)
yield f"Tombstone lifetime was changed to {request.param}"
config_manager.revert_all(True)
@pytest.mark.parametrize("object_size, tombstone_lifetime", [("simple", 2)], indirect=True)
@allure.title("Tombstone object should be removed after expiration")
def test_tombstone_lifetime(
self,
new_epoch: int,
container: str,
grpc_client: GrpcClientWrapper,
test_file: TestFile,
rpc_endpoint: str,
tombstone_lifetime: str,
):
allure.dynamic.description(tombstone_lifetime)
with reporter.step("Put object"):
oid = grpc_client.object.put(test_file.path, container, rpc_endpoint)
with reporter.step("Remove object"):
tombstone_oid = grpc_client.object.delete(container, oid, rpc_endpoint)
with reporter.step("Get tombstone object lifetime"):
tombstone_info = grpc_client.object.head(container, tombstone_oid, rpc_endpoint)
tombstone_expiration_epoch = tombstone_info["header"]["attributes"][EXPIRATION_EPOCH_ATTRIBUTE]
with reporter.step("Tombstone lifetime should be <= 3"):
epochs_to_skip = int(tombstone_expiration_epoch) - new_epoch + 1
assert epochs_to_skip <= 3
with reporter.step("Wait for tombstone expiration"):
self.tick_epochs(epochs_to_skip)
with reporter.step("Tombstone should be removed after expiration"):
with pytest.raises(RuntimeError, match=OBJECT_NOT_FOUND):
grpc_client.object.head(container, tombstone_oid, rpc_endpoint)
with pytest.raises(RuntimeError, match=OBJECT_NOT_FOUND):
grpc_client.object.get(container, tombstone_oid, rpc_endpoint)

View file

@ -7,7 +7,9 @@ from frostfs_testlib import reporter
from frostfs_testlib.cli import FrostfsCli from frostfs_testlib.cli import FrostfsCli
from frostfs_testlib.resources.cli import CLI_DEFAULT_TIMEOUT, FROSTFS_CLI_EXEC from frostfs_testlib.resources.cli import CLI_DEFAULT_TIMEOUT, FROSTFS_CLI_EXEC
from frostfs_testlib.resources.error_patterns import OBJECT_IS_LOCKED from frostfs_testlib.resources.error_patterns import OBJECT_IS_LOCKED
from frostfs_testlib.resources.wellknown_acl import PUBLIC_ACL_F
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
from frostfs_testlib.steps.cli.container import create_container
from frostfs_testlib.storage.dataclasses.wallet import WalletInfo from frostfs_testlib.storage.dataclasses.wallet import WalletInfo
from frostfs_testlib.testing.cluster_test_base import ClusterTestBase from frostfs_testlib.testing.cluster_test_base import ClusterTestBase
from frostfs_testlib.testing.test_control import expect_not_raises from frostfs_testlib.testing.test_control import expect_not_raises
@ -17,7 +19,6 @@ logger = logging.getLogger("NeoLogger")
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.grpc_api
@pytest.mark.grpc_without_user @pytest.mark.grpc_without_user
class TestObjectApiWithoutUser(ClusterTestBase): class TestObjectApiWithoutUser(ClusterTestBase):
def _parse_oid(self, stdout: str) -> str: def _parse_oid(self, stdout: str) -> str:
@ -30,72 +31,96 @@ class TestObjectApiWithoutUser(ClusterTestBase):
tombstone = id_str.split(":")[1] tombstone = id_str.split(":")[1]
return tombstone.strip() return tombstone.strip()
@pytest.fixture(scope="function")
def public_container(self, default_wallet: WalletInfo) -> str:
with reporter.step("Create public container"):
cid_public = create_container(
default_wallet,
self.shell,
self.cluster.default_rpc_endpoint,
basic_acl=PUBLIC_ACL_F,
)
return cid_public
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def cli_without_wallet(self, client_shell: Shell) -> FrostfsCli: def frostfs_cli(self, client_shell: Shell) -> FrostfsCli:
return FrostfsCli(client_shell, FROSTFS_CLI_EXEC) return FrostfsCli(client_shell, FROSTFS_CLI_EXEC)
@allure.title("Get public container by native API with generate private key") @allure.title("Get public container by native API with generate private key")
def test_get_container_with_generated_key(self, cli_without_wallet: FrostfsCli, container: str, rpc_endpoint: str): def test_get_container_with_generated_key(self, frostfs_cli: FrostfsCli, public_container: str):
""" """
Validate `container get` native API with flag `--generate-key`. Validate `container get` native API with flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Get container with generate key"): with reporter.step("Get container with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.container.get(rpc_endpoint, container, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) frostfs_cli.container.get(rpc_endpoint, cid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
@allure.title("Get list containers by native API with generate private key") @allure.title("Get list containers by native API with generate private key")
def test_list_containers_with_generated_key( def test_list_containers_with_generated_key(self, frostfs_cli: FrostfsCli, default_wallet: WalletInfo, public_container: str):
self, cli_without_wallet: FrostfsCli, default_wallet: WalletInfo, container: str, rpc_endpoint: str
):
""" """
Validate `container list` native API with flag `--generate-key`. Validate `container list` native API with flag `--generate-key`.
""" """
rpc_endpoint = self.cluster.default_rpc_endpoint
owner = default_wallet.get_address_from_json(0) owner = default_wallet.get_address_from_json(0)
with reporter.step("List containers with generate key"): with reporter.step("List containers with generate key"):
with expect_not_raises(): with expect_not_raises():
result = cli_without_wallet.container.list(rpc_endpoint, owner=owner, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) result = frostfs_cli.container.list(rpc_endpoint, owner=owner, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
with reporter.step("Expect container in received containers list"): with reporter.step("Expect container in received containers list"):
containers = result.stdout.split() containers = result.stdout.split()
assert container in containers assert public_container in containers
@allure.title("Get list of public container objects by native API with generate private key") @allure.title("Get list of public container objects by native API with generate private key")
def test_list_objects_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, rpc_endpoint: str): def test_list_objects_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str):
""" """
Validate `container list_objects` native API with flag `--generate-key`. Validate `container list_objects` native API with flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("List objects with generate key"): with reporter.step("List objects with generate key"):
with expect_not_raises(): with expect_not_raises():
result = cli_without_wallet.container.list_objects(rpc_endpoint, container, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) result = frostfs_cli.container.list_objects(rpc_endpoint, cid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
with reporter.step("Expect empty objects list"): with reporter.step("Expect empty objects list"):
objects = result.stdout.split() objects = result.stdout.split()
assert len(objects) == 0, objects assert len(objects) == 0, objects
@allure.title("Search public container nodes by native API with generate private key") @allure.title("Search public container nodes by native API with generate private key")
def test_search_nodes_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, rpc_endpoint: str): def test_search_nodes_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str):
""" """
Validate `container search_node` native API with flag `--generate-key`. Validate `container search_node` native API with flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Search nodes with generate key"): with reporter.step("Search nodes with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.container.search_node(rpc_endpoint, container, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) frostfs_cli.container.search_node(rpc_endpoint, cid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
@allure.title("Put object into public container by native API with generate private key (obj_size={object_size})") @allure.title("Put object into public container by native API with generate private key (obj_size={object_size})")
def test_put_object_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_put_object_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object put` into container with public ACL and flag `--generate-key`. Validate `object put` into container with public ACL and flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
with expect_not_raises(): with expect_not_raises():
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -105,24 +130,26 @@ class TestObjectApiWithoutUser(ClusterTestBase):
oid = self._parse_oid(result.stdout) oid = self._parse_oid(result.stdout)
with reporter.step("List objects with generate key"): with reporter.step("List objects with generate key"):
result = cli_without_wallet.container.list_objects(rpc_endpoint, container, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) result = frostfs_cli.container.list_objects(rpc_endpoint, cid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
with reporter.step("Expect object in received objects list"): with reporter.step("Expect object in received objects list"):
objects = result.stdout.split() objects = result.stdout.split()
assert oid in objects, objects assert oid in objects, objects
@allure.title("Get public container object by native API with generate private key (obj_size={object_size})") @allure.title("Get public container object by native API with generate private key (obj_size={object_size})")
def test_get_object_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_get_object_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object get` for container with public ACL and flag `--generate-key`. Validate `object get` for container with public ACL and flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
expected_hash = get_file_hash(file_path) expected_hash = get_file_hash(file_path)
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -133,9 +160,9 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Get object with generate key"): with reporter.step("Get object with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.object.get( frostfs_cli.object.get(
rpc_endpoint, rpc_endpoint,
container, cid,
oid, oid,
file=file_path, file=file_path,
generate_key=True, generate_key=True,
@ -149,15 +176,18 @@ class TestObjectApiWithoutUser(ClusterTestBase):
assert expected_hash == downloaded_hash assert expected_hash == downloaded_hash
@allure.title("Head public container object by native API with generate private key (obj_size={object_size})") @allure.title("Head public container object by native API with generate private key (obj_size={object_size})")
def test_head_object_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_head_object_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object head` for container with public ACL and flag `--generate-key`. Validate `object head` for container with public ACL and flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -168,18 +198,21 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Head object with generate key"): with reporter.step("Head object with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.object.head(rpc_endpoint, container, oid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) frostfs_cli.object.head(rpc_endpoint, cid, oid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
@allure.title("Delete public container object by native API with generate private key (obj_size={object_size})") @allure.title("Delete public container object by native API with generate private key (obj_size={object_size})")
def test_delete_object_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_delete_object_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object delete` for container with public ACL and flag `--generate key`. Validate `object delete` for container with public ACL and flag `--generate key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -190,14 +223,14 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Delete object with generate key"): with reporter.step("Delete object with generate key"):
with expect_not_raises(): with expect_not_raises():
result = cli_without_wallet.object.delete(rpc_endpoint, container, oid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) result = frostfs_cli.object.delete(rpc_endpoint, cid, oid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
oid = self._parse_tombstone_oid(result.stdout) oid = self._parse_tombstone_oid(result.stdout)
with reporter.step("Head object with generate key"): with reporter.step("Head object with generate key"):
result = cli_without_wallet.object.head( result = frostfs_cli.object.head(
rpc_endpoint, rpc_endpoint,
container, cid,
oid, oid,
generate_key=True, generate_key=True,
timeout=CLI_DEFAULT_TIMEOUT, timeout=CLI_DEFAULT_TIMEOUT,
@ -208,16 +241,19 @@ class TestObjectApiWithoutUser(ClusterTestBase):
assert object_type == "TOMBSTONE", object_type assert object_type == "TOMBSTONE", object_type
@allure.title("Lock public container object by native API with generate private key (obj_size={object_size})") @allure.title("Lock public container object by native API with generate private key (obj_size={object_size})")
def test_lock_object_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_lock_object_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object lock` for container with public ACL and flag `--generate-key`. Validate `object lock` for container with public ACL and flag `--generate-key`.
Attempt to delete the locked object. Attempt to delete the locked object.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -228,9 +264,9 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Lock object with generate key"): with reporter.step("Lock object with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.object.lock( frostfs_cli.object.lock(
rpc_endpoint, rpc_endpoint,
container, cid,
oid, oid,
generate_key=True, generate_key=True,
timeout=CLI_DEFAULT_TIMEOUT, timeout=CLI_DEFAULT_TIMEOUT,
@ -239,24 +275,27 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Delete locked object with generate key and expect error"): with reporter.step("Delete locked object with generate key and expect error"):
with pytest.raises(Exception, match=OBJECT_IS_LOCKED): with pytest.raises(Exception, match=OBJECT_IS_LOCKED):
cli_without_wallet.object.delete( frostfs_cli.object.delete(
rpc_endpoint, rpc_endpoint,
container, cid,
oid, oid,
generate_key=True, generate_key=True,
timeout=CLI_DEFAULT_TIMEOUT, timeout=CLI_DEFAULT_TIMEOUT,
) )
@allure.title("Search public container objects by native API with generate private key (obj_size={object_size})") @allure.title("Search public container objects by native API with generate private key (obj_size={object_size})")
def test_search_object_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_search_object_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object search` for container with public ACL and flag `--generate-key`. Validate `object search` for container with public ACL and flag `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -267,22 +306,25 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Object search with generate key"): with reporter.step("Object search with generate key"):
with expect_not_raises(): with expect_not_raises():
result = cli_without_wallet.object.search(rpc_endpoint, container, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT) result = frostfs_cli.object.search(rpc_endpoint, cid, generate_key=True, timeout=CLI_DEFAULT_TIMEOUT)
with reporter.step("Expect object in received objects list of container"): with reporter.step("Expect object in received objects list of container"):
object_ids = re.findall(r"(\w{43,44})", result.stdout) object_ids = re.findall(r"(\w{43,44})", result.stdout)
assert oid in object_ids assert oid in object_ids
@allure.title("Get range of public container object by native API with generate private key (obj_size={object_size})") @allure.title("Get range of public container object by native API with generate private key (obj_size={object_size})")
def test_range_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_range_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object range` for container with public ACL and `--generate-key`. Validate `object range` for container with public ACL and `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -293,9 +335,9 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Get range of object with generate key"): with reporter.step("Get range of object with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.object.range( frostfs_cli.object.range(
rpc_endpoint, rpc_endpoint,
container, cid,
oid, oid,
"0:10", "0:10",
file=file_path, file=file_path,
@ -304,15 +346,18 @@ class TestObjectApiWithoutUser(ClusterTestBase):
) )
@allure.title("Get hash of public container object by native API with generate private key (obj_size={object_size})") @allure.title("Get hash of public container object by native API with generate private key (obj_size={object_size})")
def test_hash_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_hash_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object hash` for container with public ACL and `--generate-key`. Validate `object hash` for container with public ACL and `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
generate_key=True, generate_key=True,
no_progress=True, no_progress=True,
@ -323,9 +368,9 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Get range hash of object with generate key"): with reporter.step("Get range hash of object with generate key"):
with expect_not_raises(): with expect_not_raises():
cli_without_wallet.object.hash( frostfs_cli.object.hash(
rpc_endpoint, rpc_endpoint,
container, cid,
oid, oid,
range="0:10", range="0:10",
generate_key=True, generate_key=True,
@ -333,15 +378,18 @@ class TestObjectApiWithoutUser(ClusterTestBase):
) )
@allure.title("Get public container object nodes by native API with generate private key (obj_size={object_size})") @allure.title("Get public container object nodes by native API with generate private key (obj_size={object_size})")
def test_nodes_with_generate_key(self, cli_without_wallet: FrostfsCli, container: str, file_path: TestFile, rpc_endpoint: str): def test_nodes_with_generate_key(self, frostfs_cli: FrostfsCli, public_container: str, file_path: TestFile):
""" """
Validate `object nodes` for container with public ACL and `--generate-key`. Validate `object nodes` for container with public ACL and `--generate-key`.
""" """
cid = public_container
rpc_endpoint = self.cluster.default_rpc_endpoint
with reporter.step("Put object with generate key"): with reporter.step("Put object with generate key"):
result = cli_without_wallet.object.put( result = frostfs_cli.object.put(
rpc_endpoint, rpc_endpoint,
container, cid,
file_path, file_path,
no_progress=True, no_progress=True,
generate_key=True, generate_key=True,
@ -353,14 +401,14 @@ class TestObjectApiWithoutUser(ClusterTestBase):
with reporter.step("Configure frostfs-cli for alive remote node"): with reporter.step("Configure frostfs-cli for alive remote node"):
alive_node = self.cluster.cluster_nodes[0] alive_node = self.cluster.cluster_nodes[0]
node_shell = alive_node.host.get_shell() node_shell = alive_node.host.get_shell()
alive_endpoint = alive_node.storage_node.get_rpc_endpoint() rpc_endpoint = alive_node.storage_node.get_rpc_endpoint()
node_frostfs_cli_wo_wallet = FrostfsCli(node_shell, FROSTFS_CLI_EXEC) node_frostfs_cli = FrostfsCli(node_shell, FROSTFS_CLI_EXEC)
with reporter.step("Get object nodes with generate key"): with reporter.step("Get object nodes with generate key"):
with expect_not_raises(): with expect_not_raises():
node_frostfs_cli_wo_wallet.object.nodes( node_frostfs_cli.object.nodes(
alive_endpoint, rpc_endpoint,
container, cid,
oid=oid, oid=oid,
generate_key=True, generate_key=True,
timeout=CLI_DEFAULT_TIMEOUT, timeout=CLI_DEFAULT_TIMEOUT,

View file

@ -23,10 +23,7 @@ from frostfs_testlib.utils.file_utils import generate_file
logger = logging.getLogger("NeoLogger") logger = logging.getLogger("NeoLogger")
EXPIRATION_TIMESTAMP_HEADER = "__SYSTEM__EXPIRATION_TIMESTAMP" EXPIRATION_TIMESTAMP_HEADER = "__SYSTEM__EXPIRATION_TIMESTAMP"
# TODO: Depreacated. Use EXPIRATION_EPOCH_ATTRIBUTE from testlib
EXPIRATION_EPOCH_HEADER = "__SYSTEM__EXPIRATION_EPOCH" EXPIRATION_EPOCH_HEADER = "__SYSTEM__EXPIRATION_EPOCH"
EXPIRATION_DURATION_HEADER = "__SYSTEM__EXPIRATION_DURATION" EXPIRATION_DURATION_HEADER = "__SYSTEM__EXPIRATION_DURATION"
EXPIRATION_EXPIRATION_RFC = "__SYSTEM__EXPIRATION_RFC3339" EXPIRATION_EXPIRATION_RFC = "__SYSTEM__EXPIRATION_RFC3339"
SYSTEM_EXPIRATION_EPOCH = "System-Expiration-Epoch" SYSTEM_EXPIRATION_EPOCH = "System-Expiration-Epoch"
@ -154,7 +151,9 @@ class Test_http_system_header(ClusterTestBase):
def test_unable_put_negative_duration(self, user_container: str, simple_object_size: ObjectSize): def test_unable_put_negative_duration(self, user_container: str, simple_object_size: ObjectSize):
headers = attr_into_str_header_curl({"System-Expiration-Duration": "-1h"}) headers = attr_into_str_header_curl({"System-Expiration-Duration": "-1h"})
file_path = generate_file(simple_object_size.value) file_path = generate_file(simple_object_size.value)
with reporter.step("Put object using HTTP with attribute System-Expiration-Duration where duration is negative"): with reporter.step(
"Put object using HTTP with attribute System-Expiration-Duration where duration is negative"
):
upload_via_http_gate_curl( upload_via_http_gate_curl(
cid=user_container, cid=user_container,
filepath=file_path, filepath=file_path,
@ -167,7 +166,9 @@ class Test_http_system_header(ClusterTestBase):
def test_unable_put_expired_timestamp(self, user_container: str, simple_object_size: ObjectSize): def test_unable_put_expired_timestamp(self, user_container: str, simple_object_size: ObjectSize):
headers = attr_into_str_header_curl({"System-Expiration-Timestamp": "1635075727"}) headers = attr_into_str_header_curl({"System-Expiration-Timestamp": "1635075727"})
file_path = generate_file(simple_object_size.value) file_path = generate_file(simple_object_size.value)
with reporter.step("Put object using HTTP with attribute System-Expiration-Timestamp where duration is in the past"): with reporter.step(
"Put object using HTTP with attribute System-Expiration-Timestamp where duration is in the past"
):
upload_via_http_gate_curl( upload_via_http_gate_curl(
cid=user_container, cid=user_container,
filepath=file_path, filepath=file_path,
@ -176,7 +177,9 @@ class Test_http_system_header(ClusterTestBase):
error_pattern=f"{EXPIRATION_TIMESTAMP_HEADER} must be in the future", error_pattern=f"{EXPIRATION_TIMESTAMP_HEADER} must be in the future",
) )
@allure.title("[NEGATIVE] Put object using HTTP with attribute System-Expiration-RFC3339 where duration is in the past") @allure.title(
"[NEGATIVE] Put object using HTTP with attribute System-Expiration-RFC3339 where duration is in the past"
)
def test_unable_put_expired_rfc(self, user_container: str, simple_object_size: ObjectSize): def test_unable_put_expired_rfc(self, user_container: str, simple_object_size: ObjectSize):
headers = attr_into_str_header_curl({"System-Expiration-RFC3339": "2021-11-22T09:55:49Z"}) headers = attr_into_str_header_curl({"System-Expiration-RFC3339": "2021-11-22T09:55:49Z"})
file_path = generate_file(simple_object_size.value) file_path = generate_file(simple_object_size.value)
@ -201,7 +204,9 @@ class Test_http_system_header(ClusterTestBase):
with reporter.step( with reporter.step(
f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr" f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr"
): ):
oid, head_info = self.oid_header_info_for_object(file_path=file_path, attributes=attributes, user_container=user_container) oid, head_info = self.oid_header_info_for_object(
file_path=file_path, attributes=attributes, user_container=user_container
)
self.validation_for_http_header_attr(head_info=head_info, expected_epoch=expected_epoch) self.validation_for_http_header_attr(head_info=head_info, expected_epoch=expected_epoch)
with reporter.step("Check that object becomes unavailable when epoch is expired"): with reporter.step("Check that object becomes unavailable when epoch is expired"):
for _ in range(0, epoch_count + 1): for _ in range(0, epoch_count + 1):
@ -238,7 +243,9 @@ class Test_http_system_header(ClusterTestBase):
with reporter.step( with reporter.step(
f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr" f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr"
): ):
oid, head_info = self.oid_header_info_for_object(file_path=file_path, attributes=attributes, user_container=user_container) oid, head_info = self.oid_header_info_for_object(
file_path=file_path, attributes=attributes, user_container=user_container
)
self.validation_for_http_header_attr(head_info=head_info, expected_epoch=expected_epoch) self.validation_for_http_header_attr(head_info=head_info, expected_epoch=expected_epoch)
with reporter.step("Check that object becomes unavailable when epoch is expired"): with reporter.step("Check that object becomes unavailable when epoch is expired"):
for _ in range(0, epoch_count + 1): for _ in range(0, epoch_count + 1):
@ -269,13 +276,17 @@ class Test_http_system_header(ClusterTestBase):
) )
attributes = { attributes = {
SYSTEM_EXPIRATION_TIMESTAMP: self.epoch_count_into_timestamp(epoch_duration=epoch_duration, epoch=2), SYSTEM_EXPIRATION_TIMESTAMP: self.epoch_count_into_timestamp(epoch_duration=epoch_duration, epoch=2),
SYSTEM_EXPIRATION_RFC3339: self.epoch_count_into_timestamp(epoch_duration=epoch_duration, epoch=1, rfc3339=True), SYSTEM_EXPIRATION_RFC3339: self.epoch_count_into_timestamp(
epoch_duration=epoch_duration, epoch=1, rfc3339=True
),
} }
file_path = generate_file(object_size.value) file_path = generate_file(object_size.value)
with reporter.step( with reporter.step(
f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr" f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr"
): ):
oid, head_info = self.oid_header_info_for_object(file_path=file_path, attributes=attributes, user_container=user_container) oid, head_info = self.oid_header_info_for_object(
file_path=file_path, attributes=attributes, user_container=user_container
)
self.validation_for_http_header_attr(head_info=head_info, expected_epoch=expected_epoch) self.validation_for_http_header_attr(head_info=head_info, expected_epoch=expected_epoch)
with reporter.step("Check that object becomes unavailable when epoch is expired"): with reporter.step("Check that object becomes unavailable when epoch is expired"):
for _ in range(0, epoch_count + 1): for _ in range(0, epoch_count + 1):
@ -303,14 +314,20 @@ class Test_http_system_header(ClusterTestBase):
["simple"], ["simple"],
indirect=True, indirect=True,
) )
def test_http_rfc_object_unavailable_after_expir(self, user_container: str, object_size: ObjectSize, epoch_duration: int): def test_http_rfc_object_unavailable_after_expir(
self, user_container: str, object_size: ObjectSize, epoch_duration: int
):
self.tick_epoch() self.tick_epoch()
epoch_count = 2 epoch_count = 2
expected_epoch = get_epoch(self.shell, self.cluster) + epoch_count expected_epoch = get_epoch(self.shell, self.cluster) + epoch_count
logger.info( logger.info(
f"epoch duration={epoch_duration}, current_epoch= {get_epoch(self.shell, self.cluster)} expected_epoch {expected_epoch}" f"epoch duration={epoch_duration}, current_epoch= {get_epoch(self.shell, self.cluster)} expected_epoch {expected_epoch}"
) )
attributes = {SYSTEM_EXPIRATION_RFC3339: self.epoch_count_into_timestamp(epoch_duration=epoch_duration, epoch=2, rfc3339=True)} attributes = {
SYSTEM_EXPIRATION_RFC3339: self.epoch_count_into_timestamp(
epoch_duration=epoch_duration, epoch=2, rfc3339=True
)
}
file_path = generate_file(object_size.value) file_path = generate_file(object_size.value)
with reporter.step( with reporter.step(
f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr" f"Put objects using HTTP with attributes and head command should display {EXPIRATION_EPOCH_HEADER}: {expected_epoch} attr"

View file

@ -1,4 +1,3 @@
import string
from datetime import datetime, timedelta from datetime import datetime, timedelta
import allure import allure
@ -7,15 +6,8 @@ from frostfs_testlib import reporter
from frostfs_testlib.s3 import S3ClientWrapper, VersioningStatus from frostfs_testlib.s3 import S3ClientWrapper, VersioningStatus
from frostfs_testlib.steps.s3 import s3_helper from frostfs_testlib.steps.s3 import s3_helper
from frostfs_testlib.storage.dataclasses.object_size import ObjectSize from frostfs_testlib.storage.dataclasses.object_size import ObjectSize
from frostfs_testlib.utils import string_utils
from frostfs_testlib.utils.file_utils import generate_file from frostfs_testlib.utils.file_utils import generate_file
VALID_SYMBOLS_WITHOUT_DOT = string.ascii_lowercase + string.digits + "-"
VALID_AND_INVALID_SYMBOLS = string.ascii_letters + string.punctuation
# TODO: The dot symbol is temporarily not supported.
VALID_SYMBOLS_WITH_DOT = VALID_SYMBOLS_WITHOUT_DOT + "."
@pytest.mark.nightly @pytest.mark.nightly
@pytest.mark.s3_gate @pytest.mark.s3_gate
@ -148,87 +140,3 @@ class TestS3GateBucket:
s3_client.delete_bucket(bucket) s3_client.delete_bucket(bucket)
with pytest.raises(Exception, match=r".*Not Found.*"): with pytest.raises(Exception, match=r".*Not Found.*"):
s3_client.head_bucket(bucket) s3_client.head_bucket(bucket)
@allure.title("Create bucket with valid name length (s3_client={s3_client}, length={length})")
@pytest.mark.parametrize("length", [3, 4, 32, 62, 63])
def test_s3_create_bucket_with_valid_length(self, s3_client: S3ClientWrapper, length: int):
bucket_name = string_utils.random_string(length, VALID_SYMBOLS_WITHOUT_DOT)
while not (bucket_name[0].isalnum() and bucket_name[-1].isalnum()):
bucket_name = string_utils.random_string(length, VALID_SYMBOLS_WITHOUT_DOT)
with reporter.step("Create bucket with valid name length"):
s3_client.create_bucket(bucket_name)
with reporter.step("Check bucket name in buckets"):
assert bucket_name in s3_client.list_buckets()
@allure.title("[NEGATIVE] Bucket with invalid name length should not be created (s3_client={s3_client}, length={length})")
@pytest.mark.parametrize("length", [2, 64, 254, 255, 256])
def test_s3_create_bucket_with_invalid_length(self, s3_client: S3ClientWrapper, length: int):
bucket_name = string_utils.random_string(length, VALID_SYMBOLS_WITHOUT_DOT)
while not (bucket_name[0].isalnum() and bucket_name[-1].isalnum()):
bucket_name = string_utils.random_string(length, VALID_SYMBOLS_WITHOUT_DOT)
with reporter.step("Create bucket with invalid name length and catch exception"):
with pytest.raises(Exception, match=".*(?:InvalidBucketName|Invalid bucket name).*"):
s3_client.create_bucket(bucket_name)
@allure.title("[NEGATIVE] Bucket with invalid name should not be created (s3_client={s3_client}, bucket_name={bucket_name})")
@pytest.mark.parametrize(
"bucket_name",
[
"BUCKET-1",
"buckeT-2",
# The following case for AWS CLI is not handled correctly
# "-bucket-3",
"bucket-4-",
".bucket-5",
"bucket-6.",
"bucket..7",
"bucket+8",
"bucket_9",
"bucket 10",
"127.10.5.11",
"xn--bucket-12",
"bucket-13-s3alias",
# The following names can be used in FrostFS but are prohibited by the AWS specification.
# "sthree-bucket-14"
# "sthree-configurator-bucket-15"
# "amzn-s3-demo-bucket-16"
# "sthree-bucket-17"
# "bucket-18--ol-s3"
# "bucket-19--x-s3"
# "bucket-20.mrap"
],
)
def test_s3_create_bucket_with_invalid_name(self, s3_client: S3ClientWrapper, bucket_name: str):
with reporter.step("Create bucket with invalid name and catch exception"):
with pytest.raises(Exception, match=".*(?:InvalidBucketName|Invalid bucket name).*"):
s3_client.create_bucket(bucket_name)
@allure.title("[NEGATIVE] Delete non-empty bucket (s3_client={s3_client})")
def test_s3_check_availability_non_empty_bucket_after_deleting(
self,
bucket: str,
simple_object_size: ObjectSize,
s3_client: S3ClientWrapper,
):
object_path = generate_file(simple_object_size.value)
object_name = s3_helper.object_key_from_file_path(object_path)
with reporter.step("Put object into bucket"):
s3_client.put_object(bucket, object_path)
with reporter.step("Check that object appears in bucket"):
objects = s3_client.list_objects(bucket)
assert objects, f"Expected bucket with object, got empty {objects}"
assert object_name in objects, f"Object {object_name} not found in bucket object list {objects}"
with reporter.step("Try to delete not empty bucket and get error"):
with pytest.raises(Exception, match=r".*The bucket you tried to delete is not empty.*"):
s3_client.delete_bucket(bucket)
with reporter.step("Check bucket availability"):
objects = s3_client.list_objects(bucket)
assert objects, f"Expected bucket with object, got empty {objects}"
assert object_name in objects, f"Object {object_name} not found in bucket object list {objects}"

View file

@ -2,6 +2,7 @@ allure-pytest==2.13.2
allure-python-commons==2.13.2 allure-python-commons==2.13.2
base58==2.1.0 base58==2.1.0
boto3==1.35.30 boto3==1.35.30
botocore==1.19.33
configobj==5.0.6 configobj==5.0.6
neo-mamba==1.0.0 neo-mamba==1.0.0
pexpect==4.8.0 pexpect==4.8.0