From 9c671c0c4fe3830a0a533686f0c6dad37c0b663e Mon Sep 17 00:00:00 2001 From: Evgeniy Zayats Date: Wed, 29 Nov 2023 22:40:48 -0500 Subject: [PATCH] Add functionality to deploy neofs env closes #https://github.com/nspcc-dev/neofs-testcases/issues/672 Signed-off-by: Evgeniy Zayats --- pyproject.toml | 2 + pytest_tests/conftest.py | 5 + pytest_tests/container_policy.json | 3 + pytest_tests/pytest.ini | 7 + pytest_tests/s3_bearer_rules.json | 89 ++++ pytest_tests/test_env.py | 140 ++++++ requirements.txt | 5 + src/neofs_testlib/cli/neofs_adm/morph.py | 6 +- src/neofs_testlib/cli/neofs_cli/cli.py | 3 + src/neofs_testlib/cli/neofs_cli/control.py | 30 ++ src/neofs_testlib/env/__init__.py | 0 src/neofs_testlib/env/env.py | 486 +++++++++++++++++++ src/neofs_testlib/env/templates/cli_cfg.yaml | 2 + src/neofs_testlib/env/templates/ir.yaml | 140 ++++++ src/neofs_testlib/env/templates/network.yaml | 28 ++ src/neofs_testlib/env/templates/s3.yaml | 32 ++ src/neofs_testlib/env/templates/sn.yaml | 79 +++ src/neofs_testlib/env/templates/tls.crt | 22 + src/neofs_testlib/env/templates/tls.key | 27 ++ src/neofs_testlib/utils/wallet.py | 28 +- 20 files changed, 1129 insertions(+), 5 deletions(-) create mode 100644 pytest_tests/conftest.py create mode 100644 pytest_tests/container_policy.json create mode 100644 pytest_tests/pytest.ini create mode 100644 pytest_tests/s3_bearer_rules.json create mode 100644 pytest_tests/test_env.py create mode 100644 src/neofs_testlib/cli/neofs_cli/control.py create mode 100644 src/neofs_testlib/env/__init__.py create mode 100644 src/neofs_testlib/env/env.py create mode 100644 src/neofs_testlib/env/templates/cli_cfg.yaml create mode 100644 src/neofs_testlib/env/templates/ir.yaml create mode 100644 src/neofs_testlib/env/templates/network.yaml create mode 100644 src/neofs_testlib/env/templates/s3.yaml create mode 100644 src/neofs_testlib/env/templates/sn.yaml create mode 100644 src/neofs_testlib/env/templates/tls.crt create mode 100644 src/neofs_testlib/env/templates/tls.key diff --git a/pyproject.toml b/pyproject.toml index 62eaf73..71aa986 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,8 @@ dependencies = [ "paramiko>=2.10.3", "pexpect>=4.8.0", "requests>=2.31.0", + "jinja2>=3.1.2", + "tenacity>=8.2.3", ] requires-python = ">=3.10" diff --git a/pytest_tests/conftest.py b/pytest_tests/conftest.py new file mode 100644 index 0000000..91b9e62 --- /dev/null +++ b/pytest_tests/conftest.py @@ -0,0 +1,5 @@ +def pytest_addoption(parser): + parser.addoption( + "--persist-env", action="store_true", default=False, help="persist deployed env" + ) + parser.addoption("--load-env", action="store", help="load persisted env from file") diff --git a/pytest_tests/container_policy.json b/pytest_tests/container_policy.json new file mode 100644 index 0000000..937b9c2 --- /dev/null +++ b/pytest_tests/container_policy.json @@ -0,0 +1,3 @@ +{ + "rep-1": "REP 1" +} \ No newline at end of file diff --git a/pytest_tests/pytest.ini b/pytest_tests/pytest.ini new file mode 100644 index 0000000..96c3d4b --- /dev/null +++ b/pytest_tests/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +log_cli = 1 +log_cli_level = DEBUG +log_cli_format = %(asctime)s [%(levelname)4s] %(message)s +log_format = %(asctime)s [%(levelname)4s] %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S +log_date_format = %H:%M:%S diff --git a/pytest_tests/s3_bearer_rules.json b/pytest_tests/s3_bearer_rules.json new file mode 100644 index 0000000..acd82b4 --- /dev/null +++ b/pytest_tests/s3_bearer_rules.json @@ -0,0 +1,89 @@ +{ + "records": + [ + { + "operation":"PUT", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + }, + { + "operation":"HEAD", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + }, + { + "operation":"DELETE", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + }, + { + "operation":"SEARCH", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + }, + { + "operation":"GET", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + }, + { + "operation":"GETRANGE", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + }, + { + "operation":"GETRANGEHASH", + "action":"ALLOW", + "filters":[], + "targets": + [ + { + "role":"OTHERS", + "keys":[] + } + ] + } + ] +} diff --git a/pytest_tests/test_env.py b/pytest_tests/test_env.py new file mode 100644 index 0000000..33a8533 --- /dev/null +++ b/pytest_tests/test_env.py @@ -0,0 +1,140 @@ +import json +import os +import re +import uuid +from time import sleep + +import boto3 +import pexpect +import pytest +from botocore.config import Config + +from neofs_testlib.env.env import NeoFSEnv, NodeWallet +from neofs_testlib.utils.wallet import get_last_public_key_from_wallet, init_wallet + + +def _run_with_passwd(cmd: str, password: str) -> str: + child = pexpect.spawn(cmd) + child.delaybeforesend = 1 + child.expect(".*") + child.sendline(f"{password}\r") + child.wait() + cmd = child.read() + return cmd.decode() + + +@pytest.fixture +def neofs_env(request): + if request.config.getoption("--load-env"): + neofs_env = NeoFSEnv.load(request.config.getoption("--load-env")) + else: + neofs_env = NeoFSEnv.simple() + + yield neofs_env + + if request.config.getoption("--persist-env"): + neofs_env.persist() + else: + if not request.config.getoption("--load-env"): + neofs_env.kill() + + +@pytest.fixture +def wallet() -> NodeWallet: + wallet_name = f"{str(uuid.uuid4())}.json" + wallet_path = os.path.join(os.getcwd(), wallet_name) + wallet_password = "password" + wallet_address = init_wallet(wallet_path, wallet_password) + return NodeWallet(path=wallet_path, address=wallet_address, password=wallet_password) + + +@pytest.fixture +def s3_creds(neofs_env: NeoFSEnv, zero_fee, wallet: NodeWallet) -> tuple: + bucket = str(uuid.uuid4()) + s3_bearer_rules = "pytest_tests/s3_bearer_rules.json" + + gate_public_key = get_last_public_key_from_wallet( + neofs_env.s3_gw.wallet.path, neofs_env.s3_gw.wallet.password + ) + cmd = ( + f"{neofs_env.neofs_s3_authmate_path} --debug --with-log --timeout 1m " + f"issue-secret --wallet {wallet.path} --gate-public-key={gate_public_key} " + f"--peer {neofs_env.storage_nodes[0].endpoint} --container-friendly-name {bucket} " + f"--bearer-rules {s3_bearer_rules} --container-placement-policy 'REP 1' " + f"--container-policy container_policy.json" + ) + output = _run_with_passwd(cmd, wallet.password) + + # output contains some debug info and then several JSON structures, so we find each + # JSON structure by curly brackets (naive approach, but works while JSON is not nested) + # and then we take JSON containing secret_access_key + json_blocks = re.findall(r"\{.*?\}", output, re.DOTALL) + for json_block in json_blocks: + parsed_json_block = json.loads(json_block) + if "secret_access_key" in parsed_json_block: + return ( + parsed_json_block["container_id"], + bucket, + parsed_json_block["access_key_id"], + parsed_json_block["secret_access_key"], + parsed_json_block["owner_private_key"], + ) + raise AssertionError("Can't get s3 creds") + + +@pytest.fixture +def zero_fee(neofs_env: NeoFSEnv): + neofs_env.neofs_adm().morph.set_config( + rpc_endpoint=f"http://{neofs_env.morph_rpc}", + alphabet_wallets=neofs_env.alphabet_wallets_dir, + post_data=f"ContainerFee=0 ContainerAliasFee=0", + ) + + +def test_s3_gw_put_get(neofs_env: NeoFSEnv, s3_creds, wallet: NodeWallet): + ( + cid, + bucket, + access_key_id, + secret_access_key, + _, + ) = s3_creds + + cli = neofs_env.neofs_cli(neofs_env.generate_cli_config(wallet)) + result = cli.container.list(rpc_endpoint=neofs_env.sn_rpc, wallet=wallet.path) + containers_list = result.stdout.split() + assert cid in containers_list, f"Expected cid {cid} in {containers_list}" + + session = boto3.Session() + config = Config( + retries={ + "max_attempts": 1, + "mode": "standard", + } + ) + + s3_client = session.client( + service_name="s3", + aws_access_key_id=access_key_id, + aws_secret_access_key=secret_access_key, + config=config, + endpoint_url=f"https://{neofs_env.s3_gw.address}", + verify=False, + ) + + bucket_name = str(uuid.uuid4()) + params = {"Bucket": bucket_name, "CreateBucketConfiguration": {"LocationConstraint": "rep-1"}} + s3_client.create_bucket(**params) + sleep(5) + + filename = neofs_env._generate_temp_file() + + with open(filename, "w") as file: + file.write("123456789") + + with open(filename, "rb") as file: + file_content = file.read() + + filekey = os.path.basename(filename) + s3_client.put_object(**{"Body": file_content, "Bucket": bucket_name, "Key": filekey}) + s3_client.get_object(**{"Bucket": bucket_name, "Key": filekey}) diff --git a/requirements.txt b/requirements.txt index c07c7af..1166a22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,11 @@ neo-mamba==2.3.0 paramiko==2.10.3 pexpect==4.8.0 requests==2.31.0 +tenacity +jinja2 +pyyaml +pytest +boto3 # Dev dependencies black==22.8.0 diff --git a/src/neofs_testlib/cli/neofs_adm/morph.py b/src/neofs_testlib/cli/neofs_adm/morph.py index f02133d..b477a12 100644 --- a/src/neofs_testlib/cli/neofs_adm/morph.py +++ b/src/neofs_testlib/cli/neofs_adm/morph.py @@ -150,7 +150,6 @@ def force_new_epoch( def generate_alphabet( self, - rpc_endpoint: str, alphabet_wallets: str, size: int = 7, ) -> CommandResult: @@ -159,7 +158,6 @@ def generate_alphabet( Args: alphabet_wallets: Path to alphabet wallets dir. size: Amount of alphabet wallets to generate (default 7). - rpc_endpoint: N3 RPC node endpoint. Returns: Command's result. @@ -175,9 +173,9 @@ def generate_alphabet( def generate_storage_wallet( self, - rpc_endpoint: str, alphabet_wallets: str, storage_wallet: str, + label: str, initial_gas: Optional[str] = None, ) -> CommandResult: """Generate storage node wallet for the morph network. @@ -185,8 +183,8 @@ def generate_storage_wallet( Args: alphabet_wallets: Path to alphabet wallets dir. initial_gas: Initial amount of GAS to transfer. - rpc_endpoint: N3 RPC node endpoint. storage_wallet: Path to new storage node wallet. + label: Wallet label. Returns: Command's result. diff --git a/src/neofs_testlib/cli/neofs_cli/cli.py b/src/neofs_testlib/cli/neofs_cli/cli.py index d8a025c..3eb5201 100644 --- a/src/neofs_testlib/cli/neofs_cli/cli.py +++ b/src/neofs_testlib/cli/neofs_cli/cli.py @@ -4,6 +4,7 @@ from neofs_testlib.cli.neofs_cli.acl import NeofsCliACL from neofs_testlib.cli.neofs_cli.bearer import NeofsCliBearer from neofs_testlib.cli.neofs_cli.container import NeofsCliContainer +from neofs_testlib.cli.neofs_cli.control import NeofsCliControl from neofs_testlib.cli.neofs_cli.netmap import NeofsCliNetmap from neofs_testlib.cli.neofs_cli.object import NeofsCliObject from neofs_testlib.cli.neofs_cli.session import NeofsCliSession @@ -26,6 +27,7 @@ class NeofsCli: storagegroup: Optional[NeofsCliStorageGroup] = None util: Optional[NeofsCliUtil] = None version: Optional[NeofsCliVersion] = None + control: Optional[NeofsCliControl] = None def __init__(self, shell: Shell, neofs_cli_exec_path: str, config_file: Optional[str] = None): self.accounting = NeofsCliAccounting(shell, neofs_cli_exec_path, config=config_file) @@ -39,3 +41,4 @@ def __init__(self, shell: Shell, neofs_cli_exec_path: str, config_file: Optional self.storagegroup = NeofsCliStorageGroup(shell, neofs_cli_exec_path, config=config_file) self.util = NeofsCliUtil(shell, neofs_cli_exec_path, config=config_file) self.version = NeofsCliVersion(shell, neofs_cli_exec_path, config=config_file) + self.control = NeofsCliControl(shell, neofs_cli_exec_path, config=config_file) diff --git a/src/neofs_testlib/cli/neofs_cli/control.py b/src/neofs_testlib/cli/neofs_cli/control.py new file mode 100644 index 0000000..2635eb7 --- /dev/null +++ b/src/neofs_testlib/cli/neofs_cli/control.py @@ -0,0 +1,30 @@ +from typing import Optional + +from neofs_testlib.cli.cli_command import CliCommand +from neofs_testlib.shell import CommandResult + + +class NeofsCliControl(CliCommand): + def healthcheck( + self, + endpoint: str, + post_data="", + ) -> CommandResult: + """ + Get current epoch number. + + Args: + address: Address of wallet account. + generate_key: Generate new private key. + rpc_endpoint: Remote node address (as 'multiaddr' or ':'). + ttl: TTL value in request meta header (default 2). + wallet: Path to the wallet or binary key. + xhdr: Dict with request X-Headers. + + Returns: + Command's result. + """ + return self._execute( + "control healthcheck", + **{param: value for param, value in locals().items() if param not in ["self"]}, + ) diff --git a/src/neofs_testlib/env/__init__.py b/src/neofs_testlib/env/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/neofs_testlib/env/env.py b/src/neofs_testlib/env/env.py new file mode 100644 index 0000000..3b0622b --- /dev/null +++ b/src/neofs_testlib/env/env.py @@ -0,0 +1,486 @@ +import json +import logging +import os +import pickle +import random +import socket +import string +import subprocess +from dataclasses import dataclass +from enum import Enum +from importlib.resources import files +from pathlib import Path +from typing import Optional + +import allure +import jinja2 +from tenacity import retry, stop_after_attempt, wait_fixed + +from neofs_testlib.cli import NeofsAdm, NeofsCli +from neofs_testlib.shell import LocalShell +from neofs_testlib.utils import wallet as wallet_utils + +logger = logging.getLogger("neofs.testlib.env") + + +@dataclass +class NodeWallet: + path: str + address: str + password: str + + +class WalletType(Enum): + STORAGE = 1 + ALPHABET = 2 + + +class NeoFSEnv: + def __init__(self): + self.domain = "localhost" + self.default_password = "password" + self.shell = LocalShell() + # utilities + self.neofs_adm_path = os.getenv("NEOFS_ADM_BIN", "./neofs-adm") + self.neofs_cli_path = os.getenv("NEOFS_CLI_BIN", "./neofs-cli") + self.neo_go_path = os.getenv("NEO_GO_BIN", "./neo-go") + self.neofs_ir_path = os.getenv("NEOFS_IR_BIN", "./neofs-ir") + self.neofs_node_path = os.getenv("NEOFS_NODE_BIN", "./neofs-node") + self.neofs_s3_authmate_path = os.getenv("NEOFS_S3_AUTHMATE_BIN", "./neofs-s3-authmate") + self.neofs_s3_gw_path = os.getenv("NEOFS_S3_GW_BIN", "./neofs-s3-gw") + self.neofs_rest_gw_path = os.getenv("NEOFS_REST_GW_BIN", "./neofs-rest-gw") + self.neofs_http_gw_path = os.getenv("NEOFS_HTTP_GW_BIN", "./neofs-http-gw") + # nodes inside env + self.storage_nodes = [] + self.inner_ring_nodes = [] + self.s3_gw = None + self.rest_gw = None + self.http_gw = None + + @property + def morph_rpc(self): + if len(self.inner_ring_nodes) > 0: + return self.inner_ring_nodes[0].rpc_address + raise ValueError("No Inner Ring nodes configured in this env") + + @property + def sn_rpc(self): + if len(self.storage_nodes) > 0: + return self.storage_nodes[0].endpoint + raise ValueError("No storage nodes configured in this env") + + @property + def alphabet_wallets_dir(self): + if len(self.inner_ring_nodes) > 0: + if self.inner_ring_nodes[0].alphabet_wallet.address == "": + raise ValueError("Alphabet Wallets has not beet initialized") + return os.path.dirname(self.inner_ring_nodes[0].alphabet_wallet.path) + raise ValueError("No Inner Ring nodes configured in this env") + + @property + def network_config(self): + if len(self.inner_ring_nodes) > 0: + return self.inner_ring_nodes[0].network_config + raise ValueError("No Inner Ring nodes configured in this env") + + def neofs_adm(self, network_config: Optional[str] = None) -> NeofsAdm: + if not network_config: + if len(self.inner_ring_nodes) > 0: + network_config = self.network_config + else: + raise ValueError("Network config need to be specified for neofs-adm commands") + return NeofsAdm(self.shell, self.neofs_adm_path, network_config) + + def neofs_cli(self, cli_config_path: str) -> NeofsCli: + return NeofsCli(self.shell, self.neofs_cli_path, cli_config_path) + + def generate_cli_config(self, wallet: NodeWallet): + cli_config_path = NeoFSEnv._generate_temp_file(extension="yml") + NeoFSEnv.generate_config_file( + config_template="cli_cfg.yaml", config_path=cli_config_path, wallet=wallet + ) + return cli_config_path + + @allure.step("Deploy inner ring node") + def deploy_inner_ring_node(self): + new_inner_ring_node = InnerRing(self) + new_inner_ring_node.start() + self.inner_ring_nodes.append(new_inner_ring_node) + + @allure.step("Deploy storage node") + def deploy_storage_node(self): + new_storage_node = StorageNode(self) + new_storage_node.start() + self.storage_nodes.append(new_storage_node) + + @allure.step("Deploy s3 gateway") + def deploy_s3_gw(self): + self.s3_gw = S3_GW(self) + self.s3_gw.start() + + def get_next_sn_number(self): + return len(self.storage_nodes) + 1 + + @allure.step("Generate wallet") + def generate_wallet( + self, + wallet_type: WalletType, + prepared_wallet: NodeWallet, + network_config: Optional[str] = None, + label: Optional[str] = None, + ): + neofs_adm = self.neofs_adm(network_config) + + if wallet_type == WalletType.STORAGE: + neofs_adm.morph.generate_storage_wallet( + alphabet_wallets=self.alphabet_wallets_dir, + storage_wallet=prepared_wallet.path, + initial_gas="10", + label=label, + ) + elif wallet_type == WalletType.ALPHABET: + neofs_adm.morph.generate_alphabet(alphabet_wallets=prepared_wallet.path, size=1) + prepared_wallet.path += "/az.json" + else: + raise ValueError(f"Unsupported wallet type: {wallet_type}") + + # neo-go requires some attributes to be set + with open(prepared_wallet.path, "r") as wallet_file: + wallet_json = json.load(wallet_file) + + wallet_json["name"] = None + for acc in wallet_json["accounts"]: + acc["extra"] = None + + with open(prepared_wallet.path, "w") as wallet_file: + json.dump(wallet_json, wallet_file) + ### + + prepared_wallet.address = wallet_utils.get_last_address_from_wallet( + prepared_wallet.path, prepared_wallet.password + ) + + @allure.step("Kill current neofs env") + def kill(self): + self.s3_gw.process.kill() + for sn in self.storage_nodes: + sn.process.kill() + for ir in self.inner_ring_nodes: + ir.process.kill() + + def persist(self) -> str: + persisted_path = NeoFSEnv._generate_temp_file() + with open(persisted_path, "wb") as fp: + pickle.dump(self, fp) + logger.info(f"Persist env at: {persisted_path}") + return persisted_path + + @classmethod + def load(cls, persisted_path: str) -> "NeoFSEnv": + with open(persisted_path, "rb") as fp: + return pickle.load(fp) + + @classmethod + def simple(cls) -> "NeoFSEnv": + neofs_env = NeoFSEnv() + neofs_env.deploy_inner_ring_node() + neofs_env.deploy_storage_node() + neofs_env.deploy_s3_gw() + return neofs_env + + @staticmethod + def generate_config_file(config_template: str, config_path: str, **kwargs): + jinja_env = jinja2.Environment() + config_template = files("neofs_testlib.env.templates").joinpath(config_template).read_text() + jinja_template = jinja_env.from_string(config_template) + rendered_config = jinja_template.render(**kwargs) + with open(config_path, mode="w") as fp: + fp.write(rendered_config) + + @staticmethod + def get_available_port() -> str: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("", 0)) + addr = s.getsockname() + s.close() + return addr[1] + + @staticmethod + def _generate_temp_file(extension: str = "") -> str: + file_path = f"/tmp/tmp_file{''.join(random.choices(string.ascii_lowercase, k=10))}" + if extension: + file_path += f".{extension}" + Path(file_path).touch() + return file_path + + @staticmethod + def _generate_temp_dir() -> str: + dir_path = f"/tmp/tmp_dir{''.join(random.choices(string.ascii_lowercase, k=10))}" + Path(dir_path).mkdir() + return dir_path + + +class InnerRing: + def __init__(self, neofs_env: NeoFSEnv): + self.neofs_env = neofs_env + self.network_config = NeoFSEnv._generate_temp_file(extension="yml") + self.cli_config = NeoFSEnv._generate_temp_file(extension="yml") + self.alphabet_wallet = NodeWallet( + path=NeoFSEnv._generate_temp_dir(), address="", password=self.neofs_env.default_password + ) + self.ir_node_config_path = NeoFSEnv._generate_temp_file(extension="yml") + self.ir_storage_path = NeoFSEnv._generate_temp_file(extension="db") + self.seed_nodes_address = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.rpc_address = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.p2p_address = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.grpc_address = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.ir_state_file = NeoFSEnv._generate_temp_file() + self.stdout = NeoFSEnv._generate_temp_file() + self.stderr = NeoFSEnv._generate_temp_file() + self.process = None + + def __str__(self): + return f""" + Inner Ring: + - Alphabet wallet: {self.alphabet_wallet} + - IR Config path: {self.ir_node_config_path} + - Seed nodes address: {self.seed_nodes_address} + - RPC address: {self.rpc_address} + - P2P address: {self.p2p_address} + - GRPC address: {self.grpc_address} + - IR State file path: {self.ir_state_file} + - STDOUT: {self.stdout} + - STDERR: {self.stderr} + """ + + def __getstate__(self): + attributes = self.__dict__.copy() + del attributes["process"] + return attributes + + def start(self): + if self.process is not None: + raise RuntimeError(f"This inner ring node instance has already been started") + logger.info(f"Generating network config at: {self.network_config}") + NeoFSEnv.generate_config_file( + config_template="network.yaml", + config_path=self.network_config, + morph_endpoint=self.rpc_address, + alphabet_wallets_path=self.alphabet_wallet.path, + default_password=self.neofs_env.default_password, + ) + logger.info(f"Generating alphabet wallets") + self.neofs_env.generate_wallet( + WalletType.ALPHABET, self.alphabet_wallet, network_config=self.network_config + ) + logger.info(f"Generating IR config at: {self.ir_node_config_path}") + NeoFSEnv.generate_config_file( + config_template="ir.yaml", + config_path=self.ir_node_config_path, + wallet=self.alphabet_wallet, + public_key=wallet_utils.get_last_public_key_from_wallet( + self.alphabet_wallet.path, self.alphabet_wallet.password + ), + ir_storage_path=self.ir_storage_path, + seed_nodes_address=self.seed_nodes_address, + rpc_address=self.rpc_address, + p2p_address=self.p2p_address, + grpc_address=self.grpc_address, + ir_state_file=self.ir_state_file, + ) + logger.info(f"Generating CLI config at: {self.cli_config}") + NeoFSEnv.generate_config_file( + config_template="cli_cfg.yaml", config_path=self.cli_config, wallet=self.alphabet_wallet + ) + logger.info(f"Launching Inner Ring Node:{self}") + self._launch_process() + logger.info(f"Wait until IR is READY") + self._wait_until_ready() + + self.neofs_env.neofs_adm + + def _launch_process(self): + stdout_fp = open(self.stdout, "w") + stderr_fp = open(self.stderr, "w") + self.process = subprocess.Popen( + [self.neofs_env.neofs_ir_path, "--config", self.ir_node_config_path], + stdout=stdout_fp, + stderr=stderr_fp, + ) + + @retry(wait=wait_fixed(10), stop=stop_after_attempt(10), reraise=True) + def _wait_until_ready(self): + neofs_cli = self.neofs_env.neofs_cli(self.cli_config) + result = neofs_cli.control.healthcheck(endpoint=self.grpc_address, post_data="--ir") + assert "READY" in result.stdout + + +class Shard: + def __init__(self): + self.metabase_path = NeoFSEnv._generate_temp_file() + self.blobovnicza_path = NeoFSEnv._generate_temp_file() + self.fstree_path = NeoFSEnv._generate_temp_dir() + self.pilorama_path = NeoFSEnv._generate_temp_file() + self.wc_path = NeoFSEnv._generate_temp_file() + + +class StorageNode: + def __init__(self, neofs_env: NeoFSEnv, attrs: Optional[list] = None): + self.neofs_env = neofs_env + self.wallet = NodeWallet( + path=NeoFSEnv._generate_temp_file(), + address="", + password=self.neofs_env.default_password, + ) + self.cli_config = NeoFSEnv._generate_temp_file(extension="yml") + self.storage_node_config_path = NeoFSEnv._generate_temp_file(extension="yml") + self.state_file = NeoFSEnv._generate_temp_file() + self.shards = [Shard(), Shard()] + self.endpoint = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.control_grpc_endpoint = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.stdout = NeoFSEnv._generate_temp_file() + self.stderr = NeoFSEnv._generate_temp_file() + self.process = None + if attrs: + self.attrs = {f"NEOFS_NODE_ATTRIBUTE_{index}": attr for index, attr in enumerate(attrs)} + else: + self.attrs = {} + + def __str__(self): + return f""" + Storage node: + - Endpoint: {self.endpoint} + - Control gRPC endpoint: {self.control_grpc_endpoint} + - STDOUT: {self.stdout} + - STDERR: {self.stderr} + """ + + def __getstate__(self): + attributes = self.__dict__.copy() + del attributes["process"] + return attributes + + def start(self): + logger.info(f"Generating wallet for storage node") + self.neofs_env.generate_wallet( + WalletType.STORAGE, self.wallet, label=f"sn{self.neofs_env.get_next_sn_number()}" + ) + logger.info(f"Generating config for storage node at {self.storage_node_config_path}") + NeoFSEnv.generate_config_file( + config_template="sn.yaml", + config_path=self.storage_node_config_path, + morph_endpoint=self.neofs_env.morph_rpc, + shards=self.shards, + wallet=self.wallet, + state_file=self.state_file, + ) + logger.info(f"Generating cli config for storage node at: {self.cli_config}") + NeoFSEnv.generate_config_file( + config_template="cli_cfg.yaml", config_path=self.cli_config, wallet=self.wallet + ) + logger.info(f"Launching Storage Node:{self}") + self._launch_process() + logger.info(f"Wait until storage node is READY") + self._wait_until_ready() + + def _launch_process(self): + stdout_fp = open(self.stdout, "w") + stderr_fp = open(self.stderr, "w") + env_dict = { + "NEOFS_NODE_WALLET_PATH": self.wallet.path, + "NEOFS_NODE_WALLET_PASSWORD": self.wallet.password, + "NEOFS_NODE_ADDRESSES": self.endpoint, + "NEOFS_GRPC_0_ENDPOINT": self.endpoint, + "NEOFS_CONTROL_GRPC_ENDPOINT": self.control_grpc_endpoint, + } + env_dict.update(self.attrs) + self.process = subprocess.Popen( + [self.neofs_env.neofs_node_path, "--config", self.storage_node_config_path], + stdout=stdout_fp, + stderr=stderr_fp, + env=env_dict, + ) + + @retry(wait=wait_fixed(15), stop=stop_after_attempt(30), reraise=True) + def _wait_until_ready(self): + neofs_cli = self.neofs_env.neofs_cli(self.cli_config) + result = neofs_cli.control.healthcheck(endpoint=self.control_grpc_endpoint) + assert "Health status: READY" in result.stdout, "Health is not ready" + assert "Network status: ONLINE" in result.stdout, "Network is not online" + + +class S3_GW: + def __init__(self, neofs_env: NeoFSEnv): + self.neofs_env = neofs_env + self.config_path = NeoFSEnv._generate_temp_file(extension="yml") + self.wallet = NodeWallet( + path=NeoFSEnv._generate_temp_file(), + address="", + password=self.neofs_env.default_password, + ) + self.address = f"{self.neofs_env.domain}:{NeoFSEnv.get_available_port()}" + self.tls_cert_path = NeoFSEnv._generate_temp_file() + self.tls_key_path = NeoFSEnv._generate_temp_file() + self.stdout = NeoFSEnv._generate_temp_file() + self.stderr = NeoFSEnv._generate_temp_file() + self.process = None + + def __str__(self): + return f""" + S3 Gateway: + - Address: {self.address} + - S3 GW Config path: {self.config_path} + - STDOUT: {self.stdout} + - STDERR: {self.stderr} + """ + + def __getstate__(self): + attributes = self.__dict__.copy() + del attributes["process"] + return attributes + + def start(self): + if self.process is not None: + raise RuntimeError(f"This s3 gw instance has already been started:\n{self}") + self.neofs_env.generate_wallet(WalletType.STORAGE, self.wallet, label=f"s3") + logger.info(f"Generating config for s3 gw at {self.config_path}") + self._generate_config() + logger.info(f"Launching S3 GW: {self}") + self._launch_process() + + def _generate_config(self): + tls_crt_template = files("neofs_testlib.env.templates").joinpath("tls.crt").read_text() + with open(self.tls_cert_path, mode="w") as fp: + fp.write(tls_crt_template) + tls_key_template = files("neofs_testlib.env.templates").joinpath("tls.key").read_text() + with open(self.tls_key_path, mode="w") as fp: + fp.write(tls_key_template) + + NeoFSEnv.generate_config_file( + config_template="s3.yaml", + config_path=self.config_path, + address=self.address, + cert_file_path=self.tls_cert_path, + key_file_path=self.tls_key_path, + wallet=self.wallet, + morph_endpoint=self.neofs_env.morph_rpc, + ) + + def _launch_process(self): + stdout_fp = open(self.stdout, "w") + stderr_fp = open(self.stderr, "w") + s3_gw_env = { + "S3_GW_LISTEN_DOMAINS": self.neofs_env.domain, + "S3_GW_TREE_SERVICE": self.neofs_env.storage_nodes[0].endpoint, + } + + for index, sn in enumerate(self.neofs_env.storage_nodes): + s3_gw_env[f"S3_GW_PEERS_{index}_ADDRESS"] = sn.endpoint + s3_gw_env[f"S3_GW_PEERS_{index}_WEIGHT"] = "0.2" + + self.process = subprocess.Popen( + [self.neofs_env.neofs_s3_gw_path, "--config", self.config_path], + stdout=stdout_fp, + stderr=stderr_fp, + env=s3_gw_env, + ) diff --git a/src/neofs_testlib/env/templates/cli_cfg.yaml b/src/neofs_testlib/env/templates/cli_cfg.yaml new file mode 100644 index 0000000..4f2417e --- /dev/null +++ b/src/neofs_testlib/env/templates/cli_cfg.yaml @@ -0,0 +1,2 @@ +wallet: {{ wallet.path }} +password: {{ wallet.password }} diff --git a/src/neofs_testlib/env/templates/ir.yaml b/src/neofs_testlib/env/templates/ir.yaml new file mode 100644 index 0000000..8192464 --- /dev/null +++ b/src/neofs_testlib/env/templates/ir.yaml @@ -0,0 +1,140 @@ +--- + +logger: + level: debug # Logger level: one of "debug", "info" (default), "warn", "error", "dpanic", "panic", "fatal" + +wallet: + path: {{ wallet.path }} # Path to NEP-6 NEO wallet file + address: {{ wallet.address }} # Account address in the wallet; ignore to use default address + password: {{ wallet.password }} # Account password in the wallet + +without_mainnet: true # Run application in single chain environment without mainchain + +morph: + dial_timeout: 5s # Timeout for RPC client connection to sidechain + reconnections_number: 5 # number of reconnection attempts + reconnections_delay: 5s # time delay b/w reconnection attempts + validators: # List of hex-encoded 33-byte public keys of sidechain validators to vote for at application startup; can be omitted if equals `consensus.committee` + - {{ public_key }} + consensus: # Local consensus launch mode activated only when 'endpoint.client' is unset. + magic: 15405 # Network magic. Must be unsigned integer in range [1:4294967295] + committee: # Initial committee + - {{ public_key }} # Hex-encoded public key + storage: # Blockchain storage + type: boltdb # One of following storage types: + # boltdb (local BoltDB) + # leveldb (local LevelDB) + # inmemory (volatile storage, mostly suitable for testing) + # + # If 'path' exists, it should correspond to the selected variant + path: {{ ir_storage_path }} # File system path to the storage. Ignored for 'inmemory' storage. + time_per_block: 1s # Optional time period (approximate) between two adjacent blocks. Defaults to 15s. + # Must not be negative + seed_nodes: + - {{ seed_nodes_address }} + max_traceable_blocks: 2102400 # Optional length of the chain accessible to smart contracts. Defaults to 2102400. + # Must not be greater than 4294967295 + rpc: # Optional RPC settings + listen: # Optional list of network addresses to listen Neo RPC on. By default, protocol is not served + # TCP addresses in 'host:port' format + - {{ rpc_address }} + p2p: # Optional P2P settings + dial_timeout: 5s # Optional maximum duration a single peer dial may take. Defaults to 5s. Must not be negative + proto_tick_interval: 2s # Optional time period between protocol ticks with each connected peer. Defaults to 2s. + # Must not be negative + listen: # Optional list of network addresses to listen Neo P2P on. By default, protocol is not served + # TCP addresses in 'host:port' format + - {{ p2p_address }} + peers: # Optional peer settings + min: 0 # Optional minimum number of peers a node needs for normal operation. Defaults to consensus minimum + # of 'committee' size (ceil of 2/3N-1). Must not be greater than 2147483647. Note that consensus service + # won't start until at least 'min' number of peers are connected + max: 10 # Optional limits of maximum number of peers dealing with the node. Defaults to 100. Must not be + # greater than 2147483647 + attempts: 5 # How many peers node should try to dial after falling under 'min' count. Defaults to 'min'+10. + # Must not be greater than 2147483647 + ping: # Optional settings of pinging mechanism + interval: 30s # Optional time period between pings. Defaults to 30s. Must not be negative + timeout: 90s # Optional time period to wait for pong. Defaults to 1m. Must not be negative + +fschain_autodeploy: true + +nns: + system_email: usr@domain.io + +mainnet: + dial_timeout: 5s # Timeout for RPC client connection to mainchain; ignore if mainchain is disabled + reconnections_number: 5 # number of reconnection attempts + reconnections_delay: 5s # time delay b/w reconnection attempts + +control: + authorized_keys: # List of hex-encoded 33-byte public keys that have rights to use the control service + - {{ public_key }} + grpc: + endpoint: {{ grpc_address }} # Endpoint that is listened by the control service; disabled by default + +governance: + disable: false # Disable synchronization of sidechain committee and mainchain role management contract; ignore if mainchain is disabled + +node: + persistent_state: + path: {{ ir_state_file }} # Path to application state file + +fee: + main_chain: 0 # Fixed8 value of extra GAS fee for mainchain contract invocation; ignore if notary is enabled in mainchain + +timers: + emit: 240 # Number of sidechain blocks between GAS emission cycles; disabled by default + stop_estimation: + mul: 1 # Multiplier in x/y relation of when to stop basic income estimation within the epoch + div: 4 # Divider in x/y relation of when to stop basic income estimation within the epoch + collect_basic_income: + mul: 1 # Multiplier in x/y relation of when to start basic income asset collection within the epoch + div: 2 # Divider in x/y relation of when to start basic income asset collecting within the epoch + distribute_basic_income: + mul: 3 # Multiplier in x/y relation of when to start basic income asset distribution within the epoch + div: 4 # Divider in x/y relation of when to start basic income asset distribution within the epoch + +emit: + storage: + amount: 800000000 # Fixed8 value of sidechain GAS emitted to all storage nodes once per GAS emission cycle; disabled by default + mint: + value: 20000000 # Fixed8 value of sidechain GAS transferred to account that received a deposit from mainchain + cache_size: 1000 # LRU cache size of all deposit receivers to avoid double GAS emission + threshold: 1 # Lifetime of records in LRU cache of all deposit receivers in NeoFS epochs + gas: + balance_threshold: 100000000000 # Fixed8 value of inner ring wallet balance threshold when GAS emission for deposit receivers is disabled; disabled by default + +workers: + alphabet: 10 # Number of workers to process events from alphabet contract in parallel + balance: 10 # Number of workers to process events from balance contract in parallel + container: 10 # Number of workers to process events from container contract in parallel + neofs: 10 # Number of workers to process events from neofs contracts in parallel + netmap: 10 # Number of workers to process events from netmap contract in parallel + reputation: 10 # Number of workers to process events from reputation contract in parallel + +audit: + timeout: + get: 5s # Timeout for object.Get operation during data audit + head: 5s # Timeout for object.Head operation during data audit + rangehash: 5s # Timeout for object.RangeHash operation during data audit + search: 10s # Timeout for object.Search operation during data audit + task: + exec_pool_size: 10 # Number of workers to process audit routine in parallel + queue_capacity: 100 # Maximum amount of simultaneous audit jobs + pdp: + pairs_pool_size: 10 # Number of workers to process PDP part of data audit in parallel + max_sleep_interval: 5s # Maximum timeout between object.RangeHash requests to the storage node + por: + pool_size: 10 # Number of workers to process PoR part of data audit in parallel + +indexer: + cache_timeout: 15s # Duration between internal state update about current list of inner ring nodes + +netmap_cleaner: + enabled: true # Enable voting for removing stale storage nodes from network map + threshold: 3 # Number of NeoFS epoch without bootstrap request from storage node before it considered stale + +settlement: + basic_income_rate: 0 # Optional: override basic income rate value from network config; applied only in debug mode + audit_fee: 0 # Optional: override audit fee value from network config; applied only in debug mode diff --git a/src/neofs_testlib/env/templates/network.yaml b/src/neofs_testlib/env/templates/network.yaml new file mode 100644 index 0000000..4f65000 --- /dev/null +++ b/src/neofs_testlib/env/templates/network.yaml @@ -0,0 +1,28 @@ +rpc-endpoint: http://{{ morph_endpoint }} +alphabet-wallets: {{ alphabet_wallets_path }} +network: + max_object_size: 67108864 + epoch_duration: 240 + basic_income_rate: 100000000 + homomorphic_hash_disabled: false + fee: + audit: 0 + candidate: 0 + container: 0 + container_alias: 0 + withdraw: 0 +# if credentials section is omitted, then neofs-adm will require manual password input +credentials: + az: {{ default_password }} + buky: {{ default_password }} + vedi: {{ default_password }} + glagoli: {{ default_password }} + dobro: {{ default_password }} + yest: {{ default_password }} + zhivete: {{ default_password }} +storage: + sn1: {{ default_password }} + sn2: {{ default_password }} + sn3: {{ default_password }} + sn4: {{ default_password }} + s3: {{ default_password }} diff --git a/src/neofs_testlib/env/templates/s3.yaml b/src/neofs_testlib/env/templates/s3.yaml new file mode 100644 index 0000000..7113fdf --- /dev/null +++ b/src/neofs_testlib/env/templates/s3.yaml @@ -0,0 +1,32 @@ +logger: + level: debug + +# Interval to check node health +rebalance_interval: 30s + +# Timeout to check node health during rebalance +healthcheck_timeout: 15s + +# Timeout to connect to a node +connect_timeout: 10s + +# Limits for processing of clients' requests +max_clients_count: 100 + +# Deadline after which the gate sends error `RequestTimeout` to a client +max_clients_deadline: 30s + +server: + - address: {{ address }} + tls: + enabled: true + cert_file: {{ cert_file_path }} + key_file: {{ key_file_path }} + +# Wallet configuration +wallet: + path: {{ wallet.path }} # Path to wallet + passphrase: {{ wallet.password }} # Passphrase to decrypt wallet + +# RPC endpoint +rpc_endpoint: http://{{ morph_endpoint }} \ No newline at end of file diff --git a/src/neofs_testlib/env/templates/sn.yaml b/src/neofs_testlib/env/templates/sn.yaml new file mode 100644 index 0000000..e09c0e6 --- /dev/null +++ b/src/neofs_testlib/env/templates/sn.yaml @@ -0,0 +1,79 @@ +# Logger section +logger: + level: debug # Minimum enabled logging level + + +# Profiler section +pprof: + enabled: false + address: :6060 # Server address + shutdown_timeout: 15s # Timeout for profiling HTTP server graceful shutdown + +# Application metrics section +prometheus: + enabled: false + address: :9090 # Server address + shutdown_timeout: 15s # Timeout for metrics HTTP server graceful shutdown + +# Morph section +morph: + dial_timeout: 30s # Timeout for side chain NEO RPC client connection + endpoints: # Side chain NEO RPC endpoints + - ws://{{ morph_endpoint }}/ws + + +# Tree section +tree: + enabled: true + +node: + wallet: + path: "{{ wallet.path }}" + address: "{{ wallet.address }}" + password: "{{ wallet.password }}" + + persistent_state: + path: {{ state_file }} # Path to application state file + +# Storage engine configuration +storage: + shard: + 0: + writecache: + enabled: false + path: {{ shards[0].wc_path }} # Write-cache root directory + + metabase: + path: {{ shards[0].metabase_path }} # Path to the metabase + + blobstor: + - type: peapod + path: {{ shards[0].blobovnicza_path }} # Blobovnicza root directory + depth: 2 + width: 4 + - type: fstree + path: {{ shards[0].fstree_path }} # FSTree root directory + depth: 2 + + pilorama: + path: {{ shards[0].pilorama_path }} # Path to the pilorama database + + 1: + writecache: + enabled: false + path: {{ shards[1].wc_path }} # Write-cache root directory + + metabase: + path: {{ shards[1].metabase_path }} # Path to the metabase + + blobstor: + - type: peapod + path: {{ shards[1].blobovnicza_path }} # Blobovnicza root directory + depth: 2 + width: 4 + - type: fstree + path: {{ shards[1].fstree_path }} # FSTree root directory + depth: 2 + + pilorama: + path: {{ shards[1].pilorama_path }} # Path to the pilorama database diff --git a/src/neofs_testlib/env/templates/tls.crt b/src/neofs_testlib/env/templates/tls.crt new file mode 100644 index 0000000..5139070 --- /dev/null +++ b/src/neofs_testlib/env/templates/tls.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDpDCCAowCCQDXZEH0aQRqFzANBgkqhkiG9w0BAQsFADCBkzELMAkGA1UEBhMC +UlUxFjAUBgNVBAgMDVN0LlBldGVyc2J1cmcxGTAXBgNVBAcMEFNhaW50IFBldGVy +c2J1cmcxDjAMBgNVBAoMBU5TUENDMREwDwYDVQQLDAhOZW8gU1BDQzERMA8GA1UE +AwwIbnNwY2MucnUxGzAZBgkqhkiG9w0BCQEWDG9wc0Buc3BjYy5ydTAeFw0yMDA3 +MTMxNTQyMzZaFw0zMDA3MTExNTQyMzZaMIGTMQswCQYDVQQGEwJSVTEWMBQGA1UE +CAwNU3QuUGV0ZXJzYnVyZzEZMBcGA1UEBwwQU2FpbnQgUGV0ZXJzYnVyZzEOMAwG +A1UECgwFTlNQQ0MxETAPBgNVBAsMCE5lbyBTUENDMREwDwYDVQQDDAhuc3BjYy5y +dTEbMBkGCSqGSIb3DQEJARYMb3BzQG5zcGNjLnJ1MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAwqo2l4fS0U6wZCLh7VjQn1LXN8pZlVaA62C+g1SwoWV2 +Q5qM8FDihWj3UBO3F+6vUVJl8N5S0JroxxU6L48Wmshei145SLSl/F28tsk7Bbuz +NOchonlelW77Xr6l7cDJBWUWGkDoq6a/S6w6jjCGhZq+X0gyS5nZ4HTouVNv2oFK +eeJGtueLsS4zoVovrHdLSYdZH9/yC+E1WVCzQB+vdUF/vJLTuULgqncLV0sELmRl ++xsnnAV/REJswtCmKgrmAv9pMebBw5EEgROTGazdToWdD5X44xTlHjUb1bMuF9tL +YtUMdLxXceXZFhYhiTBO7ev9awKaNYslbxh+goJo1wIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQBDEGhAyOtfsNwbZ0oZIw06e0JXCmri+8jsn5Ly/yHU0+ecHgMA5AAQ +AG2QRpZZtZCtD/Cj4i6nSTWbRhS0FgqY998p5Lnh/AXTZHBx0t3LKJupN59CIjCK +1eMEfQChoAZg66oO/obAFkq72gj8gpagMY9vFNVcszmse3FWrvlKmO1TwTEh+CzM +7wbmiL/ujm0lIf44pp0U4qYFcSimSDqbwOfeDPif9lMinzylDxMfaAKBHBHPj5Vt +fX8dgf6MIqyz51u/2G0gHfXMDxXec8huYKt2EtPyavh6kFxxGvcA15m6seJTcu+h +6WzeQFa2NBg7Z3ai4DiPXirNtcHWeqxK +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/neofs_testlib/env/templates/tls.key b/src/neofs_testlib/env/templates/tls.key new file mode 100644 index 0000000..b194ccf --- /dev/null +++ b/src/neofs_testlib/env/templates/tls.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwqo2l4fS0U6wZCLh7VjQn1LXN8pZlVaA62C+g1SwoWV2Q5qM +8FDihWj3UBO3F+6vUVJl8N5S0JroxxU6L48Wmshei145SLSl/F28tsk7BbuzNOch +onlelW77Xr6l7cDJBWUWGkDoq6a/S6w6jjCGhZq+X0gyS5nZ4HTouVNv2oFKeeJG +tueLsS4zoVovrHdLSYdZH9/yC+E1WVCzQB+vdUF/vJLTuULgqncLV0sELmRl+xsn +nAV/REJswtCmKgrmAv9pMebBw5EEgROTGazdToWdD5X44xTlHjUb1bMuF9tLYtUM +dLxXceXZFhYhiTBO7ev9awKaNYslbxh+goJo1wIDAQABAoIBAEIp3mJEjPgNOdDf +NlEYpdfxLStOQIKMo0bdXAOBToOc28SAjDTGGSflFGIIQWwF+Vq3meRzfExgyouY +AG3XwYQcZF4USX4XwG71YUXzQXdiY7ewc3Mos2gxD4kVXYpgwzJtOET2GN72zwAm +asSXY7GXdesmu8mMYkxzEAKlhFgMj+bGE/4QQUBKG9ylGIdo07zmU6rAsVhnwQTb +LE3cf+AxCeTVA7OsJCUUR4S9qsgXUN1WeaV8LNg0lYx8UTu1xlbrpSjx7B4eYy6J +FGJWuT9b3X+cBLcGk3BzheUAfqBG2UFDxUCt0grqmmTBkB850MtCDhffhPjxxrD7 +KrwAcpECgYEA6HApn2VtWI/tDYCbNix6yxeqq73fO3ng6yFry1u7EYvl8hJXBgR4 +b6kAVc3y/9pZO/5D23dHl1PQtnU5401/j6dQrb8A2TMqZ1vA8XIdIMjOiVjZtYMF +nXzmf78PEbw9jWlDVARJdAwkJeuDI4/HVvgiDAh3zxx5F8uDVP16/r8CgYEA1mXS +9owfLIPtPSxyMJoGU0jP7OP+HVwlKkXpvg7uBtINKSDW4UU4rnpIGW5MohR3ACWO +ReFliOnGA5FXBp9GzkbJ+wIYovPIsGuBdxSsBlPY1S0yPlo30hr7E6cK3B3EuxDg +SkbJcWp2EwXYEIyEcopbVUTTlBO3wmBFgm/Ps+kCgYA/+Kar9OlMR4hRgAS3uzQs +cx4I2F/46YlKjU8yj9ODd8JYhk2nHVHcQWITO3RWkEyg41DftQtiDbJSlR7SfUDP +U5gzyW69WISiH7GRgfucS0f0qxx4BVBlULvLitTl5631HnRmSivBIZpNSW01O1v8 +hpwwPaBjww1czCkgGgdg1wKBgQCkaSdTW/bX+z9lpvzWWnc5TN/uSJRpTW1Osphh +4C8WWeQvwvglfiDOZAWAQv5PWKQ9H4+v9P4Y9TSdLcpv0JrKuqxPabcc1xfyei6o +89hLbecc6vDZsfOWkowx8Oo6DDX+Qh3Nt+TorXxocBXV8vvqnkEV7ZbWuhwz2gHT +2gyMaQKBgEE7rNzm8Q03IqQ08eYaRw8gWz8EpLeVebrGqtoH9AR5cd4OeTeZAEqc +iPehXctke2pUgS47XgG98G7Yg3E9UuOYM+H2nzQCoT7jrM0dZrVGZ0ty7z1a8QGe +UrjaAC/cyIGdszhf0Rf3qA7450nit9Txh+ilLiumgnUezl+eJXyI +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/src/neofs_testlib/utils/wallet.py b/src/neofs_testlib/utils/wallet.py index b3bdad6..7d3488c 100644 --- a/src/neofs_testlib/utils/wallet.py +++ b/src/neofs_testlib/utils/wallet.py @@ -7,7 +7,7 @@ logger = logging.getLogger("neofs.testlib.utils") -def init_wallet(wallet_path: str, wallet_password: str): +def init_wallet(wallet_path: str, wallet_password: str) -> str: """ Create new wallet and new account. Args: @@ -20,6 +20,7 @@ def init_wallet(wallet_path: str, wallet_password: str): with open(wallet_path, "w") as out: json.dump(wallet.to_json(), out) logger.info(f"Init new wallet: {wallet_path}, address: {account.address}") + return account.address def get_last_address_from_wallet( @@ -45,3 +46,28 @@ def get_last_address_from_wallet( address = wallet.accounts[-1].address logger.info(f"got address: {address}") return address + + +def get_last_public_key_from_wallet( + wallet_path: str, wallet_password: str | None = None, wallet_passwords: list[str] | None = None +): + """ + Extracting the last address from the given wallet. + Args: + wallet_path: The path to the wallet to extract address from. + wallet_password: The password for the given wallet. + wallet_passwords: The password list for the given accounts in the wallet + Returns: + The address for the wallet. + """ + if wallet_password is None and wallet_passwords is None: + raise ValueError("Either wallet_password or wallet_passwords should be specified") + + with open(wallet_path) as wallet_file: + wallet_json = json.load(wallet_file) + if wallet_password is not None: + wallet_passwords = [wallet_password] * len(wallet_json["accounts"]) + wallet = neo3_wallet.Wallet.from_json(wallet_json, passwords=wallet_passwords) + public_key = wallet.accounts[-1].public_key + logger.info(f"got public_key: {public_key}") + return public_key