diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a5c8ebdf..5df55bfe 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,7 +11,10 @@ - no redundant code - move repeated logic into helper functions - use type hints to specify the expected types of function arguments and return values -- check `ruff.toml` for formatting rules -- always lint changes using `ruff check` +- check `pyproject.toml` for formatting rules +- always lint changes using `uv run ruff check` - tests should be placed in `tests/` directory, follow the existing structure and code style -- to run a test always use `bash scripts/run_tests.sh tests/path_to_test.py -k [TEST_NAME]` command \ No newline at end of file +- always use `uv` to run all commands in the repo (e.g., `uv run ruff`, `uv run pytest`, etc.) +- for running tests, export environment variables in the terminal before running the tests: `. ./scripts/export_env.sh` + +- additional external context is located in context directory \ No newline at end of file diff --git a/.gitignore b/.gitignore index 87a0d8f8..65232943 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,4 @@ tests/.skale/node_data/node_options.json tests/.skale/config/nginx.conf.j2 .zed +uv.lock \ No newline at end of file diff --git a/node_cli/cli/fair_boot.py b/node_cli/cli/fair_boot.py index f5dce9b1..d5ff6039 100644 --- a/node_cli/cli/fair_boot.py +++ b/node_cli/cli/fair_boot.py @@ -88,6 +88,6 @@ def signature_boot(validator_id): @streamed_cmd def update_node(env_file, pull_config_for_schain): update( - env_filepath=env_file, + config_file=env_file, pull_config_for_schain=pull_config_for_schain, ) diff --git a/node_cli/cli/fair_node.py b/node_cli/cli/fair_node.py index 56ab9d75..efdf3149 100644 --- a/node_cli/cli/fair_node.py +++ b/node_cli/cli/fair_node.py @@ -57,10 +57,10 @@ def fair_node_info(format): @node.command('init', help='Initialize regular Fair node') -@click.argument('env_filepath') +@click.argument('config_file') @streamed_cmd -def init_node(env_filepath: str): - init_fair(node_mode=NodeMode.ACTIVE, env_filepath=env_filepath) +def init_node(config_file: str): + init_fair(node_mode=NodeMode.ACTIVE, config_file=config_file) @node.command('register', help=TEXTS['fair']['node']['register']['help']) @@ -70,7 +70,7 @@ def register(ip: str) -> None: @node.command('update', help='Update Fair node') -@click.argument('env_filepath') +@click.argument('config_file') @click.option( '--yes', is_flag=True, @@ -88,10 +88,10 @@ def register(ip: str) -> None: is_flag=True, ) @streamed_cmd -def update_node(env_filepath: str, pull_config_for_schain, force_skaled_start: bool): +def update_node(config_file: str, pull_config_for_schain, force_skaled_start: bool): update_fair( node_mode=NodeMode.ACTIVE, - env_filepath=env_filepath, + config_file=config_file, pull_config_for_schain=pull_config_for_schain, force_skaled_start=force_skaled_start, ) @@ -106,7 +106,7 @@ def backup_node(backup_folder_path): @node.command('restore', help='Restore Fair node from a backup file.') @click.argument('backup_path') -@click.argument('env_file') +@click.argument('config_file') @click.option( '--config-only', help='Only restore configuration files in .skale and artifacts', @@ -114,12 +114,12 @@ def backup_node(backup_folder_path): hidden=True, ) @streamed_cmd -def restore_node(backup_path, env_file, config_only): - restore_fair(backup_path, env_file, config_only) +def restore_node(backup_path, config_file, config_only): + restore_fair(backup_path, config_file, config_only) @node.command('migrate', help='Switch from boot to regular Fair node.') -@click.argument('env_filepath') +@click.argument('config_file') @click.option( '--yes', is_flag=True, @@ -128,8 +128,8 @@ def restore_node(backup_path, env_file, config_only): prompt='Are you sure you want to migrate to regular Fair node? The action cannot be undone', ) @streamed_cmd -def migrate_node(env_filepath: str) -> None: - migrate_from_boot(env_filepath=env_filepath) +def migrate_node(config_file: str) -> None: + migrate_from_boot(config_file=config_file) @node.command('repair', help='Toggle fair chain repair mode') @@ -221,7 +221,7 @@ def turn_off_node() -> None: expose_value=False, prompt='Are you sure you want to turn on the node?', ) -@click.argument('env_filepath') +@click.argument('config_file') @streamed_cmd -def turn_on_node(env_filepath: str) -> None: - turn_on_fair(env_file=env_filepath, node_type=TYPE) +def turn_on_node(config_file: str) -> None: + turn_on_fair(env_file=config_file, node_type=TYPE) diff --git a/node_cli/cli/node.py b/node_cli/cli/node.py index 55304356..b34fd155 100644 --- a/node_cli/cli/node.py +++ b/node_cli/cli/node.py @@ -17,8 +17,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import get_args + import click +from skale_core.types import EnvType from node_cli.cli.info import TYPE from node_cli.core.node import ( cleanup as cleanup_skale, @@ -38,7 +41,6 @@ run_checks, ) from node_cli.configs import DEFAULT_NODE_BASE_PORT -from node_cli.configs.user import ALLOWED_ENV_TYPES from node_cli.core.node_options import upsert_node_mode from node_cli.utils.decorators import check_inited from node_cli.utils.helper import abort_if_false, streamed_cmd, IP_TYPE @@ -85,10 +87,10 @@ def register_node(name, ip, port, domain): @node.command('init', help='Initialize SKALE node') -@click.argument('env_file') +@click.argument('config_file') @streamed_cmd -def init_node(env_file): - init(env_filepath=env_file, node_type=TYPE) +def init_node(config_file): + init(config_file=config_file, node_type=TYPE) @node.command('update', help='Update node from .env file') @@ -101,12 +103,12 @@ def init_node(env_file): ) @click.option('--pull-config', 'pull_config_for_schain', hidden=True, type=str) @click.option('--unsafe', 'unsafe_ok', help='Allow unsafe update', hidden=True, is_flag=True) -@click.argument('env_file') +@click.argument('config_file') @streamed_cmd -def update_node(env_file, pull_config_for_schain, unsafe_ok): +def update_node(config_file, pull_config_for_schain, unsafe_ok): update( node_mode=NodeMode.ACTIVE, - env_filepath=env_file, + config_file=config_file, pull_config_for_schain=pull_config_for_schain, node_type=TYPE, unsafe_ok=unsafe_ok, @@ -143,7 +145,7 @@ def backup_node(backup_folder_path): def restore_node(backup_path, env_file, no_snapshot, config_only): restore( backup_path=backup_path, - env_filepath=env_file, + config_file=env_file, no_snapshot=no_snapshot, config_only=config_only, node_type=TYPE, @@ -227,7 +229,7 @@ def _set_domain_name(domain): @click.option( '--network', '-n', - type=click.Choice(ALLOWED_ENV_TYPES), + type=click.Choice(get_args(EnvType)), default='mainnet', help='Network to check', ) diff --git a/node_cli/cli/passive_fair_node.py b/node_cli/cli/passive_fair_node.py index 83eb1bd1..7bac3581 100644 --- a/node_cli/cli/passive_fair_node.py +++ b/node_cli/cli/passive_fair_node.py @@ -49,7 +49,7 @@ def passive_node(): @passive_node.command('init', help='Initialize a passive Fair node') -@click.argument('env_filepath') +@click.argument('config_file') @click.option('--id', required=True, type=int, help=TEXTS['fair']['node']['setup']['id']) @click.option('--indexer', help=TEXTS['passive_node']['init']['indexer'], is_flag=True) @click.option('--archive', help=TEXTS['passive_node']['init']['archive'], is_flag=True) @@ -61,7 +61,7 @@ def passive_node(): ) @streamed_cmd def init_passive_node( - env_filepath: str, id: int, indexer: bool, archive: bool, snapshot: str | None + config_file: str, id: int, indexer: bool, archive: bool, snapshot: str | None ): if indexer and archive: error_exit('Cannot use both --indexer and --archive options') @@ -69,7 +69,7 @@ def init_passive_node( error_exit('Cannot use any for indexer/archive node') init_fair( node_mode=NodeMode.PASSIVE, - env_filepath=env_filepath, + config_file=config_file, node_id=id, indexer=indexer, archive=archive, @@ -78,7 +78,7 @@ def init_passive_node( @passive_node.command('update', help='Update Fair node') -@click.argument('env_filepath') +@click.argument('config_file') @click.option( '--yes', is_flag=True, @@ -96,10 +96,10 @@ def init_passive_node( is_flag=True, ) @streamed_cmd -def update_node(env_filepath: str, pull_config_for_schain, force_skaled_start: bool): +def update_node(config_file: str, pull_config_for_schain, force_skaled_start: bool): update_fair( node_mode=NodeMode.PASSIVE, - env_filepath=env_filepath, + config_file=config_file, pull_config_for_schain=pull_config_for_schain, force_skaled_start=force_skaled_start, ) @@ -146,7 +146,7 @@ def turn_off_node() -> None: expose_value=False, prompt='Are you sure you want to turn on the node?', ) -@click.argument('env_filepath') +@click.argument('config_file') @streamed_cmd -def turn_on_node(env_filepath: str) -> None: - turn_on_fair(env_file=env_filepath, node_type=TYPE) +def turn_on_node(config_file: str) -> None: + turn_on_fair(env_file=config_file, node_type=TYPE) diff --git a/node_cli/cli/schains.py b/node_cli/cli/schains.py index 803ea754..6bb1fce2 100644 --- a/node_cli/cli/schains.py +++ b/node_cli/cli/schains.py @@ -21,6 +21,8 @@ import click +from skale_core.settings import get_settings + from node_cli.utils.helper import abort_if_false, URL_TYPE from node_cli.core.schains import ( describe, @@ -104,8 +106,12 @@ def info_(schain_name: str, json_format: bool) -> None: @click.argument('schain_name') @click.argument('snapshot_path') @click.option('--schain-type', default='medium') -@click.option('--env-type', default=None) -def restore( - schain_name: str, snapshot_path: str, schain_type: str, env_type: Optional[str] -) -> None: - restore_schain_from_snapshot(schain_name, snapshot_path, node_type=TYPE) +def restore(schain_name: str, snapshot_path: str, schain_type: str) -> None: + settings = get_settings() + restore_schain_from_snapshot( + schain_name, + snapshot_path, + node_type=TYPE, + env_type=settings.env_type, + schain_type=schain_type, + ) diff --git a/node_cli/configs/__init__.py b/node_cli/configs/__init__.py index 35d776d8..7ed6afee 100644 --- a/node_cli/configs/__init__.py +++ b/node_cli/configs/__init__.py @@ -19,6 +19,7 @@ import os import sys +from pathlib import Path from node_cli.utils.global_config import read_g_config @@ -43,17 +44,19 @@ NODE_DATA_PATH = os.path.join(SKALE_DIR, 'node_data') SCHAIN_NODE_DATA_PATH = os.path.join(NODE_DATA_PATH, 'schains') NODE_CLI_STATUS_FILENAME = 'node_cli.status' + +SETTINGS_DIR = Path(NODE_DATA_PATH) / 'settings' +NODE_SETTINGS_PATH = SETTINGS_DIR / 'node.toml' +INTERNAL_SETTINGS_PATH = SETTINGS_DIR / 'internal.toml' + NODE_CONFIG_PATH = os.path.join(NODE_DATA_PATH, 'node_config.json') CONTAINER_CONFIG_PATH = os.path.join(SKALE_DIR, 'config') CONTAINER_CONFIG_TMP_PATH = os.path.join(SKALE_TMP_DIR, 'config') CONTRACTS_PATH = os.path.join(SKALE_DIR, 'contracts_info') REPORTS_PATH = os.path.join(SKALE_DIR, 'reports') BACKUP_CONTRACTS_PATH = os.path.join(SKALE_DIR, '.old_contracts_info') -INIT_ENV_FILEPATH = os.path.join(SKALE_DIR, '.env') SKALE_RUN_DIR = '/var/run/skale' -SGX_CERTIFICATES_DIR_NAME = 'sgx_certs' - COMPOSE_PATH = os.path.join(CONTAINER_CONFIG_PATH, 'docker-compose.yml') FAIR_COMPOSE_PATH = os.path.join(CONTAINER_CONFIG_PATH, 'docker-compose-fair.yml') STATIC_PARAMS_FILEPATH = os.path.join(CONTAINER_CONFIG_PATH, 'static_params.yaml') @@ -73,9 +76,6 @@ SGX_CERTS_PATH = os.path.join(NODE_DATA_PATH, 'sgx_certs') SCHAINS_DATA_PATH = os.path.join(NODE_DATA_PATH, 'schains') -CURRENT_FILE_LOCATION = os.path.dirname(os.path.realpath(__file__)) -DOTENV_FILEPATH = os.path.join(os.path.dirname(CURRENT_FILE_LOCATION), '.env') - SRC_FILEBEAT_CONFIG_PATH = os.path.join(CONTAINER_CONFIG_PATH, 'filebeat.yml') FILEBEAT_CONFIG_PATH = os.path.join(NODE_DATA_PATH, 'filebeat.yml') @@ -95,9 +95,6 @@ IPTABLES_RULES_STATE_FILEPATH = os.path.join(IPTABLES_DIR, 'rules.v4') DEFAULT_SSH_PORT = 22 -FLASK_SECRET_KEY_FILENAME = 'flask_db_key.txt' -FLASK_SECRET_KEY_FILE = os.path.join(NODE_DATA_PATH, FLASK_SECRET_KEY_FILENAME) - DOCKER_CONFIG_FILEPATH = '/etc/docker/daemon.json' HIDE_STREAM_LOG = os.getenv('HIDE_STREAM_LOG') diff --git a/node_cli/configs/alias_address_validation.py b/node_cli/configs/alias_address_validation.py deleted file mode 100644 index ffdbb2ee..00000000 --- a/node_cli/configs/alias_address_validation.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of node-cli -# -# Copyright (C) 2025-Present SKALE Labs -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from enum import Enum -from typing import Dict, Optional - -import requests - -from node_cli.utils.helper import error_exit, is_contract_address - - -METADATA_URL: str = ( - 'https://raw.githubusercontent.com/skalenetwork/skale-contracts/' - 'refs/heads/deployments/metadata.json' -) - - -class ContractType(Enum): - """Contract types supported by the system using skale-contracts library.""" - - IMA = 'mainnet-ima' - MANAGER = 'skale-manager' - - -def validate_alias_or_address( - alias_or_address: str, contract_type: ContractType, endpoint: str -) -> None: - if is_contract_address(alias_or_address): - validate_contract_address(alias_or_address, endpoint) - else: - validate_contract_alias(alias_or_address, contract_type, endpoint) - - -def validate_contract_address(contract_address: str, endpoint: str) -> None: - try: - response = requests.post( - endpoint, - json={ - 'jsonrpc': '2.0', - 'method': 'eth_getCode', - 'params': [contract_address, 'latest'], - 'id': 1, - }, - ) - if response.status_code != 200: - error_exit(f'Failed to verify contract at address {contract_address}') - result = response.json().get('result') - if not result or result in ['0x', '0x0']: - error_exit(f'No contract code found at address {contract_address}') - except requests.RequestException as e: - error_exit(f'Failed to validate contract address: {str(e)}') - - -def get_deployment_url(alias: str, contract_type: ContractType, network_path: str) -> str: - return ( - f'https://raw.githubusercontent.com/skalenetwork/skale-contracts/' - f'refs/heads/deployments/{network_path}/{contract_type.value}/{alias}.json' - ) - - -def validate_contract_alias(alias: str, contract_type: ContractType, endpoint: str) -> None: - try: - chain_id = get_chain_id(endpoint) - metadata = get_network_metadata() - networks = metadata.get('networks', []) - network_path: Optional[str] = None - for net in networks: - if net.get('chainId') == chain_id: - network_path = net.get('path') - break - if not network_path: - error_exit(f'Network with chain ID {chain_id} not found in metadata') - if not isinstance(network_path, str): - error_exit(f'Invalid network path type: {network_path}') - deployment_url = get_deployment_url(alias, contract_type, network_path) - if requests.get(deployment_url).status_code != 200: - error_exit(f"Contract alias '{alias}' not found for {contract_type.value}") - except requests.RequestException as e: - error_exit(f"Failed to validate contract alias '{alias}': {str(e)}") - - -def get_chain_id(endpoint: str) -> int: - try: - response = requests.post( - endpoint, - json={'jsonrpc': '2.0', 'method': 'eth_chainId', 'params': [], 'id': 1}, - ) - if response.status_code != 200: - error_exit('Failed to get chain ID from endpoint') - return int(response.json()['result'], 16) - except requests.RequestException as e: - error_exit(f'Failed to get chain ID: {str(e)}') - - -def get_network_metadata() -> Dict: - try: - response = requests.get(METADATA_URL) - if response.status_code != 200: - error_exit('Failed to fetch networks metadata') - return response.json() - except requests.RequestException as e: - error_exit(f'Failed to fetch networks metadata: {str(e)}') diff --git a/node_cli/configs/user.py b/node_cli/configs/user.py deleted file mode 100644 index a4f52c2c..00000000 --- a/node_cli/configs/user.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of node-cli -# -# Copyright (C) 2019-Present SKALE Labs -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import inspect -import os -from abc import ABC -from dataclasses import dataclass -from typing import Dict, NamedTuple - -from dotenv.main import DotEnv - -from node_cli.configs import CONTAINER_CONFIG_PATH, SKALE_DIR -from node_cli.configs.alias_address_validation import ContractType, validate_alias_or_address -from node_cli.utils.helper import error_exit -from node_cli.utils.node_type import NodeMode, NodeType -from node_cli.core.node_options import ( - active_fair, - active_skale, - passive_skale, - passive_fair, -) - -SKALE_DIR_ENV_FILEPATH = os.path.join(SKALE_DIR, '.env') -CONFIGS_ENV_FILEPATH = os.path.join(CONTAINER_CONFIG_PATH, '.env') - -ALLOWED_ENV_TYPES = ['mainnet', 'testnet', 'qanet', 'devnet'] - - -class ValidationResult(NamedTuple): - result: bool - missing: set - extra: set - - -@dataclass(kw_only=True) -class BaseUserConfig(ABC): - node_version: str - env_type: str - filebeat_host: str - block_device: str - - container_configs_dir: str = '' - skip_docker_config: str = '' - skip_docker_cleanup: str = '' - - def to_env(self) -> Dict[str, str]: - result = {} - for field_name, field_value in self.__dict__.items(): - upper_key = field_name.upper() - result[upper_key] = str(field_value) if field_value is not None else '' - return result - - @classmethod - def validate_params(cls, params: Dict) -> ValidationResult: - parameters = inspect.signature(cls.__init__).parameters - missing = [] - keys = params.keys() - expected_keys = { - name.upper() - for name, value in parameters.items() - if name != 'self' and value.default == inspect._empty - } - optional_keys = { - name.upper() - for name, value in parameters.items() - if name != 'self' and value.default != inspect._empty - } - missing = expected_keys - keys - extra = keys - expected_keys - optional_keys - return ValidationResult(missing == set() and extra == set(), missing, extra) - - -@dataclass -class FairUserConfig(BaseUserConfig): - fair_contracts: str - boot_endpoint: str - sgx_server_url: str - enforce_btrfs: str = '' - telegraf: str = '' - influx_url: str = '' - - -@dataclass -class PassiveFairUserConfig(BaseUserConfig): - fair_contracts: str - boot_endpoint: str - enforce_btrfs: str = '' - - -@dataclass -class FairBootUserConfig(BaseUserConfig): - endpoint: str - manager_contracts: str - ima_contracts: str - sgx_server_url: str - enforce_btrfs: str = '' - - -@dataclass -class SkaleUserConfig(BaseUserConfig): - endpoint: str - manager_contracts: str - ima_contracts: str - docker_lvmpy_version: str - sgx_server_url: str - monitoring_containers: str = '' - telegraf: str = '' - influx_url: str = '' - tg_api_key: str = '' - tg_chat_id: str = '' - disable_dry_run: str = '' - default_gas_limit: str = '' - default_gas_price_wei: str = '' - bite: str = '' - - -@dataclass -class PassiveSkaleUserConfig(BaseUserConfig): - endpoint: str - manager_contracts: str - schain_name: str = '' - ima_contracts: str = '' - enforce_btrfs: str = '' - bite: str = '' - - -def get_validated_user_config( - node_type: NodeType, - node_mode: NodeMode, - env_filepath: str = SKALE_DIR_ENV_FILEPATH, - is_fair_boot: bool = False, - skip_user_conf_validation: bool = False, -) -> BaseUserConfig: - params = parse_env_file(env_filepath) - user_config_class = get_user_config_class( - node_type=node_type, - node_mode=node_mode, - is_fair_boot=is_fair_boot, - ) - _, missing_params, extra_params = user_config_class.validate_params(params) - - if len(missing_params) > 0: - error_exit(f'Missing required parameters: {missing_params}') - - if len(extra_params) > 0: - error_exit(f'Extra parameters: {extra_params}') - - params = to_lower_keys(params) - user_config = user_config_class(**params) - if not skip_user_conf_validation: - validate_user_config(user_config) - - return user_config - - -def validate_user_config(user_config: BaseUserConfig) -> None: - validate_env_type(env_type=user_config.env_type) - - if not isinstance(user_config, FairUserConfig) and not isinstance( - user_config, PassiveFairUserConfig - ): - validate_alias_or_address( - user_config.manager_contracts, ContractType.MANAGER, user_config.endpoint - ) - - if isinstance(user_config, (SkaleUserConfig, FairBootUserConfig)): - validate_alias_or_address(user_config.ima_contracts, ContractType.IMA, user_config.endpoint) - - -def to_lower_keys(params: Dict[str, str]) -> Dict[str, str]: - return {key.lower(): value for key, value in params.items()} - - -def parse_env_file(env_filepath: str) -> Dict: - if not os.path.isfile(env_filepath): - error_exit(f'Failed to load environment from {env_filepath}') - return DotEnv(env_filepath).dict() - - -def get_user_config_class( - node_type: NodeType, - node_mode: NodeMode, - is_fair_boot: bool, -) -> type[BaseUserConfig]: - if node_type == NodeType.FAIR and is_fair_boot: - user_config_class = FairBootUserConfig - elif passive_fair(node_type, node_mode): - user_config_class = PassiveFairUserConfig - elif active_fair(node_type, node_mode): - user_config_class = FairUserConfig - elif passive_skale(node_type, node_mode): - user_config_class = PassiveSkaleUserConfig - elif active_skale(node_type, node_mode): - user_config_class = SkaleUserConfig - return user_config_class - - -def validate_env_type(env_type: str) -> None: - if env_type not in ALLOWED_ENV_TYPES: - error_exit(f'Allowed ENV_TYPE values are {ALLOWED_ENV_TYPES}. Actual: "{env_type}"') diff --git a/node_cli/core/checks.py b/node_cli/core/checks.py index a4c439c9..0424577f 100644 --- a/node_cli/core/checks.py +++ b/node_cli/core/checks.py @@ -48,6 +48,8 @@ from debian import debian_support from packaging.version import parse as version_parse +from skale_core.types import EnvType + from node_cli.configs import ( CHECK_REPORT_PATH, CONTAINER_CONFIG_PATH, @@ -471,7 +473,7 @@ def run_checks( disk: str, node_type: NodeType, node_mode: NodeMode, - env_type: str = 'mainnet', + env_type: EnvType = 'mainnet', config_path: str = CONTAINER_CONFIG_PATH, check_type: CheckType = CheckType.ALL, ) -> ResultList: diff --git a/node_cli/core/host.py b/node_cli/core/host.py index da4be640..a040b8c4 100644 --- a/node_cli/core/host.py +++ b/node_cli/core/host.py @@ -19,9 +19,11 @@ import logging import os -from shutil import copyfile, chown +from shutil import chown from urllib.parse import urlparse +from skale_core.types import EnvType + from node_cli.core.resources import update_resource_allocation from node_cli.utils.helper import error_exit @@ -39,6 +41,7 @@ SGX_CERTS_PATH, REPORTS_PATH, REDIS_DATA_PATH, + SETTINGS_DIR, SCHAINS_DATA_PATH, LOG_PATH, REMOVED_CONTAINERS_FOLDER_PATH, @@ -50,7 +53,6 @@ NGINX_CONFIG_FILEPATH, ) from node_cli.configs.cli_logger import LOG_DATA_PATH -from node_cli.configs.user import SKALE_DIR_ENV_FILEPATH, CONFIGS_ENV_FILEPATH from node_cli.core.nftables import NFTablesManager from node_cli.utils.helper import safe_mkdir @@ -73,29 +75,11 @@ def fix_url(url): return False -def get_flask_secret_key() -> str: - secret_key_filepath = os.path.join(NODE_DATA_PATH, 'flask_db_key.txt') - - if not os.path.exists(secret_key_filepath): - error_exit(f'Flask secret key file not found at {secret_key_filepath}') - - try: - with open(secret_key_filepath, 'r') as key_file: - secret_key = key_file.read().strip() - return secret_key - except (IOError, OSError) as e: - error_exit(f'Failed to read Flask secret key: {e}') - - -def prepare_host(env_filepath: str, env_type: str, allocation: bool = False) -> None: - if not env_filepath or not env_type: - error_exit('Missing required parameters for host initialization') - +def prepare_host(env_type: EnvType, allocation: bool = False) -> None: try: logger.info('Preparing host started') make_dirs() chown(REDIS_DATA_PATH, user=999, group=1000) - save_env_params(env_filepath) if allocation: update_resource_allocation(env_type) @@ -121,6 +105,7 @@ def make_dirs(): LOG_PATH, REPORTS_PATH, REDIS_DATA_PATH, + SETTINGS_DIR, SKALE_RUN_DIR, SKALE_STATE_DIR, SKALE_TMP_DIR, @@ -128,16 +113,6 @@ def make_dirs(): safe_mkdir(dir_path) -def save_env_params(env_filepath: str) -> None: - copyfile(env_filepath, SKALE_DIR_ENV_FILEPATH) - - -def link_env_file(): - if not (os.path.islink(CONFIGS_ENV_FILEPATH) or os.path.isfile(CONFIGS_ENV_FILEPATH)): - logger.info('Creating symlink %s → %s', SKALE_DIR_ENV_FILEPATH, CONFIGS_ENV_FILEPATH) - os.symlink(SKALE_DIR_ENV_FILEPATH, CONFIGS_ENV_FILEPATH) - - def init_logs_dir(): safe_mkdir(LOG_DATA_PATH) safe_mkdir(REMOVED_CONTAINERS_FOLDER_PATH) diff --git a/node_cli/core/node.py b/node_cli/core/node.py index 2128d43f..7c9e2409 100644 --- a/node_cli/core/node.py +++ b/node_cli/core/node.py @@ -33,7 +33,6 @@ BACKUP_ARCHIVE_NAME, CONTAINER_CONFIG_PATH, FILESTORAGE_MAPPING, - INIT_ENV_FILEPATH, LOG_PATH, RESTORE_SLEEP_TIMEOUT, SCHAINS_MNT_DIR_REGULAR, @@ -43,9 +42,8 @@ TM_INIT_TIMEOUT, ) from node_cli.configs.cli_logger import LOG_DATA_PATH as CLI_LOG_DATA_PATH -from node_cli.configs.user import SKALE_DIR_ENV_FILEPATH, get_validated_user_config from node_cli.core.checks import run_checks as run_host_checks -from node_cli.core.host import get_flask_secret_key, is_node_inited, save_env_params +from node_cli.core.host import is_node_inited from node_cli.core.resources import update_resource_allocation from node_cli.core.node_options import ( active_fair, @@ -89,6 +87,8 @@ print_node_cmd_error, print_node_info, ) +from node_cli.utils.settings import validate_and_save_node_settings +from skale_core.settings import get_settings from node_cli.utils.texts import safe_load_texts logger = logging.getLogger(__name__) @@ -152,51 +152,64 @@ def register_node(name, p2p_ip, public_ip, port, domain_name): @check_not_inited -def init(env_filepath: str, node_type: NodeType) -> None: +def init(config_file: str, node_type: NodeType) -> None: node_mode = NodeMode.ACTIVE - env = compose_node_env(env_filepath=env_filepath, node_type=node_type, node_mode=node_mode) + settings = validate_and_save_node_settings(config_file, node_type, node_mode) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) - init_op(env_filepath=env_filepath, env=env, node_mode=node_mode) + init_op(settings=settings, compose_env=compose_env, node_mode=node_mode) logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) if not is_base_containers_alive(node_type=node_type, node_mode=node_mode): error_exit('Containers are not running', exit_code=CLIExitCodes.OPERATION_EXECUTION_ERROR) logger.info('Generating resource allocation file ...') - update_resource_allocation(env['ENV_TYPE']) + update_resource_allocation(settings.env_type) logger.info('Init procedure finished') @check_not_inited -def restore(backup_path, env_filepath, node_type: NodeType, no_snapshot=False, config_only=False): +def restore( + backup_path: str, + config_file: str, + node_type: NodeType, + no_snapshot: bool = False, + config_only: bool = False, +): node_mode = NodeMode.ACTIVE - env = compose_node_env(env_filepath=env_filepath, node_type=node_type, node_mode=node_mode) - if env is None: - return - save_env_params(env_filepath) - env['SKALE_DIR'] = SKALE_DIR - - if not no_snapshot: - logger.info('Adding BACKUP_RUN to env ...') - env['BACKUP_RUN'] = 'True' # should be str + settings = validate_and_save_node_settings(config_file, node_type, node_mode) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) - restored_ok = restore_op(env, backup_path, node_type=node_type, config_only=config_only) + restored_ok = restore_op( + settings=settings, + compose_env=compose_env, + backup_path=backup_path, + node_type=node_type, + config_only=config_only, + backup_run=not no_snapshot, + ) if not restored_ok: error_exit('Restore operation failed', exit_code=CLIExitCodes.OPERATION_EXECUTION_ERROR) time.sleep(RESTORE_SLEEP_TIMEOUT) logger.info('Generating resource allocation file ...') - update_resource_allocation(env['ENV_TYPE']) + update_resource_allocation(settings.env_type) print('Node is restored from backup') @check_not_inited def init_passive( - env_filepath: str, indexer: bool, archive: bool, snapshot: bool, snapshot_from: Optional[str] + config_file: str, indexer: bool, archive: bool, snapshot: bool, snapshot_from: Optional[str] ) -> None: node_mode = NodeMode.PASSIVE - env = compose_node_env(env_filepath, node_type=NodeType.SKALE, node_mode=node_mode) - if env is None: - return - init_passive_op(env_filepath, env, indexer, archive, snapshot, snapshot_from) + settings = validate_and_save_node_settings(config_file, NodeType.SKALE, node_mode) + compose_env = compose_node_env(node_type=NodeType.SKALE, node_mode=node_mode) + init_passive_op( + settings=settings, + compose_env=compose_env, + indexer=indexer, + archive=archive, + snapshot=snapshot, + snapshot_from=snapshot_from, + ) logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) if not is_base_containers_alive(node_type=NodeType.SKALE, node_mode=node_mode): @@ -206,13 +219,14 @@ def init_passive( @check_inited @check_user -def update_passive(env_filepath: str) -> None: +def update_passive(config_file: str) -> None: logger.info('Node update started') prev_version = CliMetaManager().get_meta_info().version if (__version__ == 'test' or __version__.startswith('2.6')) and prev_version == '2.5.0': migrate_2_6() - env = compose_node_env(env_filepath, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) - update_ok = update_passive_op(env_filepath, env) + settings = validate_and_save_node_settings(config_file, NodeType.SKALE, NodeMode.PASSIVE) + compose_env = compose_node_env(node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) + update_ok = update_passive_op(settings=settings, compose_env=compose_env) if update_ok: logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) @@ -227,76 +241,31 @@ def update_passive(env_filepath: str) -> None: @check_user def cleanup(node_mode: NodeMode, prune: bool = False) -> None: node_mode = upsert_node_mode(node_mode=node_mode) - env = compose_node_env( - SKALE_DIR_ENV_FILEPATH, - save=False, - node_type=NodeType.SKALE, - node_mode=node_mode, - skip_user_conf_validation=True, - ) - cleanup_skale_op(node_mode=node_mode, env=env, prune=prune) + env = compose_node_env(NodeType.SKALE, node_mode) + cleanup_skale_op(node_mode=node_mode, compose_env=env, prune=prune) logger.info('SKALE node was cleaned up, all containers and data removed') -def compose_node_env( - env_filepath: str, - node_type: NodeType, - node_mode: NodeMode, - inited_node: bool = False, - sync_schains: Optional[bool] = None, - pull_config_for_schain: Optional[str] = None, - save: bool = True, - is_fair_boot: bool = False, - skip_user_conf_validation: bool = False, -) -> dict[str, str]: - if env_filepath is not None: - user_config = get_validated_user_config( - node_type=node_type, - node_mode=node_mode, - env_filepath=env_filepath, - is_fair_boot=is_fair_boot, - skip_user_conf_validation=skip_user_conf_validation, - ) - if save: - save_env_params(env_filepath) - else: - user_config = get_validated_user_config( - node_type=node_type, - node_mode=node_mode, - env_filepath=INIT_ENV_FILEPATH, - is_fair_boot=is_fair_boot, - skip_user_conf_validation=skip_user_conf_validation, - ) - +def compose_node_env(node_type: NodeType, node_mode: NodeMode) -> dict[str, str]: + st = get_settings() if node_mode == NodeMode.PASSIVE or node_type == NodeType.FAIR: mnt_dir = SCHAINS_MNT_DIR_SINGLE_CHAIN else: mnt_dir = SCHAINS_MNT_DIR_REGULAR - env = { 'SKALE_DIR': SKALE_DIR, 'SCHAINS_MNT_DIR': mnt_dir, 'FILESTORAGE_MAPPING': FILESTORAGE_MAPPING, 'SKALE_LIB_PATH': SKALE_STATE_DIR, - **user_config.to_env(), + 'FILEBEAT_HOST': st.filebeat_host, } - - if inited_node and not node_mode == NodeMode.PASSIVE: - env['FLASK_SECRET_KEY'] = get_flask_secret_key() - - if sync_schains and not node_mode == NodeMode.PASSIVE: - env['BACKUP_RUN'] = 'True' - - if pull_config_for_schain: - env['PULL_CONFIG_FOR_SCHAIN'] = pull_config_for_schain - return {k: v for k, v in env.items() if v != ''} @check_inited @check_user def update( - env_filepath: str, + config_file: str, pull_config_for_schain: Optional[str], node_type: NodeType, node_mode: NodeMode, @@ -312,15 +281,9 @@ def update( if (__version__ == 'test' or __version__.startswith('2.6')) and prev_version == '2.5.0': migrate_2_6() logger.info('Node update started') - env = compose_node_env( - env_filepath, - inited_node=True, - sync_schains=False, - pull_config_for_schain=pull_config_for_schain, - node_type=node_type, - node_mode=node_mode, - ) - update_ok = update_op(env_filepath, env, node_mode=node_mode) + settings = validate_and_save_node_settings(config_file, node_type, node_mode) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) + update_ok = update_op(settings=settings, compose_env=compose_env, node_mode=node_mode) if update_ok: logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) @@ -433,24 +396,24 @@ def turn_off(node_type: NodeType, maintenance_on: bool = False, unsafe_ok: bool error_exit(error_msg, exit_code=CLIExitCodes.UNSAFE_UPDATE) if maintenance_on: set_maintenance_mode_on() - env = compose_node_env( - SKALE_DIR_ENV_FILEPATH, save=False, node_type=node_type, node_mode=node_mode - ) - turn_off_op(node_type=node_type, node_mode=node_mode, env=env) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) + turn_off_op(compose_env=compose_env, node_type=node_type, node_mode=node_mode) @check_inited @check_user -def turn_on(maintenance_off, sync_schains, env_file, node_type: NodeType) -> None: +def turn_on(maintenance_off: bool, sync_schains: bool, env_file: str, node_type: NodeType) -> None: node_mode = upsert_node_mode() - env = compose_node_env( - env_file, - inited_node=True, - sync_schains=sync_schains, + settings = validate_and_save_node_settings(env_file, node_type, node_mode) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) + backup_run = sync_schains and node_mode != NodeMode.PASSIVE + turn_on_op( + settings=settings, + compose_env=compose_env, node_type=node_type, node_mode=node_mode, + backup_run=backup_run, ) - turn_on_op(env=env, node_type=node_type, node_mode=node_mode) logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) if not is_base_containers_alive(node_type=node_type, node_mode=node_mode): @@ -545,8 +508,8 @@ def run_checks( return if disk is None: - env_config = get_validated_user_config(node_type=node_type, node_mode=node_mode) - disk = env_config.block_device + settings = get_settings() + disk = settings.block_device failed_checks = run_host_checks(disk, node_type, node_mode, network, container_config_path) if not failed_checks: print('Requirements checking successfully finished!') diff --git a/node_cli/core/resources.py b/node_cli/core/resources.py index 430eee86..34f4cac5 100644 --- a/node_cli/core/resources.py +++ b/node_cli/core/resources.py @@ -24,7 +24,9 @@ import psutil -from node_cli.configs.user import get_validated_user_config +from skale_core.types import EnvType + +from node_cli.utils.settings import validate_and_save_node_settings from node_cli.utils.docker_utils import ensure_volume from node_cli.utils.schain_types import SchainTypes from node_cli.utils.helper import write_json, read_json, run_cmd, safe_load_yml @@ -73,7 +75,7 @@ def get_resource_allocation_info(): return None -def compose_resource_allocation_config(env_type: str, params_by_env_type: Dict = None) -> Dict: +def compose_resource_allocation_config(env_type: EnvType, params_by_env_type: Dict = None) -> Dict: params_by_env_type = params_by_env_type or safe_load_yml(STATIC_PARAMS_FILEPATH) common_config = params_by_env_type['common'] schain_cpu_alloc, ima_cpu_alloc = get_cpu_alloc(common_config) @@ -93,22 +95,20 @@ def compose_resource_allocation_config(env_type: str, params_by_env_type: Dict = def generate_resource_allocation_config( - env_file, + env_file: str, node_type: NodeType, node_mode: NodeMode, - force=False, + force: bool = False, ) -> None: if not force and os.path.isfile(RESOURCE_ALLOCATION_FILEPATH): msg = 'Resource allocation file already exists' logger.debug(msg) print(msg) return - user_config = get_validated_user_config( - node_type=node_type, node_mode=node_mode, env_filepath=env_file - ) + settings = validate_and_save_node_settings(env_file, node_type, node_mode) logger.info('Generating resource allocation file ...') try: - update_resource_allocation(user_config.env_type) + update_resource_allocation(settings.env_type) except Exception as e: logger.exception(e) print("Can't generate resource allocation file, check out CLI logs") @@ -116,7 +116,7 @@ def generate_resource_allocation_config( print(f'Resource allocation file generated: {RESOURCE_ALLOCATION_FILEPATH}') -def update_resource_allocation(env_type: str) -> None: +def update_resource_allocation(env_type: EnvType) -> None: resource_allocation_config = compose_resource_allocation_config(env_type) write_json(RESOURCE_ALLOCATION_FILEPATH, resource_allocation_config) diff --git a/node_cli/core/schains.py b/node_cli/core/schains.py index 64c16031..8aac42f1 100644 --- a/node_cli/core/schains.py +++ b/node_cli/core/schains.py @@ -26,6 +26,8 @@ from pathlib import Path from typing import Dict, Optional +from skale_core.types import EnvType + from lvmpy.src.core import mount, volume_mountpoint from node_cli.configs import ( ALLOCATION_FILEPATH, @@ -34,7 +36,6 @@ SCHAIN_NODE_DATA_PATH, SCHAINS_MNT_DIR_SINGLE_CHAIN, ) -from node_cli.configs.user import get_validated_user_config from node_cli.utils.docker_utils import ensure_volume, is_volume_exists from node_cli.utils.exit_codes import CLIExitCodes from node_cli.utils.helper import ( @@ -214,12 +215,9 @@ def restore_schain_from_snapshot( schain: str, snapshot_path: str, node_type: NodeType, - env_type: Optional[str] = None, + env_type: EnvType, schain_type: str = 'medium', ) -> None: - if env_type is None: - user_config = get_validated_user_config(node_type=node_type) - env_type = user_config.env_type ensure_schain_volume(schain, schain_type, env_type) block_number = get_block_number_from_path(snapshot_path) if block_number == -1: @@ -242,12 +240,12 @@ def get_schains_by_artifacts() -> str: return '\n'.join(os.listdir(SCHAIN_NODE_DATA_PATH)) -def get_schain_volume_size(schain_type: str, env_type: str) -> int: +def get_schain_volume_size(schain_type: str, env_type: EnvType) -> int: alloc = safe_load_yml(ALLOCATION_FILEPATH) return alloc[env_type]['disk'][schain_type] -def ensure_schain_volume(schain: str, schain_type: str, env_type: str) -> None: +def ensure_schain_volume(schain: str, schain_type: str, env_type: EnvType) -> None: if not is_volume_exists(schain): size = get_schain_volume_size(schain_type, env_type) ensure_volume(schain, size) @@ -313,4 +311,4 @@ def cleanup_lvm_datadir(): cleanup_dir_content('/mnt/') logger.info('Removing LVM volume group "schains"...') run_cmd(['sudo', 'lvremove', '-f', 'schains'], check_code=False) - logger.info('Active node cleanup finished.') \ No newline at end of file + logger.info('Active node cleanup finished.') diff --git a/node_cli/core/static_config.py b/node_cli/core/static_config.py index a93a7a60..06e0e7f4 100644 --- a/node_cli/core/static_config.py +++ b/node_cli/core/static_config.py @@ -28,10 +28,12 @@ ) from node_cli.utils.node_type import NodeType +from skale_core.types import EnvType + def get_static_params( node_type: NodeType, - env_type: str = 'mainnet', + env_type: EnvType = 'mainnet', config_path: str = CONTAINER_CONFIG_PATH, ) -> dict: if node_type == NodeType.FAIR: @@ -46,7 +48,7 @@ def get_static_params( return ydata['envs'][env_type] -def get_fair_chain_name(env: dict) -> str: +def get_fair_chain_name(env_type: EnvType) -> str: node_type = NodeType.FAIR - params = get_static_params(node_type, env['ENV_TYPE']) + params = get_static_params(node_type, env_type) return params['info']['chain_name'] diff --git a/node_cli/fair/active.py b/node_cli/fair/active.py index 963686fe..2ec42bde 100644 --- a/node_cli/fair/active.py +++ b/node_cli/fair/active.py @@ -22,8 +22,8 @@ import time from typing import cast -from node_cli.configs import DEFAULT_SKALED_BASE_PORT, RESTORE_SLEEP_TIMEOUT, SKALE_DIR -from node_cli.core.host import is_node_inited, save_env_params +from node_cli.configs import DEFAULT_SKALED_BASE_PORT, RESTORE_SLEEP_TIMEOUT +from node_cli.core.host import is_node_inited from node_cli.core.node import compose_node_env, is_base_containers_alive from node_cli.operations import ( FairUpdateType, @@ -35,6 +35,7 @@ from node_cli.utils.helper import error_exit, get_request, post_request from node_cli.utils.node_type import NodeMode, NodeType from node_cli.utils.print_formatters import print_node_cmd_error, print_node_info_fair +from node_cli.utils.settings import validate_and_save_node_settings from node_cli.utils.texts import safe_load_texts logger = logging.getLogger(__name__) @@ -63,19 +64,14 @@ def get_node_info(format): @check_inited @check_user def migrate_from_boot( - env_filepath: str, + config_file: str, ) -> None: logger.info('Migrating from boot to fair node...') - env = compose_node_env( - env_filepath, - inited_node=True, - sync_schains=False, - node_type=NodeType.FAIR, - node_mode=NodeMode.ACTIVE, - ) + settings = validate_and_save_node_settings(config_file, NodeType.FAIR, NodeMode.ACTIVE) + compose_env = compose_node_env(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) migrate_ok = update_fair_op( - env_filepath, - env, + settings=settings, + compose_env=compose_env, node_mode=NodeMode.ACTIVE, update_type=FairUpdateType.FROM_BOOT, force_skaled_start=False, @@ -145,16 +141,17 @@ def exit() -> None: @check_not_inited -def restore(backup_path, env_filepath, config_only=False): +def restore(backup_path: str, config_file: str, config_only: bool = False): node_mode = NodeMode.ACTIVE - env = compose_node_env(env_filepath, node_type=NodeType.FAIR, node_mode=node_mode) - if env is None: - return - save_env_params(env_filepath) - env['SKALE_DIR'] = SKALE_DIR + settings = validate_and_save_node_settings(config_file, NodeType.FAIR, node_mode) + compose_env = compose_node_env(node_type=NodeType.FAIR, node_mode=node_mode) restored_ok = restore_fair_op( - node_mode=node_mode, env=env, backup_path=backup_path, config_only=config_only + node_mode=node_mode, + settings=settings, + compose_env=compose_env, + backup_path=backup_path, + config_only=config_only, ) if not restored_ok: error_exit('Restore operation failed', exit_code=CLIExitCodes.OPERATION_EXECUTION_ERROR) diff --git a/node_cli/fair/boot.py b/node_cli/fair/boot.py index a6415f75..d9da98fa 100644 --- a/node_cli/fair/boot.py +++ b/node_cli/fair/boot.py @@ -30,22 +30,19 @@ from node_cli.utils.helper import error_exit from node_cli.utils.node_type import NodeMode, NodeType from node_cli.utils.print_formatters import print_node_cmd_error +from node_cli.utils.settings import validate_and_save_node_settings logger = logging.getLogger(__name__) @check_not_inited -def init(env_filepath: str) -> None: +def init(config_file: str) -> None: node_mode = NodeMode.ACTIVE node_type = NodeType.FAIR - env = compose_node_env( - env_filepath, - node_type=node_type, - node_mode=node_mode, - is_fair_boot=True, - ) + settings = validate_and_save_node_settings(config_file, node_type, node_mode) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) - init_fair_boot_op(env_filepath, env, node_mode) + init_fair_boot_op(settings=settings, compose_env=compose_env, node_mode=node_mode) logger.info('Waiting for fair containers initialization') time.sleep(TM_INIT_TIMEOUT) if not is_base_containers_alive(node_type=node_type, node_mode=node_mode, is_fair_boot=True): @@ -55,19 +52,16 @@ def init(env_filepath: str) -> None: @check_inited @check_user -def update(env_filepath: str, pull_config_for_schain: str) -> None: +def update(config_file: str, pull_config_for_schain: str) -> None: logger.info('Fair boot node update started') node_mode = upsert_node_mode(node_mode=NodeMode.ACTIVE) - env = compose_node_env( - env_filepath, - inited_node=True, - sync_schains=False, - pull_config_for_schain=pull_config_for_schain, - node_type=NodeType.FAIR, - node_mode=node_mode, - is_fair_boot=True, + settings = validate_and_save_node_settings(config_file, NodeType.FAIR, node_mode) + compose_env = compose_node_env(node_type=NodeType.FAIR, node_mode=node_mode) + migrate_ok = update_fair_boot_op( + settings=settings, + compose_env=compose_env, + node_mode=NodeMode.ACTIVE, ) - migrate_ok = update_fair_boot_op(env_filepath, env, node_mode=NodeMode.ACTIVE) if migrate_ok: logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) diff --git a/node_cli/fair/common.py b/node_cli/fair/common.py index 968891d4..98f10a7f 100644 --- a/node_cli/fair/common.py +++ b/node_cli/fair/common.py @@ -20,9 +20,7 @@ import logging import time -from node_cli.configs import INIT_TIMEOUT, SKALE_DIR, TM_INIT_TIMEOUT -from node_cli.configs.user import SKALE_DIR_ENV_FILEPATH -from node_cli.core.host import save_env_params +from node_cli.configs import INIT_TIMEOUT, TM_INIT_TIMEOUT from node_cli.core.node import compose_node_env, is_base_containers_alive from node_cli.core.node_options import upsert_node_mode from node_cli.fair.passive import setup_fair_passive @@ -40,7 +38,9 @@ from node_cli.utils.helper import error_exit from node_cli.utils.node_type import NodeMode, NodeType from node_cli.utils.print_formatters import print_node_cmd_error +from node_cli.utils.settings import validate_and_save_node_settings from node_cli.utils.texts import safe_load_texts +from skale_core.settings import get_settings logger = logging.getLogger(__name__) TEXTS = safe_load_texts() @@ -49,21 +49,18 @@ @check_not_inited def init( node_mode: NodeMode, - env_filepath: str, + config_file: str, node_id: int | None = None, indexer: bool = False, archive: bool = False, snapshot: str | None = None, ) -> None: - env = compose_node_env(env_filepath, node_type=NodeType.FAIR, node_mode=node_mode) - if env is None: - return - save_env_params(env_filepath) - env['SKALE_DIR'] = SKALE_DIR + settings = validate_and_save_node_settings(config_file, NodeType.FAIR, node_mode) + compose_env = compose_node_env(node_type=NodeType.FAIR, node_mode=node_mode) init_ok = init_fair_op( - env_filepath, - env, + settings=settings, + compose_env=compose_env, node_mode=node_mode, indexer=indexer, archive=archive, @@ -79,17 +76,12 @@ def init( print('Fair node is initialized') +@check_inited @check_user def cleanup(node_mode: NodeMode, prune: bool = False) -> None: node_mode = upsert_node_mode(node_mode=node_mode) - env = compose_node_env( - SKALE_DIR_ENV_FILEPATH, - save=False, - node_type=NodeType.FAIR, - node_mode=node_mode, - skip_user_conf_validation=True, - ) - cleanup_fair_op(node_mode=node_mode, env=env, prune=prune) + compose_env = compose_node_env(node_type=NodeType.FAIR, node_mode=node_mode) + cleanup_fair_op(node_mode=node_mode, compose_env=compose_env, prune=prune) logger.info('Fair node was cleaned up, all containers and data removed') @@ -97,29 +89,23 @@ def cleanup(node_mode: NodeMode, prune: bool = False) -> None: @check_user def update( node_mode: NodeMode, - env_filepath: str, + config_file: str, pull_config_for_schain: str | None = None, force_skaled_start: bool = False, ) -> None: logger.info( 'Updating fair node: %s, pull_config_for_schain: %s, force_skaled_start: %s', - env_filepath, + config_file, pull_config_for_schain, force_skaled_start, ) node_mode = upsert_node_mode(node_mode=node_mode) - env = compose_node_env( - env_filepath, - inited_node=True, - sync_schains=False, - node_type=NodeType.FAIR, - node_mode=node_mode, - pull_config_for_schain=pull_config_for_schain, - ) + settings = validate_and_save_node_settings(config_file, NodeType.FAIR, node_mode) + compose_env = compose_node_env(node_type=NodeType.FAIR, node_mode=node_mode) update_ok = update_fair_op( - env_filepath, - env, + settings=settings, + compose_env=compose_env, node_mode=node_mode, update_type=FairUpdateType.REGULAR, force_skaled_start=force_skaled_start, @@ -133,34 +119,25 @@ def update( def repair_chain(snapshot_from: str = 'any') -> None: - node_mode = upsert_node_mode() - env = compose_node_env( - SKALE_DIR_ENV_FILEPATH, save=False, node_type=NodeType.FAIR, node_mode=node_mode - ) - repair_fair_op(env=env, snapshot_from=snapshot_from) + settings = get_settings() + repair_fair_op(env_type=settings.env_type, snapshot_from=snapshot_from) @check_inited @check_user def turn_off(node_type: NodeType) -> None: node_mode = upsert_node_mode() - env = compose_node_env( - SKALE_DIR_ENV_FILEPATH, save=False, node_type=node_type, node_mode=node_mode - ) - turn_off_op(node_type=node_type, node_mode=node_mode, env=env) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) + turn_off_op(compose_env=compose_env, node_type=node_type, node_mode=node_mode) @check_inited @check_user -def turn_on(env_file, node_type: NodeType) -> None: +def turn_on(env_file: str, node_type: NodeType) -> None: node_mode = upsert_node_mode() - env = compose_node_env( - env_file, - inited_node=True, - node_type=node_type, - node_mode=node_mode, - ) - turn_on_op(env=env, node_type=node_type, node_mode=node_mode) + settings = validate_and_save_node_settings(env_file, node_type, node_mode) + compose_env = compose_node_env(node_type=node_type, node_mode=node_mode) + turn_on_op(settings=settings, compose_env=compose_env, node_type=node_type, node_mode=node_mode) logger.info('Waiting for containers initialization') time.sleep(TM_INIT_TIMEOUT) if not is_base_containers_alive(node_type=node_type, node_mode=node_mode): diff --git a/node_cli/fair/record/chain_record.py b/node_cli/fair/record/chain_record.py index d4ac740c..b6d48886 100644 --- a/node_cli/fair/record/chain_record.py +++ b/node_cli/fair/record/chain_record.py @@ -22,6 +22,8 @@ from typing import cast from datetime import datetime +from skale_core.types import EnvType + from node_cli.core.static_config import get_fair_chain_name from node_cli.fair.record.redis_record import FlatRedisRecord, FieldInfo @@ -78,18 +80,17 @@ def set_force_skaled_start(self, value: bool) -> None: self._set_field('force_skaled_start', value) -def get_fair_chain_record(env: dict) -> ChainRecord: - return ChainRecord(get_fair_chain_name(env)) +def get_fair_chain_record(env_type: EnvType) -> ChainRecord: + return ChainRecord(get_fair_chain_name(env_type)) -def migrate_chain_record(env: dict) -> None: - version = env['NODE_VERSION'] - logger.info('Migrating fair chain record, setting config version to %s', version) - record = get_fair_chain_record(env) - record.set_config_version(version) +def migrate_chain_record(env_type: EnvType, node_version: str) -> None: + logger.info('Migrating fair chain record, setting config version to %s', node_version) + record = get_fair_chain_record(env_type) + record.set_config_version(node_version) -def update_chain_record(env: dict, force_skaled_start: bool) -> None: - record = get_fair_chain_record(env) +def update_chain_record(env_type: EnvType, force_skaled_start: bool) -> None: + record = get_fair_chain_record(env_type) record.set_force_skaled_start(force_skaled_start) logger.info('Updated fair chain record with force_skaled_start=%s', force_skaled_start) diff --git a/node_cli/operations/base.py b/node_cli/operations/base.py index 1a64d585..803fe299 100644 --- a/node_cli/operations/base.py +++ b/node_cli/operations/base.py @@ -20,10 +20,12 @@ import functools import logging import time -from typing import Dict, Optional +from typing import Optional import distro +from skale_core.settings import BaseNodeSettings, SkalePassiveSettings, SkaleSettings, get_settings + from node_cli.cli.info import TYPE, VERSION from node_cli.configs import ( CONTAINER_CONFIG_PATH, @@ -37,7 +39,6 @@ from node_cli.core.docker_config import cleanup_docker_configuration, configure_docker from node_cli.core.host import ( ensure_btrfs_kernel_module_autoloaded, - link_env_file, prepare_host, ) from node_cli.core.nftables import configure_nftables @@ -53,7 +54,7 @@ cleanup_no_lvm_datadir, update_node_cli_schain_status, ) -from node_cli.operations.common import configure_filebeat, configure_flask, unpack_backup_archive +from node_cli.operations.common import configure_filebeat, unpack_backup_archive from node_cli.operations.config_repo import ( download_skale_node, sync_skale_node, @@ -72,23 +73,26 @@ remove_dynamic_containers, system_prune, ) -from node_cli.utils.helper import cleanup_dir_content, rm_dir, str_to_bool +from node_cli.utils.helper import cleanup_dir_content, rm_dir from node_cli.utils.meta import CliMetaManager, FairCliMetaManager from node_cli.utils.node_type import NodeMode, NodeType from node_cli.utils.print_formatters import print_failed_requirements_checks +from node_cli.utils.settings import save_internal_settings logger = logging.getLogger(__name__) def checked_host(func): @functools.wraps(func) - def wrapper(env_filepath: str, env: Dict, node_mode: NodeMode, *args, **kwargs): - download_skale_node(env.get('NODE_VERSION'), env.get('CONTAINER_CONFIGS_DIR')) + def wrapper( + settings: BaseNodeSettings, compose_env: dict, node_mode: NodeMode, *args, **kwargs + ): + download_skale_node(settings.node_version, settings.container_configs_dir or None) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, node_mode, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_TMP_PATH, check_type=CheckType.PREINSTALL, ) @@ -96,15 +100,15 @@ def wrapper(env_filepath: str, env: Dict, node_mode: NodeMode, *args, **kwargs): print_failed_requirements_checks(failed_checks) return False - result = func(env_filepath, env, node_mode, *args, **kwargs) + result = func(settings, compose_env, node_mode, *args, **kwargs) if not result: return result failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, node_mode, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, check_type=CheckType.POSTINSTALL, ) @@ -117,112 +121,118 @@ def wrapper(env_filepath: str, env: Dict, node_mode: NodeMode, *args, **kwargs): @checked_host -def update(env_filepath: str, env: Dict, node_mode: NodeMode) -> bool: - compose_rm(node_type=NodeType.SKALE, node_mode=node_mode, env=env) +def update(settings: BaseNodeSettings, compose_env: dict, node_mode: NodeMode) -> bool: + compose_rm(node_type=NodeType.SKALE, node_mode=node_mode, env=compose_env) remove_dynamic_containers() sync_skale_node() ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) - lvmpy_install(env) + lvmpy_install(settings.block_device) generate_nginx_config() - prepare_host(env_filepath, env['ENV_TYPE'], allocation=True) - init_shared_space_volume(env['ENV_TYPE']) + prepare_host(settings.env_type, allocation=True) + save_internal_settings(node_type=NodeType.SKALE, node_mode=node_mode) + init_shared_space_volume(settings.env_type) meta_manager = CliMetaManager() current_stream = meta_manager.get_meta_info().config_stream - skip_cleanup = env.get('SKIP_DOCKER_CLEANUP') == 'True' - if not skip_cleanup and current_stream != env['NODE_VERSION']: + if not settings.skip_docker_cleanup and current_stream != settings.node_version: logger.info( 'Stream version was changed from %s to %s', current_stream, - env['NODE_VERSION'], + settings.node_version, ) docker_cleanup() + skale_settings = get_settings(SkaleSettings) meta_manager.update_meta( VERSION, - env['NODE_VERSION'], - env['DOCKER_LVMPY_VERSION'], + settings.node_version, + skale_settings.docker_lvmpy_version, distro.id(), distro.version(), ) - update_images(env=env, node_type=NodeType.SKALE, node_mode=node_mode) - compose_up(env=env, node_type=NodeType.SKALE, node_mode=node_mode) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.SKALE, + node_mode=node_mode, + ) + compose_up(env=compose_env, settings=settings, node_type=NodeType.SKALE, node_mode=node_mode) return True @checked_host -def init(env_filepath: str, env: dict, node_mode: NodeMode) -> None: +def init(settings: BaseNodeSettings, compose_env: dict, node_mode: NodeMode) -> None: sync_skale_node() ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) - prepare_host(env_filepath, env_type=env['ENV_TYPE']) - link_env_file() + prepare_host(env_type=settings.env_type) + save_internal_settings(node_type=NodeType.SKALE, node_mode=node_mode) mark_active_node() configure_filebeat() - configure_flask() generate_nginx_config() - lvmpy_install(env) - init_shared_space_volume(env['ENV_TYPE']) + lvmpy_install(settings.block_device) + init_shared_space_volume(settings.env_type) + skale_settings = get_settings(SkaleSettings) meta_manager = CliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], - env['DOCKER_LVMPY_VERSION'], + settings.node_version, + skale_settings.docker_lvmpy_version, distro.id(), distro.version(), ) - update_resource_allocation(env_type=env['ENV_TYPE']) - update_images(env=env, node_type=NodeType.SKALE, node_mode=node_mode) - compose_up(env=env, node_type=NodeType.SKALE, node_mode=node_mode) + update_resource_allocation(env_type=settings.env_type) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.SKALE, + node_mode=node_mode, + ) + compose_up(env=compose_env, settings=settings, node_type=NodeType.SKALE, node_mode=node_mode) def init_passive( - env_filepath: str, - env: dict, + settings: BaseNodeSettings, + compose_env: dict, indexer: bool, archive: bool, snapshot: bool, snapshot_from: Optional[str], ) -> None: - cleanup_volume_artifacts(env['BLOCK_DEVICE']) - download_skale_node(env.get('NODE_VERSION'), env.get('CONTAINER_CONFIGS_DIR')) + cleanup_volume_artifacts(settings.block_device) + download_skale_node(settings.node_version, settings.container_configs_dir or None) sync_skale_node() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) - prepare_host( - env_filepath, - env_type=env['ENV_TYPE'], - ) + prepare_host(env_type=settings.env_type) + save_internal_settings(node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, NodeMode.PASSIVE, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, - check_type=CheckType.PREINSTALL + check_type=CheckType.PREINSTALL, ) if failed_checks: print_failed_requirements_checks(failed_checks) @@ -230,57 +240,66 @@ def init_passive( set_passive_node_options(archive=archive, indexer=indexer) ensure_filestorage_mapping() - link_env_file() generate_nginx_config() - prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True') + passive_settings = get_settings(SkalePassiveSettings) + prepare_block_device(settings.block_device, force=passive_settings.enforce_btrfs) meta_manager = CliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, None, distro.id(), distro.version(), ) - update_resource_allocation(env_type=env['ENV_TYPE']) + update_resource_allocation(env_type=settings.env_type) - schain_name = env['SCHAIN_NAME'] - if snapshot or snapshot_from: + if passive_settings.schain_name and (snapshot or snapshot_from): ts = int(time.time()) - update_node_cli_schain_status(schain_name, repair_ts=ts, snapshot_from=snapshot_from) + update_node_cli_schain_status( + passive_settings.schain_name, repair_ts=ts, snapshot_from=snapshot_from + ) - update_images(env=env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) - compose_up(env=env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.SKALE, + node_mode=NodeMode.PASSIVE, + ) + compose_up( + env=compose_env, settings=settings, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE + ) -def update_passive(env_filepath: str, env: Dict) -> bool: - compose_rm(env=env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) +def update_passive(settings: BaseNodeSettings, compose_env: dict) -> bool: + compose_rm(env=compose_env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) remove_dynamic_containers() - cleanup_volume_artifacts(env['BLOCK_DEVICE']) - download_skale_node(env['NODE_VERSION'], env.get('CONTAINER_CONFIGS_DIR')) + cleanup_volume_artifacts(settings.block_device) + download_skale_node(settings.node_version, settings.container_configs_dir or None) sync_skale_node() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) ensure_filestorage_mapping() - prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True') + passive_settings = get_settings(SkalePassiveSettings) + prepare_block_device(settings.block_device, force=passive_settings.enforce_btrfs) generate_nginx_config() - prepare_host(env_filepath, env['ENV_TYPE'], allocation=True) + prepare_host(settings.env_type, allocation=True) + save_internal_settings(node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, NodeMode.PASSIVE, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, - check_type=CheckType.PREINSTALL + check_type=CheckType.PREINSTALL, ) if failed_checks: print_failed_requirements_checks(failed_checks) @@ -288,56 +307,82 @@ def update_passive(env_filepath: str, env: Dict) -> bool: meta_manager = CliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, None, distro.id(), distro.version(), ) - update_images(env=env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) - compose_up(env=env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.SKALE, + node_mode=NodeMode.PASSIVE, + ) + compose_up( + env=compose_env, settings=settings, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE + ) return True -def turn_off(env: dict, node_type: NodeType, node_mode: NodeMode) -> None: +def turn_off(compose_env: dict, node_type: NodeType, node_mode: NodeMode) -> None: logger.info('Turning off the node...') - compose_rm(env=env, node_type=node_type, node_mode=node_mode) + compose_rm(env=compose_env, node_type=node_type, node_mode=node_mode) remove_dynamic_containers() logger.info('Node was successfully turned off') -def turn_on(env: dict, node_type: NodeType, node_mode: NodeMode) -> None: +def turn_on( + settings: BaseNodeSettings, + compose_env: dict, + node_type: NodeType, + node_mode: NodeMode, + backup_run: bool = False, +) -> None: logger.info('Turning on the node...') if node_type == NodeType.FAIR: meta_manager = FairCliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, distro.id(), distro.version(), ) else: + skale_settings = get_settings((SkaleSettings, SkalePassiveSettings)) + docker_lvmpy_version = ( + skale_settings.docker_lvmpy_version + if isinstance(skale_settings, SkaleSettings) + else None + ) meta_manager = CliMetaManager() meta_manager.update_meta( - VERSION, env['NODE_VERSION'], env['DOCKER_LVMPY_VERSION'], distro.id(), distro.version() + VERSION, settings.node_version, docker_lvmpy_version, distro.id(), distro.version() ) - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) + save_internal_settings(node_type=node_type, node_mode=node_mode, backup_run=backup_run) logger.info('Launching containers on the node...') - compose_up(env=env, node_type=node_type, node_mode=node_mode) + compose_up(env=compose_env, settings=settings, node_type=node_type, node_mode=node_mode) -def restore(env, backup_path, node_type: NodeType, config_only=False): +def restore( + settings: BaseNodeSettings, + compose_env: dict, + backup_path: str, + node_type: NodeType, + config_only: bool = False, + backup_run: bool = False, +) -> bool: node_mode = upsert_node_mode(node_mode=NodeMode.ACTIVE) unpack_backup_archive(backup_path) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, node_mode, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, check_type=CheckType.PREINSTALL, ) @@ -347,32 +392,32 @@ def restore(env, backup_path, node_type: NodeType, config_only=False): ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) - link_env_file() - lvmpy_install(env) - init_shared_space_volume(env['ENV_TYPE']) + lvmpy_install(settings.block_device) + init_shared_space_volume(settings.env_type) + skale_settings = get_settings(SkaleSettings) meta_manager = CliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], - env['DOCKER_LVMPY_VERSION'], + settings.node_version, + skale_settings.docker_lvmpy_version, distro.id(), distro.version(), ) + save_internal_settings(node_type=node_type, node_mode=node_mode, backup_run=backup_run) if not config_only: - compose_up(env=env, node_type=node_type, node_mode=node_mode) + compose_up(env=compose_env, settings=settings, node_type=node_type, node_mode=node_mode) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, node_mode, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, check_type=CheckType.POSTINSTALL, ) @@ -382,19 +427,20 @@ def restore(env, backup_path, node_type: NodeType, config_only=False): return True -def cleanup_passive(env, schain_name: str) -> None: - turn_off(env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) +def cleanup_passive(compose_env: dict, schain_name: str) -> None: + turn_off(compose_env, node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE) cleanup_no_lvm_datadir(chain_name=schain_name) rm_dir(GLOBAL_SKALE_DIR) rm_dir(SKALE_DIR) -def cleanup(node_mode: NodeMode, env: dict, prune: bool = False) -> None: - turn_off(env, node_type=NodeType.SKALE, node_mode=node_mode) +def cleanup( + node_mode: NodeMode, compose_env: dict, schain_name: Optional[str] = None, prune: bool = False +) -> None: + turn_off(compose_env, node_type=NodeType.SKALE, node_mode=node_mode) if prune: system_prune() if node_mode == NodeMode.PASSIVE: - schain_name = env['SCHAIN_NAME'] cleanup_no_lvm_datadir(chain_name=schain_name) else: cleanup_lvm_datadir() diff --git a/node_cli/operations/common.py b/node_cli/operations/common.py index 7c876fa8..c595a3b8 100644 --- a/node_cli/operations/common.py +++ b/node_cli/operations/common.py @@ -19,7 +19,6 @@ import logging import os -import secrets import shutil import stat import tarfile @@ -27,7 +26,6 @@ from node_cli.configs import ( FILEBEAT_CONFIG_PATH, - FLASK_SECRET_KEY_FILE, G_CONF_HOME, SRC_FILEBEAT_CONFIG_PATH, ) @@ -43,17 +41,6 @@ def configure_filebeat(): logger.info('Filebeat configured') -def configure_flask(): - if os.path.isfile(FLASK_SECRET_KEY_FILE): - logger.info('Flask secret key already exists') - else: - logger.info('Generating Flask secret key...') - flask_secret_key = secrets.token_urlsafe(16) - with open(FLASK_SECRET_KEY_FILE, 'w') as f: - f.write(flask_secret_key) - logger.info('Flask secret key generated and saved') - - def unpack_backup_archive(backup_path: str) -> None: logger.info('Unpacking backup archive...') with tarfile.open(backup_path) as tar: diff --git a/node_cli/operations/config_repo.py b/node_cli/operations/config_repo.py index b8456db6..19cf284a 100644 --- a/node_cli/operations/config_repo.py +++ b/node_cli/operations/config_repo.py @@ -33,12 +33,13 @@ logger = logging.getLogger(__name__) -def update_images(env: dict, node_type: NodeType, node_mode: NodeMode) -> None: - local = env.get('CONTAINER_CONFIGS_DIR') != '' - if local: - compose_build(env=env, node_type=node_type, node_mode=node_mode) +def update_images( + compose_env: dict, container_configs_dir: str, node_type: NodeType, node_mode: NodeMode +) -> None: + if container_configs_dir: + compose_build(env=compose_env, node_type=node_type, node_mode=node_mode) else: - compose_pull(env=env, node_type=node_type, node_mode=node_mode) + compose_pull(env=compose_env, node_type=node_type, node_mode=node_mode) def download_skale_node(stream: Optional[str] = None, src: Optional[str] = None) -> None: diff --git a/node_cli/operations/docker_lvmpy.py b/node_cli/operations/docker_lvmpy.py index c21328f8..eb307d55 100644 --- a/node_cli/operations/docker_lvmpy.py +++ b/node_cli/operations/docker_lvmpy.py @@ -56,12 +56,10 @@ def sync_docker_lvmpy_repo(env): sync_repo(DOCKER_LVMPY_REPO_URL, DOCKER_LVMPY_PATH, env['DOCKER_LVMPY_VERSION']) -def lvmpy_install(env): +def lvmpy_install(block_device: str) -> None: ensure_filestorage_mapping() logging.info('Configuring and starting lvmpy') - setup_lvmpy( - block_device=env['BLOCK_DEVICE'], volume_group=VOLUME_GROUP, exec_start=LVMPY_RUN_CMD - ) + setup_lvmpy(block_device=block_device, volume_group=VOLUME_GROUP, exec_start=LVMPY_RUN_CMD) init_healing_cron() logger.info('docker-lvmpy is configured and started') diff --git a/node_cli/operations/fair.py b/node_cli/operations/fair.py index f1e62305..3a48fdbd 100644 --- a/node_cli/operations/fair.py +++ b/node_cli/operations/fair.py @@ -23,6 +23,9 @@ import distro +from skale_core.settings import BaseNodeSettings, FairBaseSettings, FairSettings, get_settings +from skale_core.types import EnvType + from node_cli.cli.info import TYPE, VERSION from node_cli.configs import ( CONTAINER_CONFIG_PATH, @@ -33,7 +36,7 @@ from node_cli.core.checks import CheckType from node_cli.core.checks import run_checks as run_host_checks from node_cli.core.docker_config import cleanup_docker_configuration, configure_docker -from node_cli.core.host import ensure_btrfs_kernel_module_autoloaded, link_env_file, prepare_host +from node_cli.core.host import ensure_btrfs_kernel_module_autoloaded, prepare_host from node_cli.core.nftables import configure_nftables from node_cli.core.nginx import generate_nginx_config from node_cli.core.schains import cleanup_no_lvm_datadir @@ -46,7 +49,7 @@ ) from node_cli.migrations.fair.from_boot import migrate_nftables_from_boot from node_cli.operations.base import checked_host, turn_off -from node_cli.operations.common import configure_filebeat, configure_flask, unpack_backup_archive +from node_cli.operations.common import configure_filebeat, unpack_backup_archive from node_cli.operations.config_repo import ( sync_skale_node, update_images, @@ -66,10 +69,11 @@ system_prune, wait_for_container, ) -from node_cli.utils.helper import cleanup_dir_content, rm_dir, str_to_bool +from node_cli.utils.helper import cleanup_dir_content, rm_dir from node_cli.utils.meta import FairCliMetaManager from node_cli.utils.print_formatters import print_failed_requirements_checks from node_cli.utils.node_type import NodeMode, NodeType +from node_cli.utils.settings import save_internal_settings logger = logging.getLogger(__name__) @@ -81,42 +85,56 @@ class FairUpdateType(Enum): @checked_host -def init_fair_boot(env_filepath: str, env: dict) -> None: +def init_fair_boot( + settings: BaseNodeSettings, + compose_env: dict, + node_mode: NodeMode, +) -> None: sync_skale_node() - cleanup_volume_artifacts(env['BLOCK_DEVICE']) + cleanup_volume_artifacts(settings.block_device) ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) - prepare_host(env_filepath, env_type=env['ENV_TYPE']) - link_env_file() + prepare_host(env_type=settings.env_type) + save_internal_settings(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) mark_active_node() configure_filebeat() - configure_flask() generate_nginx_config() - prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True') + fair_settings = get_settings((FairSettings, FairBaseSettings)) + prepare_block_device(settings.block_device, force=fair_settings.enforce_btrfs) meta_manager = FairCliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, distro.id(), distro.version(), ) - update_images(env=env, node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.FAIR, + node_mode=NodeMode.ACTIVE, + ) - compose_up(env=env, node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE, is_fair_boot=True) + compose_up( + env=compose_env, + settings=settings, + node_type=NodeType.FAIR, + node_mode=NodeMode.ACTIVE, + is_fair_boot=True, + ) @checked_host def init( - env_filepath: str, - env: dict, + settings: BaseNodeSettings, + compose_env: dict, node_mode: NodeMode, indexer: bool, archive: bool, @@ -124,24 +142,33 @@ def init( ) -> bool: sync_skale_node() ensure_btrfs_kernel_module_autoloaded() - cleanup_volume_artifacts(env['BLOCK_DEVICE']) + cleanup_volume_artifacts(settings.block_device) - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() configure_nftables() configure_filebeat() - configure_flask() generate_nginx_config() - prepare_host(env_filepath, env_type=env['ENV_TYPE']) - link_env_file() + prepare_host(env_type=settings.env_type) + save_internal_settings(node_type=NodeType.FAIR, node_mode=node_mode) - prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True') + fair_settings = get_settings((FairSettings, FairBaseSettings)) + prepare_block_device(settings.block_device, force=fair_settings.enforce_btrfs) - update_images(env=env, node_type=NodeType.FAIR, node_mode=node_mode) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.FAIR, + node_mode=node_mode, + ) compose_up( - env=env, node_type=NodeType.FAIR, node_mode=node_mode, services=list(REDIS_SERVICE_DICT) + env=compose_env, + settings=settings, + node_type=NodeType.FAIR, + node_mode=node_mode, + services=list(REDIS_SERVICE_DICT), ) upsert_node_mode(node_mode=node_mode) @@ -151,129 +178,159 @@ def init( if snapshot: logger.info('Waiting %s seconds for redis to start', REDIS_START_TIMEOUT) time.sleep(REDIS_START_TIMEOUT) - trigger_skaled_snapshot_mode(env=env, snapshot_from=snapshot) + trigger_skaled_snapshot_mode(env_type=settings.env_type, snapshot_from=snapshot) meta_manager = FairCliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, distro.id(), distro.version(), ) - compose_up(env=env, node_type=NodeType.FAIR, node_mode=node_mode) + compose_up(env=compose_env, settings=settings, node_type=NodeType.FAIR, node_mode=node_mode) wait_for_container(BASE_PASSIVE_FAIR_COMPOSE_SERVICES['api']) time.sleep(REDIS_START_TIMEOUT) return True @checked_host -def update_fair_boot(env_filepath: str, env: dict, node_mode: NodeMode = NodeMode.ACTIVE) -> bool: - compose_rm(node_type=NodeType.FAIR, node_mode=node_mode, env=env) +def update_fair_boot( + settings: BaseNodeSettings, + compose_env: dict, + node_mode: NodeMode = NodeMode.ACTIVE, +) -> bool: + compose_rm(node_type=NodeType.FAIR, node_mode=node_mode, env=compose_env) remove_dynamic_containers() - cleanup_volume_artifacts(env['BLOCK_DEVICE']) + cleanup_volume_artifacts(settings.block_device) sync_skale_node() ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) + configure_nftables(enable_monitoring=settings.monitoring_containers) generate_nginx_config() - prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True') + fair_settings = get_settings((FairSettings, FairBaseSettings)) + prepare_block_device(settings.block_device, force=fair_settings.enforce_btrfs) - prepare_host(env_filepath, env['ENV_TYPE']) + prepare_host(settings.env_type) + save_internal_settings(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) meta_manager = FairCliMetaManager() current_stream = meta_manager.get_meta_info().config_stream - skip_cleanup = env.get('SKIP_DOCKER_CLEANUP') == 'True' - if not skip_cleanup and current_stream != env['NODE_VERSION']: + if not settings.skip_docker_cleanup and current_stream != settings.node_version: logger.info( 'Stream version was changed from %s to %s', current_stream, - env['NODE_VERSION'], + settings.node_version, ) docker_cleanup() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, distro.id(), distro.version(), ) - update_images(env=env, node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) - compose_up(env=env, node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE, is_fair_boot=True) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.FAIR, + node_mode=NodeMode.ACTIVE, + ) + compose_up( + env=compose_env, + settings=settings, + node_type=NodeType.FAIR, + node_mode=NodeMode.ACTIVE, + is_fair_boot=True, + ) return True @checked_host def update( - env_filepath: str, - env: dict, + settings: BaseNodeSettings, + compose_env: dict, node_mode: NodeMode, update_type: FairUpdateType, force_skaled_start: bool, ) -> bool: - compose_rm(node_type=NodeType.FAIR, node_mode=node_mode, env=env) + compose_rm(node_type=NodeType.FAIR, node_mode=node_mode, env=compose_env) if update_type not in (FairUpdateType.INFRA_ONLY, FairUpdateType.FROM_BOOT): remove_dynamic_containers() sync_skale_node() ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() configure_nftables() generate_nginx_config() - prepare_host(env_filepath, env['ENV_TYPE'], allocation=True) + prepare_host(settings.env_type, allocation=True) + save_internal_settings(node_type=NodeType.FAIR, node_mode=node_mode) meta_manager = FairCliMetaManager() current_stream = meta_manager.get_meta_info().config_stream - skip_cleanup = env.get('SKIP_DOCKER_CLEANUP') == 'True' - if not skip_cleanup and current_stream != env['NODE_VERSION']: + if not settings.skip_docker_cleanup and current_stream != settings.node_version: logger.info( 'Stream version was changed from %s to %s', current_stream, - env['NODE_VERSION'], + settings.node_version, ) docker_cleanup() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, distro.id(), distro.version(), ) - fair_chain_name = get_fair_chain_name(env) + fair_chain_name = get_fair_chain_name(settings.env_type) if update_type == FairUpdateType.FROM_BOOT: migrate_nftables_from_boot(chain_name=fair_chain_name) - update_images(env=env, node_type=NodeType.FAIR, node_mode=node_mode) + update_images( + compose_env=compose_env, + container_configs_dir=settings.container_configs_dir, + node_type=NodeType.FAIR, + node_mode=node_mode, + ) compose_up( - env=env, node_type=NodeType.FAIR, node_mode=node_mode, services=list(REDIS_SERVICE_DICT) + env=compose_env, + settings=settings, + node_type=NodeType.FAIR, + node_mode=node_mode, + services=list(REDIS_SERVICE_DICT), ) wait_for_container(REDIS_SERVICE_DICT['redis']) time.sleep(REDIS_START_TIMEOUT) if update_type == FairUpdateType.FROM_BOOT: - migrate_chain_record(env) - update_chain_record(env, force_skaled_start=force_skaled_start) - compose_up(env=env, node_type=NodeType.FAIR, node_mode=node_mode) + migrate_chain_record(settings.env_type, settings.node_version) + update_chain_record(settings.env_type, force_skaled_start=force_skaled_start) + compose_up(env=compose_env, settings=settings, node_type=NodeType.FAIR, node_mode=node_mode) return True -def restore(node_mode: NodeMode, env, backup_path, config_only=False): +def restore( + node_mode: NodeMode, + settings: BaseNodeSettings, + compose_env: dict, + backup_path: str, + config_only: bool = False, +) -> bool: unpack_backup_archive(backup_path) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, node_mode, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, check_type=CheckType.PREINSTALL, ) @@ -283,30 +340,27 @@ def restore(node_mode: NodeMode, env, backup_path, config_only=False): ensure_btrfs_kernel_module_autoloaded() - if env.get('SKIP_DOCKER_CONFIG') != 'True': + if not settings.skip_docker_config: configure_docker() - enable_monitoring = str_to_bool(env.get('MONITORING_CONTAINERS', 'False')) - configure_nftables(enable_monitoring=enable_monitoring) - - link_env_file() + configure_nftables(enable_monitoring=settings.monitoring_containers) meta_manager = FairCliMetaManager() meta_manager.update_meta( VERSION, - env['NODE_VERSION'], + settings.node_version, distro.id(), distro.version(), ) if not config_only: - compose_up(env=env, node_type=NodeType.FAIR, node_mode=node_mode) + compose_up(env=compose_env, settings=settings, node_type=NodeType.FAIR, node_mode=node_mode) failed_checks = run_host_checks( - env['BLOCK_DEVICE'], + settings.block_device, TYPE, node_mode, - env['ENV_TYPE'], + settings.env_type, CONTAINER_CONFIG_PATH, check_type=CheckType.POSTINSTALL, ) @@ -316,8 +370,8 @@ def restore(node_mode: NodeMode, env, backup_path, config_only=False): return True -def cleanup(node_mode: NodeMode, env: dict, prune: bool = False) -> None: - turn_off(env, node_type=NodeType.FAIR, node_mode=node_mode) +def cleanup(node_mode: NodeMode, compose_env: dict, prune: bool = False) -> None: + turn_off(compose_env, node_type=NodeType.FAIR, node_mode=node_mode) if prune: system_prune() cleanup_no_lvm_datadir() @@ -327,15 +381,15 @@ def cleanup(node_mode: NodeMode, env: dict, prune: bool = False) -> None: cleanup_docker_configuration() -def trigger_skaled_snapshot_mode(env: dict, snapshot_from: str = 'any') -> None: - record = get_fair_chain_record(env) +def trigger_skaled_snapshot_mode(env_type: EnvType, snapshot_from: str = 'any') -> None: + record = get_fair_chain_record(env_type) if not snapshot_from: snapshot_from = 'any' logger.info('Triggering skaled snapshot mode, snapshot_from: %s', snapshot_from) record.set_snapshot_from(snapshot_from) -def repair(env: dict, snapshot_from: str = 'any') -> None: +def repair(env_type: EnvType, snapshot_from: str = 'any') -> None: logger.info('Starting fair node repair') container_name = 'sk_admin' if is_admin_running(): @@ -346,7 +400,7 @@ def repair(env: dict, snapshot_from: str = 'any') -> None: logger.info('Cleaning up datadir') cleanup_no_lvm_datadir() logger.info('Requesting fair node repair') - trigger_skaled_snapshot_mode(env=env, snapshot_from=snapshot_from) + trigger_skaled_snapshot_mode(env_type=env_type, snapshot_from=snapshot_from) logger.info('Starting admin') start_container_by_name(container_name=container_name) logger.info('Fair node repair completed successfully') diff --git a/node_cli/utils/docker_utils.py b/node_cli/utils/docker_utils.py index 695a1253..98a4946c 100644 --- a/node_cli/utils/docker_utils.py +++ b/node_cli/utils/docker_utils.py @@ -29,15 +29,16 @@ from docker.errors import NotFound from docker.models.containers import Container +from skale_core.settings import BaseNodeSettings + from node_cli.configs import ( COMPOSE_PATH, FAIR_COMPOSE_PATH, NGINX_CONTAINER_NAME, REMOVED_CONTAINERS_FOLDER_PATH, - SGX_CERTIFICATES_DIR_NAME, ) from node_cli.core.node_options import active_fair, active_skale, passive_fair, passive_skale -from node_cli.utils.helper import run_cmd, str_to_bool +from node_cli.utils.helper import run_cmd from node_cli.utils.node_type import NodeMode, NodeType logger = logging.getLogger(__name__) @@ -327,6 +328,7 @@ def get_up_compose_cmd( def compose_up( env, + settings: BaseNodeSettings, node_type: NodeType, node_mode: NodeMode, is_fair_boot: bool = False, @@ -338,9 +340,6 @@ def compose_up( run_cmd(cmd=get_up_compose_cmd(node_type=node_type, node_mode=node_mode), env=env) return - if 'SGX_CERTIFICATES_DIR_NAME' not in env: - env['SGX_CERTIFICATES_DIR_NAME'] = SGX_CERTIFICATES_DIR_NAME - if active_fair(node_type, node_mode): logger.info('Running fair base set of containers') if is_fair_boot: @@ -368,7 +367,7 @@ def compose_up( logger.debug('Launching skale node containers with env %s', env) run_cmd(cmd=get_up_compose_cmd(node_type=node_type, node_mode=node_mode), env=env) - if 'TG_API_KEY' in env and 'TG_CHAT_ID' in env: + if settings.tg_api_key and settings.tg_chat_id: logger.info('Running containers for Telegram notifications') run_cmd( cmd=get_up_compose_cmd( @@ -379,7 +378,7 @@ def compose_up( env=env, ) - if str_to_bool(env.get('MONITORING_CONTAINERS', 'False')): + if settings.monitoring_containers: logger.info('Running monitoring containers') run_cmd( cmd=get_up_compose_cmd( diff --git a/node_cli/utils/helper.py b/node_cli/utils/helper.py index dd1a7151..8a642aee 100644 --- a/node_cli/utils/helper.py +++ b/node_cli/utils/helper.py @@ -30,6 +30,7 @@ import urllib.parse import urllib.request import uuid +from pathlib import Path from functools import wraps from logging import Formatter, StreamHandler from typing import Any, NoReturn, Optional @@ -85,13 +86,13 @@ def write_json(path: str, content: dict) -> None: json.dump(content, outfile, indent=4) -def save_json(path: str, content: dict) -> None: +def save_json(path: str | Path, content: dict) -> None: tmp_path = get_tmp_path(path) write_json(tmp_path, content) shutil.move(tmp_path, path) -def init_file(path, content=None): +def init_file(path: str | Path, content=None): if not os.path.exists(path): write_json(path, content) @@ -143,16 +144,6 @@ def get_username(): return os.environ.get('USERNAME') or os.environ.get('USER') -def str_to_bool(val: str) -> bool: - val = val.lower() - if val in ('y', 'yes', 't', 'true', 'on', '1'): - return True - elif val in ('n', 'no', 'f', 'false', 'off', '0'): - return False - else: - raise ValueError(f'Invalid truth value {val!r}') - - def error_exit(error_payload: Any, exit_code: CLIExitCodes = CLIExitCodes.FAILURE) -> NoReturn: """Print error message and exit the program with specified exit code. @@ -339,7 +330,7 @@ def cleanup_dir_content(folder: str) -> None: shutil.rmtree(file_path) -def safe_mkdir(path: str, print_res: bool = False) -> None: +def safe_mkdir(path: str | Path, print_res: bool = False) -> None: if os.path.exists(path): logger.debug(f'Directory {path} already exists') return @@ -411,8 +402,8 @@ def convert(self, value, param, ctx): IP_TYPE = IpType() -def get_tmp_path(path: str) -> str: - base, ext = os.path.splitext(path) +def get_tmp_path(path: str | Path) -> str: + base, ext = os.path.splitext(str(path)) salt = uuid.uuid4().hex[:5] return base + salt + '.tmp' + ext @@ -425,10 +416,6 @@ def get_ssh_port(ssh_service_name='ssh'): return DEFAULT_SSH_PORT -def is_contract_address(value: str) -> bool: - return bool(re.fullmatch(r'0x[a-fA-F0-9]{40}', value)) - - def is_btrfs_subvolume(path: str) -> bool: """Check if the given path is a Btrfs subvolume.""" try: diff --git a/node_cli/utils/node_type.py b/node_cli/utils/node_type.py index bf3f6d4f..754a1d69 100644 --- a/node_cli/utils/node_type.py +++ b/node_cli/utils/node_type.py @@ -20,9 +20,9 @@ from enum import Enum -class NodeType(Enum): - SKALE = 0 - FAIR = 1 +class NodeType(str, Enum): + SKALE = 'skale' + FAIR = 'fair' class NodeMode(str, Enum): diff --git a/node_cli/utils/print_formatters.py b/node_cli/utils/print_formatters.py index 7edec113..1da07d51 100644 --- a/node_cli/utils/print_formatters.py +++ b/node_cli/utils/print_formatters.py @@ -339,9 +339,6 @@ def format_timestamp(value): return str(value) -1 - - def print_chain_record(record): print( inspect.cleandoc(f""" diff --git a/node_cli/utils/settings.py b/node_cli/utils/settings.py new file mode 100644 index 00000000..8e7f957c --- /dev/null +++ b/node_cli/utils/settings.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# This file is part of node-cli +# +# Copyright (C) 2026 SKALE Labs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import tomllib + +from dotenv.main import DotEnv + +from skale_core.settings import ( + SETTINGS_MAP, + BaseNodeSettings, + FairBaseSettings, + FairSettings, + InternalSettings, + SkalePassiveSettings, + SkaleSettings, + write_internal_settings_file, + write_node_settings_file, +) + +from node_cli.configs import INTERNAL_SETTINGS_PATH, NODE_SETTINGS_PATH, SKALE_DIR +from node_cli.utils.node_type import NodeMode, NodeType + +InternalSettings.model_config['toml_file'] = INTERNAL_SETTINGS_PATH +SkaleSettings.model_config['toml_file'] = NODE_SETTINGS_PATH +SkalePassiveSettings.model_config['toml_file'] = NODE_SETTINGS_PATH +FairSettings.model_config['toml_file'] = NODE_SETTINGS_PATH +FairBaseSettings.model_config['toml_file'] = NODE_SETTINGS_PATH + + +def load_config_file(filepath: str) -> dict: + if filepath.endswith('.toml'): + with open(filepath, 'rb') as f: + return tomllib.load(f) + return {k.lower(): v for k, v in DotEnv(filepath).dict().items()} + + +def validate_and_save_node_settings( + config_filepath: str, + node_type: NodeType, + node_mode: NodeMode, +) -> BaseNodeSettings: + data = load_config_file(config_filepath) + settings_type = SETTINGS_MAP[(node_type.value, node_mode.value)] + write_node_settings_file(path=NODE_SETTINGS_PATH, settings_type=settings_type, data=data) + return settings_type() + + +def save_internal_settings( + node_type: NodeType, + node_mode: NodeMode, + backup_run: bool = False, + pull_config_for_schain: str | None = None, +) -> None: + data = { + 'node_type': node_type.value, + 'node_mode': node_mode.value, + 'skale_dir_host': str(SKALE_DIR), + 'backup_run': backup_run, + 'pull_config_for_schain': pull_config_for_schain, + } + write_internal_settings_file(path=INTERNAL_SETTINGS_PATH, data=data) diff --git a/pyproject.toml b/pyproject.toml index cf813bfa..4ab3258d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,15 +10,13 @@ readme = "README.md" requires-python = ">=3.13" license = { file = "LICENSE" } keywords = ["skale", "cli"] -authors = [ - { name = "SKALE Labs", email = "support@skalelabs.com" } -] +authors = [{ name = "SKALE Labs", email = "support@skalelabs.com" }] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Affero General Public License v3", - "Natural Language :: English", - "Programming Language :: Python :: 3.13", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Natural Language :: English", + "Programming Language :: Python :: 3.13", ] dependencies = [ @@ -40,13 +38,14 @@ dependencies = [ "MarkupSafe==3.0.3", "Flask==3.1.2", "itsdangerous==2.2.0", - "cryptography==46.0.3", + "cryptography==46.0.5", "filelock==3.20.0", "sh==2.2.2", "python-crontab==3.3.0", "requests-mock==1.12.1", - "redis==7.1.0", - "PyInstaller==6.16.0", + "redis==7.1.1", + "PyInstaller==6.18.0", + "skale.py-core==7.13.dev1", ] [project.urls] @@ -78,11 +77,13 @@ target-version = "py313" [tool.ruff.format] quote-style = "single" +[tool.uv] +prerelease = "allow" + + [tool.pytest.ini_options] log_cli = false log_cli_level = "INFO" log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" log_cli_date_format = "%Y-%m-%d %H:%M:%S" -filterwarnings = [ - "ignore::DeprecationWarning", -] +filterwarnings = ["ignore::DeprecationWarning"] diff --git a/scripts/export_env.sh b/scripts/export_env.sh new file mode 100644 index 00000000..af30b4ab --- /dev/null +++ b/scripts/export_env.sh @@ -0,0 +1,8 @@ +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +PROJECT_DIR=$(dirname $DIR) + +export LVMPY_LOG_DIR="$PROJECT_DIR/tests/" +export HIDE_STREAM_LOG=true +export TEST_HOME_DIR="$PROJECT_DIR/tests/" +export GLOBAL_SKALE_DIR="$PROJECT_DIR/tests/etc/skale" +export DOTENV_FILEPATH='tests/test-env' \ No newline at end of file diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index efc72c6c..1d3a3fe6 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -3,9 +3,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR=$(dirname $DIR) -LVMPY_LOG_DIR="$PROJECT_DIR/tests/" \ - HIDE_STREAM_LOG=true \ - TEST_HOME_DIR="$PROJECT_DIR/tests/" \ - GLOBAL_SKALE_DIR="$PROJECT_DIR/tests/etc/skale" \ - DOTENV_FILEPATH='tests/test-env' \ - py.test --cov=$PROJECT_DIR/ --ignore=tests/core/nftables_test.py --ignore=tests/core/migration_test.py tests/ $@ +. "$DIR/export_env.sh" + +py.test --cov=$PROJECT_DIR/ --ignore=tests/core/nftables_test.py --ignore=tests/core/migration_test.py tests/ $@ diff --git a/tests/.skale/config/docker-compose-fair.yml b/tests/.skale/config/docker-compose-fair.yml new file mode 100644 index 00000000..c09f2c08 --- /dev/null +++ b/tests/.skale/config/docker-compose-fair.yml @@ -0,0 +1,5 @@ +services: + test: + container_name: test + image: alpine:latest + network_mode: host diff --git a/tests/cli/fair_cli_test.py b/tests/cli/fair_cli_test.py index 3c6b9ef5..14e2aee9 100644 --- a/tests/cli/fair_cli_test.py +++ b/tests/cli/fair_cli_test.py @@ -20,6 +20,7 @@ from tests.helper import run_command, subprocess_run_mock from tests.resources_test import BIG_DISK_SIZE + @mock.patch('node_cli.cli.fair_node.restore_fair') def test_fair_node_restore(mock_restore_core, valid_env_file, tmp_path): runner = CliRunner() @@ -105,7 +106,7 @@ def test_fair_node_migrate(mock_migrate_core, valid_env_file): result = runner.invoke(migrate_node, ['--yes', valid_env_file]) assert result.exit_code == 0, f'Output: {result.output}\nException: {result.exception}' - mock_migrate_core.assert_called_once_with(env_filepath=valid_env_file) + mock_migrate_core.assert_called_once_with(config_file=valid_env_file) @mock.patch('node_cli.cli.fair_node.exit_fair') @@ -136,4 +137,5 @@ def test_cleanup_node(mocked_g_config, inited_node): result = run_command(cleanup_node, ['--yes']) assert result.exit_code == 0 cleanup_mock.assert_called_once_with( - node_mode=NodeMode.ACTIVE, prune=False, env={'SCHAIN_NAME': 'test'}) + node_mode=NodeMode.ACTIVE, prune=False, compose_env={'SCHAIN_NAME': 'test'} + ) diff --git a/tests/cli/fair_passive_node_test.py b/tests/cli/fair_passive_node_test.py index a8b7818a..00c61ca0 100644 --- a/tests/cli/fair_passive_node_test.py +++ b/tests/cli/fair_passive_node_test.py @@ -15,15 +15,12 @@ init_default_logger() -def test_init_fair_passive(mocked_g_config, tmp_path): - env_file = tmp_path / 'test-env' - env_file.write_text('') +def test_init_fair_passive(mocked_g_config, fair_passive_settings, fair_passive_user_conf): pathlib.Path(SKALE_DIR).mkdir(parents=True, exist_ok=True) with ( mock.patch('subprocess.run', new=subprocess_run_mock), mock.patch('node_cli.fair.common.init_fair_op', return_value=True), mock.patch('node_cli.fair.common.compose_node_env', return_value={}), - mock.patch('node_cli.fair.common.save_env_params'), mock.patch('node_cli.fair.passive.setup_fair_passive'), mock.patch('node_cli.fair.common.time.sleep'), mock.patch('node_cli.core.node.is_base_containers_alive', return_value=True), @@ -34,7 +31,7 @@ def test_init_fair_passive(mocked_g_config, tmp_path): result = run_command( init_passive_node, [ - env_file.as_posix(), + fair_passive_user_conf.as_posix(), '--id', '1', ], @@ -42,15 +39,14 @@ def test_init_fair_passive(mocked_g_config, tmp_path): assert result.exit_code == 0 -def test_init_fair_passive_snapshot_any(mocked_g_config, tmp_path): - env_file = tmp_path / 'test-env' - env_file.write_text('') +def test_init_fair_passive_snapshot_any( + mocked_g_config, fair_passive_settings, fair_passive_user_conf +): pathlib.Path(SKALE_DIR).mkdir(parents=True, exist_ok=True) with ( mock.patch('subprocess.run', new=subprocess_run_mock), mock.patch('node_cli.fair.common.init_fair_op', return_value=True), mock.patch('node_cli.fair.common.compose_node_env', return_value={}), - mock.patch('node_cli.fair.common.save_env_params'), mock.patch('node_cli.fair.passive.setup_fair_passive'), mock.patch('node_cli.fair.common.time.sleep'), mock.patch('node_cli.core.node.is_base_containers_alive', return_value=True), @@ -61,7 +57,7 @@ def test_init_fair_passive_snapshot_any(mocked_g_config, tmp_path): result = run_command( init_passive_node, [ - env_file.as_posix(), + fair_passive_user_conf.as_posix(), '--id', '2', '--snapshot', @@ -71,9 +67,9 @@ def test_init_fair_passive_snapshot_any(mocked_g_config, tmp_path): assert result.exit_code == 0 -def test_update_fair_passive(mocked_g_config, tmp_path, clean_node_options): - env_file = tmp_path / 'test-env' - env_file.write_text('') +def test_update_fair_passive( + mocked_g_config, fair_passive_settings, fair_passive_user_conf, clean_node_options +): pathlib.Path(NODE_DATA_PATH).mkdir(parents=True, exist_ok=True) with ( mock.patch('subprocess.run', new=subprocess_run_mock), @@ -84,7 +80,7 @@ def test_update_fair_passive(mocked_g_config, tmp_path, clean_node_options): mock.patch('node_cli.operations.base.configure_nftables'), mock.patch('node_cli.utils.decorators.is_node_inited', return_value=True), ): - result = run_command(update_node, [env_file.as_posix(), '--yes']) + result = run_command(update_node, [fair_passive_user_conf.as_posix(), '--yes']) assert result.exit_code == 0 @@ -107,4 +103,5 @@ def test_cleanup_node(mocked_g_config): result = run_command(cleanup_node, ['--yes']) assert result.exit_code == 0 cleanup_mock.assert_called_once_with( - node_mode=NodeMode.PASSIVE, prune=False, env={'SCHAIN_NAME': 'test'}) + node_mode=NodeMode.PASSIVE, compose_env={'SCHAIN_NAME': 'test'}, prune=False + ) diff --git a/tests/cli/node_test.py b/tests/cli/node_test.py index b31cddc0..a0c064f0 100644 --- a/tests/cli/node_test.py +++ b/tests/cli/node_test.py @@ -347,18 +347,17 @@ def test_restore(request, node_type, node_mode, test_user_conf, mocked_g_config, return_value=CliMeta(version='2.4.0', config_stream='3.0.2'), ), patch('node_cli.operations.base.configure_nftables'), - patch('node_cli.configs.user.validate_alias_or_address'), ): user_conf_path = request.getfixturevalue(test_user_conf).as_posix() result = run_command(restore_node, [backup_path, user_conf_path]) assert result.exit_code == 0 assert 'Node is restored from backup\n' in result.output # noqa - assert mock_restore_op.call_args[0][0].get('BACKUP_RUN') == 'True' + assert mock_restore_op.call_args.kwargs.get('backup_run') is True result = run_command(restore_node, [backup_path, user_conf_path, '--no-snapshot']) assert result.exit_code == 0 assert 'Node is restored from backup\n' in result.output # noqa - assert mock_restore_op.call_args[0][0].get('BACKUP_RUN') is None + assert mock_restore_op.call_args.kwargs.get('backup_run') is False def test_maintenance_on(): @@ -385,14 +384,12 @@ def test_maintenance_off(mocked_g_config): ) -def test_turn_off_maintenance_on(mocked_g_config, regular_user_conf, active_node_option): +def test_turn_off_maintenance_on(mocked_g_config, regular_user_conf, active_node_option, skale_active_settings): resp_mock = response_mock(requests.codes.ok, {'status': 'ok', 'payload': None}) with ( mock.patch('subprocess.run', new=subprocess_run_mock), - mock.patch('node_cli.core.node.SKALE_DIR_ENV_FILEPATH', regular_user_conf.as_posix()), mock.patch('node_cli.core.node.turn_off_op'), mock.patch('node_cli.utils.decorators.is_node_inited', return_value=True), - mock.patch('node_cli.configs.user.validate_alias_or_address'), mock.patch('node_cli.cli.node.TYPE', NodeType.SKALE), ): result = run_command_mock( @@ -418,15 +415,13 @@ def test_turn_off_maintenance_on(mocked_g_config, regular_user_conf, active_node assert result.exit_code == CLIExitCodes.UNSAFE_UPDATE -def test_turn_on_maintenance_off(mocked_g_config, regular_user_conf, active_node_option): +def test_turn_on_maintenance_off(mocked_g_config, regular_user_conf, active_node_option, skale_active_settings): resp_mock = response_mock(requests.codes.ok, {'status': 'ok', 'payload': None}) with ( mock.patch('subprocess.run', new=subprocess_run_mock), - mock.patch('node_cli.core.node.get_flask_secret_key'), mock.patch('node_cli.core.node.turn_on_op'), mock.patch('node_cli.core.node.is_base_containers_alive'), mock.patch('node_cli.utils.decorators.is_node_inited', return_value=True), - mock.patch('node_cli.configs.user.validate_alias_or_address'), mock.patch('node_cli.cli.node.TYPE', NodeType.SKALE), ): result = run_command_mock( @@ -475,6 +470,7 @@ def test_node_version(meta_file_v2): == "{'version': '0.1.1', 'config_stream': 'develop', 'docker_lvmpy_version': '1.1.2'}\n" ) + def test_cleanup_node(mocked_g_config): pathlib.Path(SKALE_DIR).mkdir(parents=True, exist_ok=True) @@ -493,4 +489,4 @@ def test_cleanup_node(mocked_g_config): ): result = run_command(cleanup_node, ['--yes']) assert result.exit_code == 0 - cleanup_mock.assert_called_once_with(node_mode=NodeMode.ACTIVE, prune=False, env={}) \ No newline at end of file + cleanup_mock.assert_called_once_with(node_mode=NodeMode.ACTIVE, prune=False, compose_env={}) diff --git a/tests/cli/passive_node_test.py b/tests/cli/passive_node_test.py index c9ea9527..e8419220 100644 --- a/tests/cli/passive_node_test.py +++ b/tests/cli/passive_node_test.py @@ -45,7 +45,6 @@ def test_init_passive(mocked_g_config, clean_node_options, passive_user_conf): mock.patch('node_cli.core.resources.get_disk_size', return_value=BIG_DISK_SIZE), mock.patch('node_cli.operations.base.configure_nftables'), mock.patch('node_cli.utils.decorators.is_node_inited', return_value=False), - mock.patch('node_cli.configs.user.validate_alias_or_address'), ): result = run_command(_init_passive, [passive_user_conf.as_posix()]) @@ -66,9 +65,11 @@ def test_init_passive_archive(mocked_g_config, clean_node_options, passive_user_ mock.patch('node_cli.operations.base.sync_skale_node'), mock.patch('node_cli.operations.base.configure_docker'), mock.patch('node_cli.operations.base.prepare_host'), + mock.patch('node_cli.operations.base.save_internal_settings'), + mock.patch('node_cli.operations.base.run_host_checks', return_value=[]), mock.patch('node_cli.operations.base.ensure_filestorage_mapping'), - mock.patch('node_cli.operations.base.link_env_file'), mock.patch('node_cli.operations.base.generate_nginx_config'), + mock.patch('node_cli.operations.base.get_settings'), mock.patch('node_cli.operations.base.prepare_block_device'), mock.patch('node_cli.operations.base.CliMetaManager.update_meta'), mock.patch('node_cli.operations.base.update_resource_allocation'), @@ -77,7 +78,6 @@ def test_init_passive_archive(mocked_g_config, clean_node_options, passive_user_ mock.patch('node_cli.core.resources.get_disk_size', return_value=BIG_DISK_SIZE), mock.patch('node_cli.operations.base.configure_nftables'), mock.patch('node_cli.utils.decorators.is_node_inited', return_value=False), - mock.patch('node_cli.configs.user.validate_alias_or_address'), mock.patch('node_cli.cli.node.TYPE', NodeType.SKALE), ): result = run_command(_init_passive, [passive_user_conf.as_posix(), '--archive']) @@ -121,7 +121,6 @@ def test_update_passive(passive_user_conf, mocked_g_config): 'node_cli.core.node.CliMetaManager.get_meta_info', return_value=CliMeta(version='2.6.0', config_stream='3.0.2'), ), - mock.patch('node_cli.configs.user.validate_alias_or_address'), ): result = run_command(_update_passive, [passive_user_conf.as_posix(), '--yes']) assert result.exit_code == 0 @@ -146,4 +145,5 @@ def test_cleanup_node(mocked_g_config): result = run_command(cleanup_node, ['--yes']) assert result.exit_code == 0 cleanup_mock.assert_called_once_with( - node_mode=NodeMode.PASSIVE, prune=False, env={'SCHAIN_NAME': 'test'}) + node_mode=NodeMode.PASSIVE, prune=False, compose_env={'SCHAIN_NAME': 'test'} + ) diff --git a/tests/configs/configs_env_validate_test.py b/tests/configs/configs_env_validate_test.py deleted file mode 100644 index 2db057ff..00000000 --- a/tests/configs/configs_env_validate_test.py +++ /dev/null @@ -1,189 +0,0 @@ -import os -from typing import Optional - -import pytest -import requests - -from node_cli.configs.alias_address_validation import ( - ContractType, - get_chain_id, - get_network_metadata, - validate_alias_or_address, - validate_contract_address, - validate_contract_alias, -) -from node_cli.configs.user import ( - ALLOWED_ENV_TYPES, - FairBootUserConfig, - FairUserConfig, - PassiveFairUserConfig, - SkaleUserConfig, - PassiveSkaleUserConfig, - get_user_config_class, - get_validated_user_config, - validate_env_type, -) -from node_cli.utils.node_type import NodeType, NodeMode - -ENDPOINT = 'http://localhost:8545' - - -class FakeResponse: - def __init__(self, status_code: int, json_data: Optional[dict] = None): - self.status_code = status_code - self._json_data = json_data or {} - - def json(self): - return self._json_data - - -@pytest.mark.parametrize( - 'node_type, node_mode, is_fair_boot, expected_type', - [ - (NodeType.SKALE, NodeMode.ACTIVE, False, SkaleUserConfig), - (NodeType.SKALE, NodeMode.PASSIVE, False, PassiveSkaleUserConfig), - (NodeType.FAIR, NodeMode.ACTIVE, True, FairBootUserConfig), - (NodeType.FAIR, NodeMode.ACTIVE, False, FairUserConfig), - (NodeType.FAIR, NodeMode.PASSIVE, False, PassiveFairUserConfig), - ], - ids=['skale_active', 'skale_passive', 'fair_boot', 'fair_active', 'fair_passive'], -) -def test_build_env_params_keys(node_type, node_mode, is_fair_boot, expected_type): - env_type = get_user_config_class( - node_type=node_type, node_mode=node_mode, is_fair_boot=is_fair_boot - ) - assert env_type == expected_type - - -@pytest.mark.parametrize( - 'env_types, should_fail', - [ - (ALLOWED_ENV_TYPES, False), - (['invalid'], True), - ], - ids=[ - 'correct_env', - 'invalid_env', - ], -) -def test_env_types(env_types, should_fail): - for env_type in env_types: - if should_fail: - with pytest.raises(SystemExit): - validate_env_type(env_type=env_type) - else: - validate_env_type(env_type=env_type) - - -def test_get_chain_id_success(monkeypatch): - fake_response = FakeResponse(200, {'result': '0x1'}) - - def fake_post(url, json): - return fake_response - - monkeypatch.setattr(requests, 'post', fake_post) - assert get_chain_id(ENDPOINT) == 1 - - -def test_get_chain_id_failure(monkeypatch): - fake_response = FakeResponse(404) - - def fake_post(url, json): - return fake_response - - monkeypatch.setattr(requests, 'post', fake_post) - with pytest.raises(SystemExit): - get_chain_id(ENDPOINT) - - -@pytest.mark.parametrize( - 'metadata,status_code,should_raise', - [ - ({'networks': [{'chainId': 1, 'path': 'mainnet'}]}, 200, False), - (None, 404, True), - ], -) -def test_get_network_metadata(requests_mock, metadata, status_code, should_raise): - metadata_url = 'https://raw.githubusercontent.com/skalenetwork/skale-contracts/refs/heads/deployments/metadata.json' - requests_mock.get(metadata_url, json=metadata, status_code=status_code) - - if should_raise: - with pytest.raises(SystemExit): - get_network_metadata() - else: - assert get_network_metadata() == metadata - - -@pytest.mark.parametrize( - 'code,should_raise', - [ - ('0x123', False), - ('0x', True), - ], -) -def test_validate_contract_address(requests_mock, code, should_raise): - requests_mock.post(ENDPOINT, json={'result': code}) - addr = '0x' + 'a' * 40 - if should_raise: - with pytest.raises(SystemExit): - validate_contract_address(addr, ENDPOINT) - else: - validate_contract_address(addr, ENDPOINT) - - -@pytest.mark.parametrize( - 'networks,should_raise', - [ - ([{'chainId': 1, 'path': 'mainnet'}], False), - ([], True), - ], -) -def test_validate_contract_alias(requests_mock, networks, should_raise): - requests_mock.post(ENDPOINT, json={'result': '0x1'}) - metadata_url = 'https://raw.githubusercontent.com/skalenetwork/skale-contracts/refs/heads/deployments/metadata.json' - requests_mock.get(metadata_url, json={'networks': networks}, status_code=200) - - if not should_raise: - alias_url = 'https://raw.githubusercontent.com/skalenetwork/skale-contracts/refs/heads/deployments/mainnet/skale-manager/test-alias.json' - requests_mock.get(alias_url, status_code=200) - validate_contract_alias('test-alias', ContractType.MANAGER, ENDPOINT) - else: - with pytest.raises(SystemExit): - validate_contract_alias('test-alias', ContractType.MANAGER, ENDPOINT) - - -def test_validate_env_alias_or_address_with_address(requests_mock): - addr = '0x' + 'b' * 40 - requests_mock.post(ENDPOINT, json={'result': '0x1'}) - validate_alias_or_address(addr, ContractType.IMA, ENDPOINT) - - -def test_validate_env_alias_or_address_with_alias(requests_mock): - requests_mock.post(ENDPOINT, json={'result': '0x1'}) - metadata_url = 'https://raw.githubusercontent.com/skalenetwork/skale-contracts/refs/heads/deployments/metadata.json' - metadata = {'networks': [{'chainId': 1, 'path': 'mainnet'}]} - requests_mock.get(metadata_url, json=metadata, status_code=200) - alias_url = 'https://raw.githubusercontent.com/skalenetwork/skale-contracts/refs/heads/deployments/mainnet/mainnet-ima/test-alias.json' - requests_mock.get(alias_url, status_code=200) - validate_alias_or_address('test-alias', ContractType.IMA, ENDPOINT) - - -def test_get_validated_env_config_missing_file(): - with pytest.raises(SystemExit): - get_validated_user_config( - env_filepath='nonexistent.env', node_type=NodeType.SKALE, node_mode=NodeMode.ACTIVE - ) - - -def test_get_validated_env_config_unreadable_file(tmp_path): - env_file = tmp_path / 'unreadable.env' - env_file.touch() - original_mode = env_file.stat().st_mode - try: - os.chmod(env_file, 0o000) - with pytest.raises(PermissionError): - get_validated_user_config( - env_filepath=str(env_file), node_type=NodeType.SKALE, node_mode=NodeMode.ACTIVE - ) - finally: - os.chmod(env_file, original_mode) diff --git a/tests/conftest.py b/tests/conftest.py index 5434e697..8b610d96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,8 +48,40 @@ from node_cli.utils.docker_utils import docker_client from node_cli.utils.global_config import generate_g_config_file from node_cli.utils.node_type import NodeMode +from tests.fixtures.settings import ( # noqa: F401 + INTERNAL_FAIR_ACTIVE, + INTERNAL_FAIR_PASSIVE, + INTERNAL_SKALE_ACTIVE, + INTERNAL_SKALE_PASSIVE, + NODE_FAIR_ACTIVE, + NODE_FAIR_PASSIVE, + NODE_SKALE_ACTIVE, + NODE_SKALE_PASSIVE, + _cleanup_settings, + _write_settings, + fair_active_settings, + fair_passive_settings, + skale_active_settings, + skale_passive_settings, +) from tests.helper import TEST_META_V1, TEST_META_V2, TEST_META_V3, TEST_SCHAINS_MNT_DIR_SINGLE_CHAIN +TIMEOUT_PATCHES = [ + 'node_cli.configs.TM_INIT_TIMEOUT', + 'node_cli.configs.RESTORE_SLEEP_TIMEOUT', + 'node_cli.configs.INIT_TIMEOUT', +] + + +@pytest.fixture(autouse=True, scope='session') +def _fast_timeouts(): + patchers = [mock.patch(target, 1) for target in TIMEOUT_PATCHES] + for p in patchers: + p.start() + yield + for p in patchers: + p.stop() + @pytest.fixture() def tmp_dir_path(): @@ -290,20 +322,13 @@ def tmp_passive_datadir(): def valid_env_params(): return { 'ENDPOINT': 'http://localhost:8545', - 'IMA_ENDPOINT': 'http://127.0.01', - 'DB_USER': 'user', - 'DB_PASSWORD': 'pass', - 'DB_PORT': '3307', 'NODE_VERSION': 'master', 'FILEBEAT_HOST': '127.0.0.1:3010', - 'SGX_SERVER_URL': 'http://127.0.0.1', + 'SGX_URL': 'http://127.0.0.1', 'BLOCK_DEVICE': '/dev/sss', - 'DOCKER_LVMPY_VERSION': 'master', 'ENV_TYPE': 'devnet', - 'SCHAIN_NAME': 'test', 'ENFORCE_BTRFS': 'False', - 'MANAGER_CONTRACTS': 'test-manager', - 'IMA_CONTRACTS': 'test-ima', + 'FAIR_CONTRACTS': 'test-fair', } @@ -319,6 +344,7 @@ def valid_env_file(valid_env_params): finally: if file_name: os.unlink(file_name) + _cleanup_settings() @pytest.fixture @@ -356,12 +382,13 @@ def set_env_var(name, value): @pytest.fixture def regular_user_conf(tmp_path): test_env_path = pathlib.Path(tmp_path / 'test-env') + _write_settings(INTERNAL_SKALE_ACTIVE, NODE_SKALE_ACTIVE) try: test_env = """ ENDPOINT=http://localhost:8545 NODE_VERSION='main' FILEBEAT_HOST=127.0.0.1:3010 - SGX_SERVER_URL=http://127.0.0.1 + SGX_URL=http://127.0.0.1 BLOCK_DEVICE=/dev/sss DOCKER_LVMPY_VERSION='master' ENV_TYPE='devnet' @@ -373,17 +400,19 @@ def regular_user_conf(tmp_path): yield test_env_path finally: test_env_path.unlink() + _cleanup_settings() @pytest.fixture def fair_user_conf(tmp_path): test_env_path = pathlib.Path(tmp_path / 'test-env') + _write_settings(INTERNAL_FAIR_ACTIVE, NODE_FAIR_ACTIVE) try: test_env = """ - BOOT_ENDPOINT=http://localhost:8545 + ENDPOINT=http://localhost:8545 NODE_VERSION='main' FILEBEAT_HOST=127.0.0.1:3010 - SGX_SERVER_URL=http://127.0.0.1 + SGX_URL=http://127.0.0.1 BLOCK_DEVICE=/dev/sss ENV_TYPE='devnet' ENFORCE_BTRFS=False @@ -394,32 +423,57 @@ def fair_user_conf(tmp_path): yield test_env_path finally: test_env_path.unlink() + _cleanup_settings() @pytest.fixture def fair_boot_user_conf(tmp_path): test_env_path = pathlib.Path(tmp_path / 'test-env') + _write_settings(INTERNAL_FAIR_ACTIVE, NODE_FAIR_ACTIVE) try: test_env = """ ENDPOINT=http://localhost:8545 NODE_VERSION='main' FILEBEAT_HOST=127.0.0.1:3010 - SGX_SERVER_URL=http://127.0.0.1 + SGX_URL=http://127.0.0.1 BLOCK_DEVICE=/dev/sss ENV_TYPE='devnet' - MANAGER_CONTRACTS='test-manager' - IMA_CONTRACTS='test-ima' + FAIR_CONTRACTS='test-fair' + """ + with open(test_env_path, 'w') as env_file: + env_file.write(test_env) + yield test_env_path + finally: + test_env_path.unlink() + _cleanup_settings() + + +@pytest.fixture +def fair_passive_user_conf(tmp_path): + test_env_path = pathlib.Path(tmp_path / 'test-env') + _write_settings(INTERNAL_FAIR_PASSIVE, NODE_FAIR_PASSIVE) + try: + test_env = """ + ENDPOINT=http://localhost:8545 + NODE_VERSION='main' + FILEBEAT_HOST=127.0.0.1:3010 + BLOCK_DEVICE=/dev/sss + ENV_TYPE='devnet' + ENFORCE_BTRFS=False + FAIR_CONTRACTS='test-fair' """ with open(test_env_path, 'w') as env_file: env_file.write(test_env) yield test_env_path finally: test_env_path.unlink() + _cleanup_settings() @pytest.fixture def passive_user_conf(tmp_path): test_env_path = pathlib.Path(tmp_path / 'test-env') + _write_settings(INTERNAL_SKALE_PASSIVE, NODE_SKALE_PASSIVE) try: test_env = """ ENDPOINT=http://localhost:8545 @@ -430,12 +484,14 @@ def passive_user_conf(tmp_path): SCHAIN_NAME='test-schain' ENFORCE_BTRFS=False MANAGER_CONTRACTS='test-manager' + IMA_CONTRACTS='test-ima' """ with open(test_env_path, 'w') as env_file: env_file.write(test_env) yield test_env_path finally: test_env_path.unlink() + _cleanup_settings() @pytest.fixture diff --git a/tests/core/core_node_test.py b/tests/core/core_node_test.py index 953e7ca5..b28719d4 100644 --- a/tests/core/core_node_test.py +++ b/tests/core/core_node_test.py @@ -10,9 +10,13 @@ import pytest import requests -from node_cli.configs import NODE_DATA_PATH, SCHAINS_MNT_DIR_REGULAR, SCHAINS_MNT_DIR_SINGLE_CHAIN +from node_cli.configs import ( + NODE_DATA_PATH, + SCHAINS_MNT_DIR_REGULAR, + SCHAINS_MNT_DIR_SINGLE_CHAIN, +) from node_cli.configs.resource_allocation import RESOURCE_ALLOCATION_FILEPATH -from node_cli.configs.user import SKALE_DIR_ENV_FILEPATH + from node_cli.core.node import ( cleanup, compose_node_env, @@ -165,112 +169,38 @@ def test_is_base_containers_alive_empty(node_type, node_mode, is_boot): @pytest.mark.parametrize( - ( - 'node_type, node_mode, test_user_conf, is_boot, inited_node, sync_schains,' - 'expected_mnt_dir, expect_flask_key, expect_backup_run' - ), + 'node_type, node_mode, expected_mnt_dir', [ ( NodeType.SKALE, NodeMode.ACTIVE, - 'regular_user_conf', - False, - True, - False, - SCHAINS_MNT_DIR_REGULAR, - True, - False, - ), - ( - NodeType.SKALE, - NodeMode.ACTIVE, - 'regular_user_conf', - False, - True, - True, SCHAINS_MNT_DIR_REGULAR, - True, - True, ), ( NodeType.SKALE, NodeMode.PASSIVE, - 'passive_user_conf', - False, - False, - False, - SCHAINS_MNT_DIR_SINGLE_CHAIN, - False, - False, - ), - ( - NodeType.FAIR, - NodeMode.ACTIVE, - 'fair_boot_user_conf', - True, - True, - False, SCHAINS_MNT_DIR_SINGLE_CHAIN, - True, - False, ), ( NodeType.FAIR, NodeMode.ACTIVE, - 'fair_user_conf', - False, - True, - False, SCHAINS_MNT_DIR_SINGLE_CHAIN, - True, - False, ), ], ids=[ 'regular', - 'regular_passive_flag', 'passive', - 'fair_boot', - 'fair_regular', + 'fair', ], ) -def test_compose_node_env( - request, - node_type, - node_mode, - test_user_conf, - is_boot, - inited_node, - sync_schains, - expected_mnt_dir, - expect_flask_key, - expect_backup_run, -): - user_config_path = request.getfixturevalue(test_user_conf) - - with ( - mock.patch('node_cli.configs.user.validate_alias_or_address'), - mock.patch('node_cli.core.node.save_env_params'), - mock.patch('node_cli.core.node.get_flask_secret_key', return_value='mock_secret'), - ): - result_env = compose_node_env( - env_filepath=user_config_path.as_posix(), - inited_node=inited_node, - sync_schains=sync_schains, - node_type=node_type, - node_mode=node_mode, - is_fair_boot=is_boot, - save=True, - ) +def test_compose_node_env(node_type, node_mode, expected_mnt_dir, regular_user_conf): + result_env = compose_node_env( + node_type=node_type, + node_mode=node_mode, + ) assert result_env['SCHAINS_MNT_DIR'] == expected_mnt_dir - assert ( - 'FLASK_SECRET_KEY' in result_env and result_env['FLASK_SECRET_KEY'] is not None - ) == expect_flask_key - if expect_flask_key: - assert result_env['FLASK_SECRET_KEY'] == 'mock_secret' - should_have_backup = sync_schains and node_mode != NodeMode.PASSIVE - assert ('BACKUP_RUN' in result_env and result_env['BACKUP_RUN'] == 'True') == should_have_backup + assert 'BACKUP_RUN' not in result_env @pytest.fixture @@ -346,9 +276,8 @@ def test_init_node(regular_user_conf, no_resource_file): # todo: write new init mock.patch('node_cli.core.node.init_op'), mock.patch('node_cli.core.node.is_base_containers_alive', return_value=True), mock.patch('node_cli.utils.helper.post_request', resp_mock), - mock.patch('node_cli.configs.user.validate_alias_or_address'), ): - init(env_filepath=regular_user_conf.as_posix(), node_type=NodeType.SKALE) + init(config_file=regular_user_conf.as_posix(), node_type=NodeType.SKALE) assert os.path.isfile(RESOURCE_ALLOCATION_FILEPATH) @@ -358,8 +287,6 @@ def test_update_node(regular_user_conf, mocked_g_config, resource_file, inited_n with ( mock.patch('subprocess.run', new=subprocess_run_mock), mock.patch('node_cli.core.node.update_op'), - mock.patch('node_cli.core.node.get_flask_secret_key'), - mock.patch('node_cli.core.node.save_env_params'), mock.patch('node_cli.operations.base.configure_nftables'), mock.patch('node_cli.core.host.prepare_host'), mock.patch('node_cli.core.node.is_base_containers_alive', return_value=True), @@ -370,7 +297,6 @@ def test_update_node(regular_user_conf, mocked_g_config, resource_file, inited_n 'node_cli.core.node.CliMetaManager.get_meta_info', return_value=CliMeta(version='2.6.0', config_stream='3.0.2'), ), - mock.patch('node_cli.configs.user.validate_alias_or_address'), ): with mock.patch( 'node_cli.utils.helper.requests.get', return_value=safe_update_api_response() @@ -492,12 +418,7 @@ def test_cleanup_success( cleanup(node_mode=NodeMode.ACTIVE) - mock_compose_env.assert_called_once_with( - SKALE_DIR_ENV_FILEPATH, - save=False, - node_type=NodeType.SKALE, - node_mode=NodeMode.ACTIVE, - skip_user_conf_validation=True, - ) + mock_compose_env.assert_called_once_with(NodeType.SKALE, NodeMode.ACTIVE) mock_cleanup_skale_op.assert_called_once_with( - node_mode=NodeMode.ACTIVE, env=mock_env, prune=False) \ No newline at end of file + node_mode=NodeMode.ACTIVE, compose_env=mock_env, prune=False + ) diff --git a/tests/fair/fair_node_test.py b/tests/fair/fair_node_test.py index a0408caa..6554cd3f 100644 --- a/tests/fair/fair_node_test.py +++ b/tests/fair/fair_node_test.py @@ -2,8 +2,6 @@ import pytest -from node_cli.configs import SKALE_DIR -from node_cli.configs.user import SKALE_DIR_ENV_FILEPATH from node_cli.fair.boot import init as init_boot from node_cli.fair.boot import update from node_cli.fair.common import cleanup @@ -14,11 +12,9 @@ @mock.patch('node_cli.fair.active.time.sleep') @mock.patch('node_cli.fair.active.restore_fair_op') -@mock.patch('node_cli.fair.active.save_env_params') @mock.patch('node_cli.fair.active.compose_node_env') def test_restore_fair( mock_compose_env, - mock_save_env, mock_restore_op, mock_sleep, valid_env_file, @@ -32,14 +28,11 @@ def test_restore_fair( restore(backup_path, valid_env_file) - mock_compose_env.assert_called_once_with( - valid_env_file, node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE - ) - mock_save_env.assert_called_once_with(valid_env_file) - expected_env = {**mock_env, 'SKALE_DIR': SKALE_DIR} + mock_compose_env.assert_called_once_with(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) mock_restore_op.assert_called_once_with( node_mode=NodeMode.ACTIVE, - env=expected_env, + settings=mock.ANY, + compose_env=mock_env, backup_path=backup_path, config_only=False, ) @@ -63,16 +56,11 @@ def test_init_fair_boot( init_boot(valid_env_file) - mock_compose_env.assert_called_once_with( - valid_env_file, - node_type=NodeType.FAIR, - node_mode=NodeMode.ACTIVE, - is_fair_boot=True, - ) + mock_compose_env.assert_called_once_with(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) mock_init_op.assert_called_once_with( - valid_env_file, - mock_env, - NodeMode.ACTIVE, + settings=mock.ANY, + compose_env=mock_env, + node_mode=NodeMode.ACTIVE, ) mock_sleep.assert_called_once() mock_is_alive.assert_called_once_with( @@ -103,18 +91,10 @@ def test_update_fair_boot( update(valid_env_file, pull_config_for_schain) - mock_compose_env.assert_called_once_with( - valid_env_file, - inited_node=True, - sync_schains=False, - pull_config_for_schain=pull_config_for_schain, - node_type=NodeType.FAIR, - node_mode=NodeMode.ACTIVE, - is_fair_boot=True, - ) + mock_compose_env.assert_called_once_with(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) mock_update_op.assert_called_once_with( - valid_env_file, - mock_env, + settings=mock.ANY, + compose_env=mock_env, node_mode=NodeMode.ACTIVE, ) mock_sleep.assert_called_once() @@ -141,16 +121,10 @@ def test_migrate_from_boot( migrate_from_boot(valid_env_file) - mock_compose_env.assert_called_once_with( - valid_env_file, - inited_node=True, - sync_schains=False, - node_type=NodeType.FAIR, - node_mode=NodeMode.ACTIVE, - ) + mock_compose_env.assert_called_once_with(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) mock_migrate_op.assert_called_once_with( - valid_env_file, - mock_env, + settings=mock.ANY, + compose_env=mock_env, node_mode=NodeMode.ACTIVE, update_type=FairUpdateType.FROM_BOOT, force_skaled_start=False, @@ -164,25 +138,20 @@ def test_cleanup_success( mock_compose_env, mock_cleanup_fair_op, mock_is_user_valid, - inited_node, resource_alloc, meta_file_v3, active_node_option, + inited_node, ): mock_env = {'ENV_TYPE': 'devnet'} mock_compose_env.return_value = mock_env cleanup(node_mode=NodeMode.ACTIVE) - mock_compose_env.assert_called_once_with( - SKALE_DIR_ENV_FILEPATH, - save=False, - node_type=NodeType.FAIR, - node_mode=NodeMode.ACTIVE, - skip_user_conf_validation=True, - ) + mock_compose_env.assert_called_once_with(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE) mock_cleanup_fair_op.assert_called_once_with( - node_mode=NodeMode.ACTIVE, env=mock_env, prune=False) + node_mode=NodeMode.ACTIVE, compose_env=mock_env, prune=False + ) @mock.patch('node_cli.utils.decorators.is_user_valid', return_value=True) @@ -192,10 +161,10 @@ def test_cleanup_calls_operations_in_correct_order( mock_compose_env, mock_cleanup_fair_op, mock_is_user_valid, - inited_node, resource_alloc, meta_file_v3, active_node_option, + inited_node, ): from node_cli.fair.common import cleanup @@ -210,12 +179,10 @@ def test_cleanup_calls_operations_in_correct_order( expected_calls = [ mock.call.compose_env( - mock.ANY, - save=False, - node_type=mock.ANY, + node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE, - skip_user_conf_validation=True), - mock.call.cleanup_fair_op(node_mode=NodeMode.ACTIVE, env=mock_env, prune=False), + ), + mock.call.cleanup_fair_op(node_mode=NodeMode.ACTIVE, compose_env=mock_env, prune=False), ] manager.assert_has_calls(expected_calls, any_order=False) @@ -227,10 +194,10 @@ def test_cleanup_continues_after_fair_op_error( mock_compose_env, mock_cleanup_fair_op, mock_is_user_valid, - inited_node, resource_alloc, meta_file_v3, active_node_option, + inited_node, ): mock_env = {'ENV_TYPE': 'devnet'} mock_compose_env.return_value = mock_env @@ -240,15 +207,16 @@ def test_cleanup_continues_after_fair_op_error( mock_compose_env.assert_called_once() mock_cleanup_fair_op.assert_called_once_with( - node_mode=NodeMode.ACTIVE, env=mock_env, prune=False) + node_mode=NodeMode.ACTIVE, compose_env=mock_env, prune=False + ) @mock.patch('node_cli.utils.decorators.is_user_valid', return_value=False) def test_cleanup_fails_when_user_invalid( mock_is_user_valid, - inited_node, resource_alloc, meta_file_v3, + inited_node, ): """Test that cleanup fails when user validation fails""" import pytest @@ -259,11 +227,12 @@ def test_cleanup_fails_when_user_invalid( cleanup(node_mode=NodeMode.ACTIVE) -def test_cleanup_fails_when_not_inited(ensure_meta_removed, active_node_option): +def test_cleanup_fails_when_not_inited(ensure_meta_removed, active_node_option, fair_user_conf): import pytest - with pytest.raises(SystemExit): - cleanup(node_mode=NodeMode.ACTIVE) + with mock.patch('node_cli.operations.cleanup_fair_op', return_value=None): + with pytest.raises(SystemExit): + cleanup(node_mode=NodeMode.ACTIVE) @mock.patch('node_cli.utils.decorators.is_user_valid', return_value=True) @@ -275,10 +244,10 @@ def test_cleanup_logs_success_message( mock_compose_env, mock_cleanup_fair_op, mock_is_user_valid, - inited_node, resource_alloc, meta_file_v3, active_node_option, + inited_node, ): mock_env = {'ENV_TYPE': 'devnet'} mock_compose_env.return_value = mock_env diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/settings.py b/tests/fixtures/settings.py new file mode 100644 index 00000000..55b080b2 --- /dev/null +++ b/tests/fixtures/settings.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# This file is part of node-cli +# +# Copyright (C) 2026-Present SKALE Labs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import tomli_w + +from skale_core.settings import get_internal_settings + +from node_cli.configs import INTERNAL_SETTINGS_PATH, NODE_SETTINGS_PATH + +SKALE_DIR_HOST = './skale-data/' + +INTERNAL_SKALE_ACTIVE = { + 'node_type': 'skale', + 'node_mode': 'active', + 'skale_dir_host': SKALE_DIR_HOST, +} + +INTERNAL_SKALE_PASSIVE = { + 'node_type': 'skale', + 'node_mode': 'passive', + 'skale_dir_host': SKALE_DIR_HOST, +} + +INTERNAL_FAIR_ACTIVE = { + 'node_type': 'fair', + 'node_mode': 'active', + 'skale_dir_host': SKALE_DIR_HOST, +} + +INTERNAL_FAIR_PASSIVE = { + 'node_type': 'fair', + 'node_mode': 'passive', + 'skale_dir_host': SKALE_DIR_HOST, +} + +_BASE_NODE = { + 'env_type': 'devnet', + 'endpoint': 'http://127.0.0.1:8545', + 'container_stop_timeout': 1, + 'tg_api_key': '123', + 'tg_chat_id': '-1231232', + 'node_version': '0.0.0', + 'block_device': '/dev/sda', +} + +NODE_SKALE_ACTIVE = { + **_BASE_NODE, + 'sgx_url': 'https://localhost:1026', + 'docker_lvmpy_version': '0.0.0', + 'manager_contracts': 'test-manager', + 'ima_contracts': 'test-ima', +} + +NODE_SKALE_PASSIVE = { + **_BASE_NODE, + 'manager_contracts': 'test-manager', + 'ima_contracts': 'test-ima', + 'schain_name': 'test-schain', + 'enforce_btrfs': False, +} + +NODE_FAIR_ACTIVE = { + **_BASE_NODE, + 'sgx_url': 'https://localhost:1026', + 'fair_contracts': 'test-fair', + 'enforce_btrfs': False, +} + +NODE_FAIR_PASSIVE = { + **_BASE_NODE, + 'fair_contracts': 'test-fair', + 'enforce_btrfs': False, +} + + +def _write_settings(internal: dict, node: dict) -> None: + INTERNAL_SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True) + INTERNAL_SETTINGS_PATH.write_bytes(tomli_w.dumps(internal).encode()) + NODE_SETTINGS_PATH.write_bytes(tomli_w.dumps(node).encode()) + get_internal_settings.cache_clear() + + +def _cleanup_settings() -> None: + INTERNAL_SETTINGS_PATH.unlink(missing_ok=True) + NODE_SETTINGS_PATH.unlink(missing_ok=True) + get_internal_settings.cache_clear() + + +@pytest.fixture +def skale_active_settings(): + _write_settings(INTERNAL_SKALE_ACTIVE, NODE_SKALE_ACTIVE) + yield + _cleanup_settings() + + +@pytest.fixture +def skale_passive_settings(): + _write_settings(INTERNAL_SKALE_PASSIVE, NODE_SKALE_PASSIVE) + yield + _cleanup_settings() + + +@pytest.fixture +def fair_active_settings(): + _write_settings(INTERNAL_FAIR_ACTIVE, NODE_FAIR_ACTIVE) + yield + _cleanup_settings() + + +@pytest.fixture +def fair_passive_settings(): + _write_settings(INTERNAL_FAIR_PASSIVE, NODE_FAIR_PASSIVE) + yield + _cleanup_settings() diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py new file mode 100644 index 00000000..e7b3f4be --- /dev/null +++ b/tests/utils/settings_test.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# This file is part of node-cli +# +# Copyright (C) 2026 SKALE Labs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +