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/node.py b/node_cli/cli/node.py
index 55304356..f13825d9 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,
+ env_filepath=config_file,
pull_config_for_schain=pull_config_for_schain,
node_type=TYPE,
unsafe_ok=unsafe_ok,
@@ -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/configs/__init__.py b/node_cli/configs/__init__.py
index 35d776d8..f9fc0aa5 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,6 +44,11 @@
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')
@@ -52,8 +58,6 @@
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')
@@ -95,9 +99,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/user.py b/node_cli/configs/_user.py
similarity index 98%
rename from node_cli/configs/user.py
rename to node_cli/configs/_user.py
index a4f52c2c..6ab7b6f5 100644
--- a/node_cli/configs/user.py
+++ b/node_cli/configs/_user.py
@@ -52,6 +52,7 @@ class ValidationResult(NamedTuple):
class BaseUserConfig(ABC):
node_version: str
env_type: str
+ endpoint: str
filebeat_host: str
block_device: str
@@ -89,7 +90,6 @@ def validate_params(cls, params: Dict) -> ValidationResult:
@dataclass
class FairUserConfig(BaseUserConfig):
fair_contracts: str
- boot_endpoint: str
sgx_server_url: str
enforce_btrfs: str = ''
telegraf: str = ''
@@ -99,13 +99,11 @@ class FairUserConfig(BaseUserConfig):
@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
@@ -114,7 +112,6 @@ class FairBootUserConfig(BaseUserConfig):
@dataclass
class SkaleUserConfig(BaseUserConfig):
- endpoint: str
manager_contracts: str
ima_contracts: str
docker_lvmpy_version: str
@@ -132,7 +129,6 @@ class SkaleUserConfig(BaseUserConfig):
@dataclass
class PassiveSkaleUserConfig(BaseUserConfig):
- endpoint: str
manager_contracts: str
schain_name: str = ''
ima_contracts: str = ''
diff --git a/node_cli/core/host.py b/node_cli/core/host.py
index da4be640..920b651e 100644
--- a/node_cli/core/host.py
+++ b/node_cli/core/host.py
@@ -39,6 +39,7 @@
SGX_CERTS_PATH,
REPORTS_PATH,
REDIS_DATA_PATH,
+ SETTINGS_DIR,
SCHAINS_DATA_PATH,
LOG_PATH,
REMOVED_CONTAINERS_FOLDER_PATH,
@@ -50,7 +51,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,20 +73,6 @@ 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')
@@ -121,6 +107,7 @@ def make_dirs():
LOG_PATH,
REPORTS_PATH,
REDIS_DATA_PATH,
+ SETTINGS_DIR,
SKALE_RUN_DIR,
SKALE_STATE_DIR,
SKALE_TMP_DIR,
@@ -128,16 +115,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..191d2077 100644
--- a/node_cli/core/node.py
+++ b/node_cli/core/node.py
@@ -43,9 +43,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,
@@ -152,11 +151,11 @@ 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)
+ 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(env_filepath=config_file, env=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):
@@ -169,7 +168,7 @@ def init(env_filepath: str, node_type: NodeType) -> None:
@check_not_inited
def restore(backup_path, env_filepath, node_type: NodeType, no_snapshot=False, config_only=False):
node_mode = NodeMode.ACTIVE
- env = compose_node_env(env_filepath=env_filepath, node_type=node_type, node_mode=node_mode)
+ env = compose_node_env(node_type=node_type, node_mode=node_mode)
if env is None:
return
save_env_params(env_filepath)
@@ -211,7 +210,7 @@ def update_passive(env_filepath: str) -> None:
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)
+ env = compose_node_env(node_type=NodeType.SKALE, node_mode=NodeMode.PASSIVE)
update_ok = update_passive_op(env_filepath, env)
if update_ok:
logger.info('Waiting for containers initialization')
@@ -227,69 +226,22 @@ 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,
- )
+ env = compose_node_env(NodeType.SKALE, node_mode)
cleanup_skale_op(node_mode=node_mode, 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]:
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(),
}
-
- 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 != ''}
@@ -313,10 +265,10 @@ def update(
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,
+ # env_filepath,
+ # inited_node=True,
+ # sync_schains=False,
+ # pull_config_for_schain=pull_config_for_schain,
node_type=node_type,
node_mode=node_mode,
)
diff --git a/node_cli/operations/base.py b/node_cli/operations/base.py
index 1a64d585..713a6775 100644
--- a/node_cli/operations/base.py
+++ b/node_cli/operations/base.py
@@ -53,7 +53,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,
@@ -76,6 +76,7 @@
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_settings
logger = logging.getLogger(__name__)
@@ -134,6 +135,7 @@ def update(env_filepath: str, env: Dict, node_mode: NodeMode) -> bool:
generate_nginx_config()
prepare_host(env_filepath, env['ENV_TYPE'], allocation=True)
+ save_settings(node_type=NodeType.SKALE, node_mode=node_mode)
init_shared_space_volume(env['ENV_TYPE'])
meta_manager = CliMetaManager()
@@ -170,12 +172,12 @@ def init(env_filepath: str, env: dict, node_mode: NodeMode) -> None:
configure_nftables(enable_monitoring=enable_monitoring)
prepare_host(env_filepath, env_type=env['ENV_TYPE'])
+ save_settings(node_type=NodeType.SKALE, node_mode=node_mode)
link_env_file()
mark_active_node()
configure_filebeat()
- configure_flask()
generate_nginx_config()
lvmpy_install(env)
@@ -222,7 +224,7 @@ def init_passive(
NodeMode.PASSIVE,
env['ENV_TYPE'],
CONTAINER_CONFIG_PATH,
- check_type=CheckType.PREINSTALL
+ check_type=CheckType.PREINSTALL,
)
if failed_checks:
print_failed_requirements_checks(failed_checks)
@@ -280,7 +282,7 @@ def update_passive(env_filepath: str, env: Dict) -> bool:
NodeMode.PASSIVE,
env['ENV_TYPE'],
CONTAINER_CONFIG_PATH,
- check_type=CheckType.PREINSTALL
+ check_type=CheckType.PREINSTALL,
)
if failed_checks:
print_failed_requirements_checks(failed_checks)
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/fair.py b/node_cli/operations/fair.py
index f1e62305..096d1623 100644
--- a/node_cli/operations/fair.py
+++ b/node_cli/operations/fair.py
@@ -46,7 +46,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,
@@ -70,6 +70,7 @@
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_settings
logger = logging.getLogger(__name__)
@@ -93,11 +94,11 @@ def init_fair_boot(env_filepath: str, env: dict) -> None:
configure_nftables(enable_monitoring=enable_monitoring)
prepare_host(env_filepath, env_type=env['ENV_TYPE'])
+ save_settings(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE)
link_env_file()
mark_active_node()
configure_filebeat()
- configure_flask()
generate_nginx_config()
prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True')
@@ -131,10 +132,10 @@ def init(
configure_nftables()
configure_filebeat()
- configure_flask()
generate_nginx_config()
prepare_host(env_filepath, env_type=env['ENV_TYPE'])
+ save_settings(node_type=NodeType.FAIR, node_mode=node_mode)
link_env_file()
prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True')
@@ -186,6 +187,7 @@ def update_fair_boot(env_filepath: str, env: dict, node_mode: NodeMode = NodeMod
prepare_block_device(env['BLOCK_DEVICE'], force=env['ENFORCE_BTRFS'] == 'True')
prepare_host(env_filepath, env['ENV_TYPE'])
+ save_settings(node_type=NodeType.FAIR, node_mode=NodeMode.ACTIVE)
meta_manager = FairCliMetaManager()
current_stream = meta_manager.get_meta_info().config_stream
@@ -231,6 +233,7 @@ def update(
generate_nginx_config()
prepare_host(env_filepath, env['ENV_TYPE'], allocation=True)
+ save_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'
diff --git a/node_cli/utils/docker_utils.py b/node_cli/utils/docker_utils.py
index 695a1253..37e3fab9 100644
--- a/node_cli/utils/docker_utils.py
+++ b/node_cli/utils/docker_utils.py
@@ -338,9 +338,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:
diff --git a/node_cli/utils/helper.py b/node_cli/utils/helper.py
index dd1a7151..9d96a3ca 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)
@@ -339,7 +340,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 +412,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
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..c0b3aa76
--- /dev/null
+++ b/node_cli/utils/settings.py
@@ -0,0 +1,47 @@
+# -*- 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 .
+
+from skale.core.settings import (
+ SETTINGS_MAP,
+ write_node_settings_file,
+ write_internal_settings_file,
+ InternalSettings,
+ SkaleSettings,
+ SkalePassiveSettings,
+ FairSettings,
+ FairBaseSettings,
+)
+
+from node_cli.configs import NODE_SETTINGS_PATH, INTERNAL_SETTINGS_PATH
+
+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 save_settings(node_type: NodeType, node_mode: NodeMode) -> None:
+ write_internal_settings_file(path=INTERNAL_SETTINGS_PATH, data={}) # todof: fix
+ settings_type = SETTINGS_MAP[(node_type.value, node_mode.value)]
+ write_node_settings_file(
+ path=NODE_SETTINGS_PATH, settings_type=settings_type, data={}
+ ) # todof: fix
diff --git a/pyproject.toml b/pyproject.toml
index cf813bfa..3f2e25d1 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==7.12dev2",
]
[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..23592396 100755
--- a/scripts/run_tests.sh
+++ b/scripts/run_tests.sh
@@ -1,11 +1,3 @@
#!/usr/bin/env bash
-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/ $@
+py.test --cov=$PROJECT_DIR/ --ignore=tests/core/nftables_test.py --ignore=tests/core/migration_test.py tests/ $@
diff --git a/tests/cli/node_test.py b/tests/cli/node_test.py
index b31cddc0..bb5aa6d0 100644
--- a/tests/cli/node_test.py
+++ b/tests/cli/node_test.py
@@ -422,7 +422,6 @@ def test_turn_on_maintenance_off(mocked_g_config, regular_user_conf, active_node
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),
@@ -475,6 +474,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 +493,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, env={})
diff --git a/tests/conftest.py b/tests/conftest.py
index 5434e697..cc5a5fa3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -380,7 +380,7 @@ def fair_user_conf(tmp_path):
test_env_path = pathlib.Path(tmp_path / 'test-env')
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
diff --git a/tests/core/core_node_test.py b/tests/core/core_node_test.py
index 953e7ca5..0fe113b2 100644
--- a/tests/core/core_node_test.py
+++ b/tests/core/core_node_test.py
@@ -243,7 +243,6 @@ def test_compose_node_env(
inited_node,
sync_schains,
expected_mnt_dir,
- expect_flask_key,
expect_backup_run,
):
user_config_path = request.getfixturevalue(test_user_conf)
@@ -251,7 +250,6 @@ def test_compose_node_env(
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(),
@@ -264,11 +262,6 @@ def test_compose_node_env(
)
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
@@ -358,7 +351,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'),
@@ -500,4 +492,5 @@ def test_cleanup_success(
skip_user_conf_validation=True,
)
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, env=mock_env, prune=False
+ )
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
new file mode 100644
index 00000000..1d9deeb1
--- /dev/null
+++ b/tests/utils/settings_test.py
@@ -0,0 +1,20 @@
+# -*- 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 .
+
+from node_cli.utils.node_type import NodeMode, NodeType