Add GenericCli utility

Signed-off-by: Andrey Berezin <a.berezin@yadro.com>
This commit is contained in:
Andrey Berezin 2024-02-14 16:16:59 +03:00
parent 4f3814690e
commit 751381cd60
8 changed files with 80 additions and 71 deletions

View file

@ -1,4 +1,5 @@
from frostfs_testlib.cli.frostfs_adm import FrostfsAdm from frostfs_testlib.cli.frostfs_adm import FrostfsAdm
from frostfs_testlib.cli.frostfs_authmate import FrostfsAuthmate from frostfs_testlib.cli.frostfs_authmate import FrostfsAuthmate
from frostfs_testlib.cli.frostfs_cli import FrostfsCli from frostfs_testlib.cli.frostfs_cli import FrostfsCli
from frostfs_testlib.cli.generic_cli import GenericCli
from frostfs_testlib.cli.neogo import NeoGo, NetworkType from frostfs_testlib.cli.neogo import NeoGo, NetworkType

View file

@ -0,0 +1,30 @@
from typing import Optional
from frostfs_testlib.hosting.interfaces import Host
from frostfs_testlib.shell.interfaces import CommandOptions, Shell
class GenericCli(object):
def __init__(self, cli_name: str, host: Host) -> None:
self.host = host
self.cli_name = cli_name
def __call__(
self,
args: Optional[str] = "",
pipes: Optional[str] = "",
shell: Optional[Shell] = None,
options: Optional[CommandOptions] = None,
):
if not shell:
shell = self.host.get_shell()
cli_config = self.host.get_cli_config(self.cli_name, True)
extra_args = ""
exec_path = self.cli_name
if cli_config:
extra_args = " ".join(cli_config.extra_args)
exec_path = cli_config.exec_path
cmd = f"{exec_path} {args} {extra_args} {pipes}"
return shell.exec(cmd, options)

View file

@ -10,9 +10,7 @@ class ParsedAttributes:
def parse(cls, attributes: dict[str, Any]): def parse(cls, attributes: dict[str, Any]):
# Pick attributes supported by the class # Pick attributes supported by the class
field_names = set(field.name for field in fields(cls)) field_names = set(field.name for field in fields(cls))
supported_attributes = { supported_attributes = {key: value for key, value in attributes.items() if key in field_names}
key: value for key, value in attributes.items() if key in field_names
}
return cls(**supported_attributes) return cls(**supported_attributes)
@ -29,6 +27,7 @@ class CLIConfig:
name: str name: str
exec_path: str exec_path: str
attributes: dict[str, str] = field(default_factory=dict) attributes: dict[str, str] = field(default_factory=dict)
extra_args: list[str] = field(default_factory=list)
@dataclass @dataclass

View file

@ -54,7 +54,7 @@ class Host(ABC):
raise ValueError(f"Unknown service name: '{service_name}'") raise ValueError(f"Unknown service name: '{service_name}'")
return service_config return service_config
def get_cli_config(self, cli_name: str) -> CLIConfig: def get_cli_config(self, cli_name: str, allow_empty: bool = False) -> CLIConfig:
"""Returns config of CLI tool with specified name. """Returns config of CLI tool with specified name.
The CLI must be located on this host. The CLI must be located on this host.
@ -66,7 +66,7 @@ class Host(ABC):
Config of the CLI tool. Config of the CLI tool.
""" """
cli_config = self._cli_config_by_name.get(cli_name) cli_config = self._cli_config_by_name.get(cli_name)
if cli_config is None: if cli_config is None and not allow_empty:
raise ValueError(f"Unknown CLI name: '{cli_name}'") raise ValueError(f"Unknown CLI name: '{cli_name}'")
return cli_config return cli_config

View file

@ -8,7 +8,7 @@ from typing import Optional, Union
import requests import requests
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli import FrostfsCli from frostfs_testlib.cli import FrostfsCli, GenericCli
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.common import DEFAULT_WALLET_CONFIG from frostfs_testlib.resources.common import DEFAULT_WALLET_CONFIG
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
@ -345,8 +345,8 @@ def _parse_cid(output: str) -> str:
@reporter.step("Search container by name") @reporter.step("Search container by name")
def search_container_by_name(name: str, node: ClusterNode): def search_container_by_name(name: str, node: ClusterNode):
node_shell = node.host.get_shell() curl = GenericCli("curl", node.host)
output = node_shell.exec(f"curl -I HEAD http://127.0.0.1:8084/{name}") output = curl(f"-I http://127.0.0.1:8084/{name}")
pattern = r"X-Container-Id: (\S+)" pattern = r"X-Container-Id: (\S+)"
cid = re.findall(pattern, output.stdout) cid = re.findall(pattern, output.stdout)
if cid: if cid:

View file

@ -11,13 +11,14 @@ from urllib.parse import quote_plus
import requests import requests
from frostfs_testlib import reporter from frostfs_testlib import reporter
from frostfs_testlib.cli import GenericCli
from frostfs_testlib.resources.common import SIMPLE_OBJECT_SIZE from frostfs_testlib.resources.common import SIMPLE_OBJECT_SIZE
from frostfs_testlib.s3.aws_cli_client import command_options from frostfs_testlib.s3.aws_cli_client import command_options
from frostfs_testlib.shell import Shell from frostfs_testlib.shell import Shell
from frostfs_testlib.shell.local_shell import LocalShell from frostfs_testlib.shell.local_shell import LocalShell
from frostfs_testlib.steps.cli.object import get_object from frostfs_testlib.steps.cli.object import get_object
from frostfs_testlib.steps.storage_policy import get_nodes_without_object from frostfs_testlib.steps.storage_policy import get_nodes_without_object
from frostfs_testlib.storage.cluster import StorageNode from frostfs_testlib.storage.cluster import ClusterNode, StorageNode
from frostfs_testlib.testing.test_control import retry from frostfs_testlib.testing.test_control import retry
from frostfs_testlib.utils.file_utils import get_file_hash from frostfs_testlib.utils.file_utils import get_file_hash
@ -31,8 +32,7 @@ local_shell = LocalShell()
def get_via_http_gate( def get_via_http_gate(
cid: str, cid: str,
oid: str, oid: str,
endpoint: str, node: ClusterNode,
http_hostname: str,
request_path: Optional[str] = None, request_path: Optional[str] = None,
timeout: Optional[int] = 300, timeout: Optional[int] = 300,
): ):
@ -40,18 +40,19 @@ def get_via_http_gate(
This function gets given object from HTTP gate This function gets given object from HTTP gate
cid: container id to get object from cid: container id to get object from
oid: object ID oid: object ID
endpoint: http gate endpoint node: node to make request
http_hostname: http host name on the node
request_path: (optional) http request, if ommited - use default [{endpoint}/get/{cid}/{oid}] request_path: (optional) http request, if ommited - use default [{endpoint}/get/{cid}/{oid}]
""" """
# if `request_path` parameter omitted, use default # if `request_path` parameter omitted, use default
if request_path is None: if request_path is None:
request = f"{endpoint}/get/{cid}/{oid}" request = f"{node.http_gate.get_endpoint()}/get/{cid}/{oid}"
else: else:
request = f"{endpoint}{request_path}" request = f"{node.http_gate.get_endpoint()}{request_path}"
resp = requests.get(request, headers={"Host": http_hostname}, stream=True, timeout=timeout, verify=False) resp = requests.get(
request, headers={"Host": node.storage_node.get_http_hostname()[0]}, stream=True, timeout=timeout, verify=False
)
if not resp.ok: if not resp.ok:
raise Exception( raise Exception(
@ -72,15 +73,14 @@ def get_via_http_gate(
@reporter.step("Get via Zip HTTP Gate") @reporter.step("Get via Zip HTTP Gate")
def get_via_zip_http_gate(cid: str, prefix: str, endpoint: str, http_hostname: str, timeout: Optional[int] = 300): def get_via_zip_http_gate(cid: str, prefix: str, node: ClusterNode, timeout: Optional[int] = 300):
""" """
This function gets given object from HTTP gate This function gets given object from HTTP gate
cid: container id to get object from cid: container id to get object from
prefix: common prefix prefix: common prefix
endpoint: http gate endpoint node: node to make request
http_hostname: http host name on the node
""" """
request = f"{endpoint}/zip/{cid}/{prefix}" request = f"{node.http_gate.get_endpoint()}/zip/{cid}/{prefix}"
resp = requests.get(request, stream=True, timeout=timeout, verify=False) resp = requests.get(request, stream=True, timeout=timeout, verify=False)
if not resp.ok: if not resp.ok:
@ -109,8 +109,7 @@ def get_via_zip_http_gate(cid: str, prefix: str, endpoint: str, http_hostname: s
def get_via_http_gate_by_attribute( def get_via_http_gate_by_attribute(
cid: str, cid: str,
attribute: dict, attribute: dict,
endpoint: str, node: ClusterNode,
http_hostname: str,
request_path: Optional[str] = None, request_path: Optional[str] = None,
timeout: Optional[int] = 300, timeout: Optional[int] = 300,
): ):
@ -126,11 +125,13 @@ def get_via_http_gate_by_attribute(
attr_value = quote_plus(str(attribute.get(attr_name))) attr_value = quote_plus(str(attribute.get(attr_name)))
# if `request_path` parameter ommited, use default # if `request_path` parameter ommited, use default
if request_path is None: if request_path is None:
request = f"{endpoint}/get_by_attribute/{cid}/{quote_plus(str(attr_name))}/{attr_value}" request = f"{node.http_gate.get_endpoint()}/get_by_attribute/{cid}/{quote_plus(str(attr_name))}/{attr_value}"
else: else:
request = f"{endpoint}{request_path}" request = f"{node.http_gate.get_endpoint()}{request_path}"
resp = requests.get(request, stream=True, timeout=timeout, verify=False, headers={"Host": http_hostname}) resp = requests.get(
request, stream=True, timeout=timeout, verify=False, headers={"Host": node.storage_node.get_http_hostname()[0]}
)
if not resp.ok: if not resp.ok:
raise Exception( raise Exception(
@ -247,19 +248,18 @@ def upload_via_http_gate_curl(
@retry(max_attempts=3, sleep_interval=1) @retry(max_attempts=3, sleep_interval=1)
@reporter.step("Get via HTTP Gate using Curl") @reporter.step("Get via HTTP Gate using Curl")
def get_via_http_curl(cid: str, oid: str, endpoint: str, http_hostname: str) -> str: def get_via_http_curl(cid: str, oid: str, node: ClusterNode) -> str:
""" """
This function gets given object from HTTP gate using curl utility. This function gets given object from HTTP gate using curl utility.
cid: CID to get object from cid: CID to get object from
oid: object OID oid: object OID
endpoint: http gate endpoint node: node for request
http_hostname: http host name of the node
""" """
request = f"{endpoint}/get/{cid}/{oid}" request = f"{node.http_gate.get_endpoint()}/get/{cid}/{oid}"
file_path = os.path.join(os.getcwd(), ASSETS_DIR, f"{cid}_{oid}_{str(uuid.uuid4())}") file_path = os.path.join(os.getcwd(), ASSETS_DIR, f"{cid}_{oid}_{str(uuid.uuid4())}")
cmd = f'curl -k -H "Host: {http_hostname}" {request} > {file_path}' curl = GenericCli("curl", node.host)
local_shell.exec(cmd) curl(f'-k -H "Host: {node.storage_node.get_http_hostname()[0]}"', f"{request} > {file_path}", shell=local_shell)
return file_path return file_path
@ -274,12 +274,11 @@ def _attach_allure_step(request: str, status_code: int, req_type="GET"):
def try_to_get_object_and_expect_error( def try_to_get_object_and_expect_error(
cid: str, cid: str,
oid: str, oid: str,
node: ClusterNode,
error_pattern: str, error_pattern: str,
endpoint: str,
http_hostname: str,
) -> None: ) -> None:
try: try:
get_via_http_gate(cid=cid, oid=oid, endpoint=endpoint, http_hostname=http_hostname) get_via_http_gate(cid=cid, oid=oid, node=node)
raise AssertionError(f"Expected error on getting object with cid: {cid}") raise AssertionError(f"Expected error on getting object with cid: {cid}")
except Exception as err: except Exception as err:
match = error_pattern.casefold() in str(err).casefold() match = error_pattern.casefold() in str(err).casefold()
@ -292,13 +291,10 @@ def get_object_by_attr_and_verify_hashes(
file_name: str, file_name: str,
cid: str, cid: str,
attrs: dict, attrs: dict,
endpoint: str, node: ClusterNode,
http_hostname: str,
) -> None: ) -> None:
got_file_path_http = get_via_http_gate(cid=cid, oid=oid, endpoint=endpoint, http_hostname=http_hostname) got_file_path_http = get_via_http_gate(cid=cid, oid=oid, node=node)
got_file_path_http_attr = get_via_http_gate_by_attribute( got_file_path_http_attr = get_via_http_gate_by_attribute(cid=cid, attribute=attrs, node=node)
cid=cid, attribute=attrs, endpoint=endpoint, http_hostname=http_hostname
)
assert_hashes_are_equal(file_name, got_file_path_http, got_file_path_http_attr) assert_hashes_are_equal(file_name, got_file_path_http, got_file_path_http_attr)
@ -309,8 +305,7 @@ def verify_object_hash(
cid: str, cid: str,
shell: Shell, shell: Shell,
nodes: list[StorageNode], nodes: list[StorageNode],
endpoint: str, request_node: ClusterNode,
http_hostname: str,
object_getter=None, object_getter=None,
) -> None: ) -> None:
@ -336,7 +331,7 @@ def verify_object_hash(
shell=shell, shell=shell,
endpoint=random_node.get_rpc_endpoint(), endpoint=random_node.get_rpc_endpoint(),
) )
got_file_path_http = object_getter(cid=cid, oid=oid, endpoint=endpoint, http_hostname=http_hostname) got_file_path_http = object_getter(cid=cid, oid=oid, node=request_node)
assert_hashes_are_equal(file_name, got_file_path, got_file_path_http) assert_hashes_are_equal(file_name, got_file_path, got_file_path_http)
@ -365,10 +360,9 @@ def attr_into_str_header_curl(attrs: dict) -> list:
def try_to_get_object_via_passed_request_and_expect_error( def try_to_get_object_via_passed_request_and_expect_error(
cid: str, cid: str,
oid: str, oid: str,
node: ClusterNode,
error_pattern: str, error_pattern: str,
endpoint: str,
http_request_path: str, http_request_path: str,
http_hostname: str,
attrs: Optional[dict] = None, attrs: Optional[dict] = None,
) -> None: ) -> None:
try: try:
@ -376,17 +370,15 @@ def try_to_get_object_via_passed_request_and_expect_error(
get_via_http_gate( get_via_http_gate(
cid=cid, cid=cid,
oid=oid, oid=oid,
endpoint=endpoint, node=node,
request_path=http_request_path, request_path=http_request_path,
http_hostname=http_hostname,
) )
else: else:
get_via_http_gate_by_attribute( get_via_http_gate_by_attribute(
cid=cid, cid=cid,
attribute=attrs, attribute=attrs,
endpoint=endpoint, node=node,
request_path=http_request_path, request_path=http_request_path,
http_hostname=http_hostname,
) )
raise AssertionError(f"Expected error on getting object with cid: {cid}") raise AssertionError(f"Expected error on getting object with cid: {cid}")
except Exception as err: except Exception as err:

View file

@ -326,6 +326,8 @@ class ClusterStateController:
@reporter.step("Restore blocked nodes") @reporter.step("Restore blocked nodes")
def restore_all_traffic(self): def restore_all_traffic(self):
if not self.dropped_traffic:
return
parallel(self._restore_traffic_to_node, self.dropped_traffic) parallel(self._restore_traffic_to_node, self.dropped_traffic)
@run_optionally(optionals.OPTIONAL_FAILOVER_ENABLED) @run_optionally(optionals.OPTIONAL_FAILOVER_ENABLED)

View file

@ -5,6 +5,7 @@ from frostfs_testlib.storage.constants import ConfigAttributes
from frostfs_testlib.storage.dataclasses.node_base import NodeBase from frostfs_testlib.storage.dataclasses.node_base import NodeBase
from frostfs_testlib.storage.dataclasses.shard import Shard from frostfs_testlib.storage.dataclasses.shard import Shard
class InnerRing(NodeBase): class InnerRing(NodeBase):
""" """
Class represents inner ring node in a cluster Class represents inner ring node in a cluster
@ -17,11 +18,7 @@ class InnerRing(NodeBase):
def service_healthcheck(self) -> bool: def service_healthcheck(self) -> bool:
health_metric = "frostfs_ir_ir_health" health_metric = "frostfs_ir_ir_health"
output = ( output = self.host.get_shell().exec(f"curl -s localhost:6662 | grep {health_metric} | sed 1,2d").stdout
self.host.get_shell()
.exec(f"curl -s localhost:6662 | grep {health_metric} | sed 1,2d")
.stdout
)
return health_metric in output return health_metric in output
def get_netmap_cleaner_threshold(self) -> str: def get_netmap_cleaner_threshold(self) -> str:
@ -50,11 +47,7 @@ class S3Gate(NodeBase):
def service_healthcheck(self) -> bool: def service_healthcheck(self) -> bool:
health_metric = "frostfs_s3_gw_state_health" health_metric = "frostfs_s3_gw_state_health"
output = ( output = self.host.get_shell().exec(f"curl -s localhost:8086 | grep {health_metric} | sed 1,2d").stdout
self.host.get_shell()
.exec(f"curl -s localhost:8086 | grep {health_metric} | sed 1,2d")
.stdout
)
return health_metric in output return health_metric in output
@property @property
@ -72,11 +65,7 @@ class HTTPGate(NodeBase):
def service_healthcheck(self) -> bool: def service_healthcheck(self) -> bool:
health_metric = "frostfs_http_gw_state_health" health_metric = "frostfs_http_gw_state_health"
output = ( output = self.host.get_shell().exec(f"curl -s localhost:5662 | grep {health_metric} | sed 1,2d").stdout
self.host.get_shell()
.exec(f"curl -s localhost:5662 | grep {health_metric} | sed 1,2d")
.stdout
)
return health_metric in output return health_metric in output
@property @property
@ -135,11 +124,7 @@ class StorageNode(NodeBase):
def service_healthcheck(self) -> bool: def service_healthcheck(self) -> bool:
health_metric = "frostfs_node_state_health" health_metric = "frostfs_node_state_health"
output = ( output = self.host.get_shell().exec(f"curl -s localhost:6672 | grep {health_metric} | sed 1,2d").stdout
self.host.get_shell()
.exec(f"curl -s localhost:6672 | grep {health_metric} | sed 1,2d")
.stdout
)
return health_metric in output return health_metric in output
def get_shard_config_path(self) -> str: def get_shard_config_path(self) -> str:
@ -174,10 +159,10 @@ class StorageNode(NodeBase):
def get_storage_config(self) -> str: def get_storage_config(self) -> str:
return self.host.get_storage_config(self.name) return self.host.get_storage_config(self.name)
def get_http_hostname(self) -> str: def get_http_hostname(self) -> list[str]:
return self._get_attribute(ConfigAttributes.HTTP_HOSTNAME) return self._get_attribute(ConfigAttributes.HTTP_HOSTNAME)
def get_s3_hostname(self) -> str: def get_s3_hostname(self) -> list[str]:
return self._get_attribute(ConfigAttributes.S3_HOSTNAME) return self._get_attribute(ConfigAttributes.S3_HOSTNAME)
def delete_blobovnicza(self): def delete_blobovnicza(self):