prepare: Make setup config persistent.

This commit is contained in:
tecnovert 2025-04-02 20:36:44 +02:00
parent c28eb9ab9b
commit 7972a50341
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
5 changed files with 209 additions and 48 deletions

View file

@ -283,18 +283,23 @@ TOR_PROXY_HOST = os.getenv("TOR_PROXY_HOST", "127.0.0.1")
TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050))
TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051))
TOR_DNS_PORT = int(os.getenv("TOR_DNS_PORT", 5353))
TOR_CONTROL_LISTEN_INTERFACE = os.getenv(
"TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TORRC_PROXY_HOST = os.getenv(
"TORRC_PROXY_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TORRC_CONTROL_HOST = os.getenv(
"TORRC_CONTROL_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TORRC_DNS_HOST = os.getenv(
"TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
def setTorrcVars():
global TOR_CONTROL_LISTEN_INTERFACE, TORRC_PROXY_HOST, TORRC_CONTROL_HOST, TORRC_DNS_HOST
TOR_CONTROL_LISTEN_INTERFACE = os.getenv(
"TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TORRC_PROXY_HOST = os.getenv(
"TORRC_PROXY_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TORRC_CONTROL_HOST = os.getenv(
"TORRC_CONTROL_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TORRC_DNS_HOST = os.getenv(
"TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
)
TEST_TOR_PROXY = toBool(
os.getenv("TEST_TOR_PROXY", "true")
@ -2073,7 +2078,69 @@ def load_config(config_path):
if not os.path.exists(config_path):
exitWithError("{} does not exist".format(config_path))
with open(config_path) as fs:
return json.load(fs)
settings = json.load(fs)
BSX_ALLOW_ENV_OVERRIDE = toBool(os.getenv("BSX_ALLOW_ENV_OVERRIDE", "false"))
saved_env_var_settings = [
("setup_docker_mode", "BSX_DOCKER_MODE"),
("setup_local_tor", "BSX_LOCAL_TOR"),
("setup_tor_control_listen_interface", "TOR_CONTROL_LISTEN_INTERFACE"),
("setup_torrc_proxy_host", "TORRC_PROXY_HOST"),
("setup_torrc_control_host", "TORRC_CONTROL_HOST"),
("setup_torrc_dns_host", "TORRC_DNS_HOST"),
("tor_proxy_host", "TOR_PROXY_HOST"),
("tor_proxy_port", "TOR_PROXY_PORT"),
("tor_control_port", "TOR_CONTROL_PORT"),
]
for setting in saved_env_var_settings:
config_name, env_name = setting
env_value = globals()[env_name]
saved_config_value = settings.get(config_name, env_value)
if saved_config_value != env_value:
if os.getenv(env_name):
# If the env var was manually set override the saved config if allowed else fail.
if BSX_ALLOW_ENV_OVERRIDE:
logger.warning(
f"Env var {env_name} differs from saved config '{config_name}', overriding."
)
else:
print(
f"Env var {env_name} differs from saved config '{config_name}', set 'BSX_ALLOW_ENV_OVERRIDE' to override.",
file=sys.stderr,
)
sys.exit(1)
else:
logger.info(f"Setting {env_name} from saved config '{config_name}'.")
globals()[env_name] = saved_config_value
# Recalculate env vars that depend on the changed var
if env_name == "BSX_LOCAL_TOR":
setTorrcVars()
return settings
def save_config(config_path, settings, add_options: bool = True) -> None:
if add_options is True:
if os.getenv("BSX_DOCKER_MODE") or "docker_mode" not in settings:
settings["setup_docker_mode"] = BSX_DOCKER_MODE
if os.getenv("BSX_LOCAL_TOR") or "local_tor" not in settings:
settings["setup_local_tor"] = BSX_LOCAL_TOR
# Add to settings only if manually set
if os.getenv("TOR_CONTROL_LISTEN_INTERFACE"):
settings["setup_tor_control_listen_interface"] = (
TOR_CONTROL_LISTEN_INTERFACE
)
if os.getenv("TORRC_PROXY_HOST"):
settings["setup_torrc_proxy_host"] = TORRC_PROXY_HOST
if os.getenv("TORRC_CONTROL_HOST"):
settings["setup_torrc_control_host"] = TORRC_CONTROL_HOST
if os.getenv("TORRC_DNS_HOST"):
settings["setup_torrc_dns_host"] = TORRC_DNS_HOST
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
def signal_handler(sig, frame):
@ -2143,6 +2210,7 @@ def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None:
def main():
global use_tor_proxy, with_coins_changed
setTorrcVars()
data_dir = None
bin_dir = None
port_offset = None
@ -2735,9 +2803,7 @@ def main():
settings, coin, tor_control_password, enable=True, extra_opts=extra_opts
)
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
logger.info("Done.")
return 0
@ -2754,9 +2820,7 @@ def main():
extra_opts=extra_opts,
)
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
logger.info("Done.")
return 0
@ -2781,9 +2845,7 @@ def main():
if "manage_wallet_daemon" in coin_settings:
coin_settings["manage_wallet_daemon"] = False
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
logger.info("Done.")
return 0
@ -2805,8 +2867,7 @@ def main():
coin_settings["manage_daemon"] = True
if "manage_wallet_daemon" in coin_settings:
coin_settings["manage_wallet_daemon"] = True
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
logger.info("Done.")
return 0
exitWithError("{} is already in the settings file".format(add_coin))
@ -2821,7 +2882,6 @@ def main():
test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
settings["chainclients"][add_coin] = chainclients[add_coin]
settings["use_tor_proxy"] = use_tor_proxy
if not no_cores:
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
@ -2843,8 +2903,7 @@ def main():
use_tor_proxy,
)
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
logger.info(f"Done. Coin {add_coin} successfully added.")
return 0
@ -2863,8 +2922,7 @@ def main():
if c not in settings["chainclients"]:
settings["chainclients"][c] = chainclients[c]
elif upgrade_cores:
with open(config_path) as fs:
settings = json.load(fs)
settings = load_config(config_path)
with_coins_start = with_coins
if not with_coins_changed:
@ -2913,8 +2971,8 @@ def main():
# Run second loop to update, so all versions are logged together.
# Backup settings
old_config_path = config_path[:-5] + "_" + str(int(time.time())) + ".json"
with open(old_config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(old_config_path, settings, add_options=False)
for c in with_coins:
prepareCore(c, known_coins[c], settings, data_dir, extra_opts)
current_coin_settings = chainclients[c]
@ -2927,8 +2985,7 @@ def main():
settings["chainclients"][c][
"core_version_group"
] = current_version_group
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
logger.info("Done.")
return 0
@ -2978,8 +3035,7 @@ def main():
for c in with_coins:
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts)
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
save_config(config_path, settings)
if particl_wallet_mnemonic == "none":
logger.info("Done.")

View file

@ -197,7 +197,7 @@ class Test(unittest.TestCase):
logger.handlers = []
logger.setLevel(logging.INFO) # DEBUG shows many messages from requests.post
formatter = logging.Formatter("%(asctime)s %(levelname)s : %(message)s")
stream_stdout = logging.StreamHandler()
stream_stdout = logging.StreamHandler(sys.stdout)
stream_stdout.setFormatter(formatter)
logger.addHandler(stream_stdout)

View file

@ -6,30 +6,34 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import sys
import importlib
import json
import shutil
import logging
import unittest
import threading
import multiprocessing
import os
import shutil
import sys
import threading
import unittest
from io import StringIO
from unittest.mock import patch
import basicswap.config as cfg
from tests.basicswap.util import (
make_boolean,
read_json_api,
waitForServer,
)
TEST_DELETE_DIRS = make_boolean(os.getenv("TEST_DELETE_DIRS", True))
bin_path = os.path.expanduser(os.getenv("TEST_BIN_PATH", ""))
test_base_path = os.path.expanduser(os.getenv("TEST_PREPARE_PATH", "~/test_basicswap"))
test_path_plain = os.path.join(test_base_path, "plain")
test_path_encrypted = os.path.join(test_base_path, "encrypted")
test_path_encrypt = os.path.join(test_base_path, "encrypt")
delay_event = threading.Event()
logger = logging.getLogger()
logger.level = logging.DEBUG
@ -59,14 +63,28 @@ def start_run(args, env_pairs=[]):
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
# Reset env vars
reset_vars = ["BSX_ALLOW_ENV_OVERRIDE", "BSX_DOCKER_MODE", "BSX_LOCAL_TOR"]
for var_name in reset_vars:
if var_name in os.environ:
del os.environ[var_name]
@classmethod
def tearDownClass(self):
try:
for test_dir in (test_path_plain, test_path_encrypted, test_path_encrypt):
if os.path.exists(test_dir):
shutil.rmtree(test_dir)
if TEST_DELETE_DIRS:
for test_dir in (
test_path_plain,
test_path_encrypted,
test_path_encrypt,
):
if os.path.exists(test_dir):
shutil.rmtree(test_dir)
except Exception as ex:
logger.warning("tearDownClass %s", str(ex))
logger.warning(f"tearDownClass {ex}")
super(Test, self).tearDownClass()
def test_plain(self):
@ -146,6 +164,7 @@ class Test(unittest.TestCase):
self.assertTrue(
settings["chainclients"]["bitcoin"]["connection_type"] == "rpc"
)
assert settings.get("use_tor", False) is False
logging.info("notorproxy")
testargs = [
@ -165,8 +184,89 @@ class Test(unittest.TestCase):
"--usetorproxy and --notorproxy together" in fake_stderr.getvalue()
)
logger.info("Test persistent setup config.")
with open(config_path) as fs:
settings = json.load(fs)
assert settings.get("use_tor", False) is False
assert settings["setup_docker_mode"] is False
assert settings["setup_local_tor"] is False
assert "setup_tor_control_listen_interface" not in settings
assert "setup_torrc_dns_host" not in settings
os.environ["BSX_LOCAL_TOR"] = "true"
# Reimport to reset globals and set env
importlib.reload(prepareSystem)
testargs = [
"basicswap-prepare",
"-datadir=" + test_path_plain,
"--enabletor",
]
with patch("sys.stderr", new=StringIO()) as fake_stderr:
with patch.object(sys, "argv", testargs):
with self.assertRaises(SystemExit) as cm:
prepareSystem.main()
self.assertEqual(cm.exception.code, 1)
self.assertTrue(
"Env var BSX_LOCAL_TOR differs from saved config"
in fake_stderr.getvalue()
)
os.environ["BSX_ALLOW_ENV_OVERRIDE"] = "true"
os.environ["TOR_CONTROL_LISTEN_INTERFACE"] = "127.1.1.1"
importlib.reload(prepareSystem)
with patch.object(sys, "argv", testargs):
prepareSystem.main()
with open(config_path) as fs:
settings = json.load(fs)
assert settings.get("use_tor", False) is True
assert settings["setup_docker_mode"] is False
assert settings["setup_local_tor"] is True
assert settings["setup_tor_control_listen_interface"] == "127.1.1.1"
assert "setup_torrc_dns_host" not in settings
particl_config_path = os.path.join(
test_path_plain, "particl", "particl.conf"
)
with open(particl_config_path) as fs:
particl_conf = fs.read()
assert "bind=127.1.1.1:" in particl_conf
testargs = [
"basicswap-prepare",
"-datadir=" + test_path_plain,
"-disablecoin=bitcoin",
]
with patch.object(sys, "argv", testargs):
prepareSystem.main()
os.environ["TOR_PROXY_HOST"] = "127.2.2.2"
del os.environ["BSX_ALLOW_ENV_OVERRIDE"]
importlib.reload(prepareSystem)
testargs = [
"basicswap-prepare",
"-datadir=" + test_path_plain,
"-addcoin=bitcoin",
"-notorproxy", # Disable TOR connection check
]
with patch("sys.stderr", new=StringIO()) as fake_stderr:
with patch.object(sys, "argv", testargs):
with self.assertRaises(SystemExit) as cm:
prepareSystem.main()
self.assertEqual(cm.exception.code, 1)
self.assertTrue(
"Env var TOR_PROXY_HOST differs from saved config"
in fake_stderr.getvalue()
)
finally:
del prepareSystem
del prepareSystem # Does not cause next import to refresh globals and env (tested v3.13)
def test_encrypted(self):
if os.path.exists(test_path_encrypted):

View file

@ -12,6 +12,7 @@ import os
import random
import shutil
import signal
import sys
import threading
import time
import traceback
@ -350,7 +351,7 @@ class BaseTest(unittest.TestCase):
logger.handlers = []
logger.setLevel(logging.INFO) # DEBUG shows many messages from requests.post
formatter = logging.Formatter("%(asctime)s %(levelname)s : %(message)s")
stream_stdout = logging.StreamHandler()
stream_stdout = logging.StreamHandler(sys.stdout)
stream_stdout.setFormatter(formatter)
logger.addHandler(stream_stdout)

View file

@ -19,7 +19,11 @@ REQUIRED_SETTINGS = {
}
def make_boolean(s):
def make_boolean(s) -> bool:
if isinstance(s, bool):
return s
if isinstance(s, int):
return False if s == 0 else True
return s.lower() in ["1", "true"]