cmds: Implement chia beta (#12389)

* cmds: Implement `chia beta` for the beta test program

* Unhide and document all `beta` subcommands

* Refactor all subcommands

* Introduce `chia beta configure`

* Introduce `chia beta status`

* Test all `chia beta` commands

* Use a separate file logger for beta logs

* Write the plotting call args to the log file

* Sort potential submissions

* Some refactoring around log file log handler creation

* JSON dump the plotting args
This commit is contained in:
dustinface 2022-09-12 23:57:31 +02:00 committed by GitHub
parent 8aa0e7da90
commit 70fe981fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 785 additions and 82 deletions

163
chia/cmds/beta.py Normal file
View File

@ -0,0 +1,163 @@
import zipfile
from datetime import datetime
from pathlib import Path
from typing import List, Optional
import click
from chia.cmds.beta_funcs import (
default_beta_root_path,
prepare_chia_blockchain_log,
prepare_logs,
prepare_plotting_log,
prompt_beta_warning,
prompt_for_beta_path,
update_beta_config,
validate_beta_path,
)
from chia.util.config import lock_and_load_config, save_config
def print_restart_warning() -> None:
print("\nRestart the daemon and any running chia services for changes to take effect.")
@click.group("beta", hidden=True)
def beta_cmd() -> None:
pass
@beta_cmd.command("configure", help="Configure the beta test mode parameters")
@click.option("-p", "--path", help="The beta mode root path", type=str, required=False)
@click.pass_context
def configure(ctx: click.Context, path: Optional[str]) -> None:
root_path = ctx.obj["root_path"]
with lock_and_load_config(root_path, "config.yaml") as config:
if "beta" not in config:
ctx.exit("beta test mode is not enabled, enable it first with `chia beta enable`")
# Adjust the path
if path is None:
beta_root_path = prompt_for_beta_path(Path(config["beta"].get("path", default_beta_root_path())))
else:
beta_root_path = Path(path)
validate_beta_path(beta_root_path)
update_beta_config(True, beta_root_path, config)
save_config(root_path, "config.yaml", config)
print("\nbeta config updated")
print_restart_warning()
@beta_cmd.command("enable", help="Enable beta test mode")
@click.option(
"-f",
"--force",
help="Force accept the beta program warning",
is_flag=True,
default=False,
)
@click.option("-p", "--path", help="The beta mode root path", type=str, required=False)
@click.pass_context
def enable_cmd(ctx: click.Context, force: bool, path: Optional[str]) -> None:
root_path = ctx.obj["root_path"]
with lock_and_load_config(root_path, "config.yaml") as config:
if config.get("beta", {}).get("enabled", False):
ctx.exit("beta test mode is already enabled")
if not force and not prompt_beta_warning():
ctx.abort()
# Use the existing beta path if there is one and no path was provided as parameter
current_path = config.get("beta", {}).get("path")
current_path = None if current_path is None else Path(current_path)
if path is None and current_path is None:
beta_root_path = prompt_for_beta_path(current_path or default_beta_root_path())
else:
beta_root_path = Path(path or current_path)
validate_beta_path(beta_root_path)
update_beta_config(True, beta_root_path, config)
save_config(root_path, "config.yaml", config)
print(f"\nbeta test mode enabled with path {str(beta_root_path)!r}")
print_restart_warning()
@beta_cmd.command("disable", help="Disable beta test mode")
@click.pass_context
def disable_cmd(ctx: click.Context) -> None:
root_path = ctx.obj["root_path"]
with lock_and_load_config(root_path, "config.yaml") as config:
if not config.get("beta", {}).get("enabled", False):
ctx.exit("beta test mode is not enabled")
config["beta"]["enabled"] = False
save_config(root_path, "config.yaml", config)
print("\nbeta test mode disabled")
print_restart_warning()
@beta_cmd.command("prepare_submission", help="Prepare the collected log data for submission")
@click.pass_context
def prepare_submission_cmd(ctx: click.Context) -> None:
with lock_and_load_config(ctx.obj["root_path"], "config.yaml") as config:
beta_root_path = config.get("beta", {}).get("path", None)
if beta_root_path is None:
ctx.exit("beta test mode not enabled. Run `chia beta enable` first.")
beta_root_path = Path(beta_root_path)
validate_beta_path(beta_root_path)
available_results = sorted([path for path in beta_root_path.iterdir() if path.is_dir()])
if len(available_results) == 0:
ctx.exit(f"No beta logs found in {str(beta_root_path)!r}.")
print("Available versions:")
for i in range(len(available_results)):
print(f" [{i + 1}] {available_results[i].name}")
user_input = input("Select the version you want to prepare for submission: ")
try:
if int(user_input) <= 0:
raise IndexError()
prepare_result = available_results[int(user_input) - 1]
except IndexError:
ctx.exit(f"Invalid choice: {user_input}")
plotting_path = Path(prepare_result / "plotting")
chia_blockchain_path = Path(prepare_result / "chia-blockchain")
chia_logs = prepare_logs(plotting_path, prepare_chia_blockchain_log)
plotting_logs = prepare_logs(chia_blockchain_path, prepare_plotting_log)
submission_file_path = (
prepare_result / f"submission_{prepare_result.name}__{datetime.now().strftime('%m_%d_%Y__%H_%M_%S')}.zip"
)
def add_files(paths: List[Path]) -> int:
added = 0
for path in paths:
if path.name.startswith("."):
continue
zip_file.write(path, path.relative_to(prepare_result))
added += 1
return added
with zipfile.ZipFile(submission_file_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
files_added = add_files(chia_logs) + add_files(plotting_logs)
if files_added == 0:
submission_file_path.unlink()
ctx.exit(f"No logs files found in {str(plotting_path)!r} and {str(chia_blockchain_path)!r}.")
print(f"\nDone. You can find the prepared submission data in {submission_file_path}.")
@beta_cmd.command("status", help="Show the current beta configuration")
@click.pass_context
def status(ctx: click.Context) -> None:
with lock_and_load_config(ctx.obj["root_path"], "config.yaml") as config:
beta_config = config.get("beta")
if beta_config is None:
ctx.exit("beta test mode is not enabled, enable it first with `chia beta enable`")
print(f"enabled: {beta_config['enabled']}")
print(f"path: {beta_config['path']}")

100
chia/cmds/beta_funcs.py Normal file
View File

@ -0,0 +1,100 @@
import os
import sys
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from chia.util.chia_logging import get_beta_logging_config
from chia.util.errors import InvalidPathError
from chia.util.misc import format_bytes, prompt_yes_no, validate_directory_writable
def default_beta_root_path() -> Path:
return Path(os.path.expanduser(os.getenv("CHIA_BETA_ROOT", "~/chia-beta-test"))).resolve()
def warn_if_beta_enabled(config: Dict[str, Any]) -> None:
if config.get("beta", {}).get("enabled", False):
print("\nWARNING: beta test mode is enabled. Run `chia beta disable` if this is unintentional.\n")
def prompt_beta_warning() -> bool:
logging_config = get_beta_logging_config()
# The `/ 5` is just a rough estimation for `gzip` being used by the log rotation in beta mode. It was like
# 7-10x compressed in example tests with 2MB files.
min_space = format_bytes(int(logging_config["log_maxfilesrotation"] * logging_config["log_maxbytesrotation"] / 5))
return prompt_yes_no(
f"\nWARNING: Enabling the beta test mode increases disk writes and may lead to {min_space} of "
"extra logfiles getting stored on your disk. This should only be done if you are part of the beta test "
"program at: https://chia.net/beta-test\n\nDo you really want to enable the beta test mode?"
)
def prompt_for_beta_path(default_path: Path) -> Path:
path: Optional[Path] = None
for _ in range(3):
user_input = input(
"\nEnter a directory where the beta test logs can be stored or press enter to use the default "
f"[{str(default_path)}]:"
)
test_path = Path(user_input) if user_input else default_path
if not test_path.is_dir() and prompt_yes_no(
f"\nDirectory {str(test_path)!r} doesn't exist.\n\nDo you want to create it?"
):
test_path.mkdir(parents=True)
try:
validate_directory_writable(test_path)
except InvalidPathError as e:
print(str(e))
continue
path = test_path
break
if path is None:
sys.exit("Aborted!")
else:
return path
def update_beta_config(enabled: bool, path: Path, config: Dict[str, Any]) -> None:
if "beta" not in config:
config["beta"] = {}
config["beta"].update(
{
"enabled": enabled,
"path": str(path),
}
)
def validate_beta_path(beta_root_path: Path) -> None:
try:
validate_directory_writable(beta_root_path)
except InvalidPathError as e:
sys.exit(str(e))
def prepare_plotting_log(path: Path) -> None:
# TODO: Do stuff we want to do with the logs before submission. Maybe even just fully parse them and
# create some final result files and zip them instead of just the logs.
print(f" - {path.name}")
def prepare_chia_blockchain_log(path: Path) -> None:
# TODO: Do stuff we want to do with the logs before submission. Maybe even just fully parse them and
# create some final result files and zip them instead of just the logs.
print(f" - {path.name}")
def prepare_logs(prepare_path: Path, prepare_callback: Callable[[Path], None]) -> List[Path]:
result = [path for path in prepare_path.iterdir()] if prepare_path.exists() else []
if len(result):
print(f"\nPreparing {prepare_path.name!r} logs:")
for log in result:
if log.name.startswith("."):
continue
prepare_callback(log)
return result

View File

@ -2,6 +2,7 @@ from io import TextIOWrapper
import click
from chia import __version__
from chia.cmds.beta import beta_cmd
from chia.cmds.configure import configure_cmd
from chia.cmds.farm import farm_cmd
from chia.cmds.data import data_cmd
@ -144,6 +145,7 @@ cli.add_command(db_cmd)
cli.add_command(peer_cmd)
cli.add_command(data_cmd)
cli.add_command(passphrase_cmd)
cli.add_command(beta_cmd)
def main() -> None:

View File

@ -10,8 +10,10 @@ from chia.util.service_groups import all_groups
@click.pass_context
def start_cmd(ctx: click.Context, restart: bool, group: str) -> None:
import asyncio
from chia.cmds.beta_funcs import warn_if_beta_enabled
from .start_funcs import async_start
root_path = ctx.obj["root_path"]
config = load_config(root_path, "config.yaml")
warn_if_beta_enabled(config)
asyncio.run(async_start(root_path, config, group, restart, ctx.obj["force_legacy_keyring_migration"]))

View File

@ -50,6 +50,10 @@ async def async_stop(root_path: Path, config: Dict[str, Any], group: str, stop_d
@click.argument("group", type=click.Choice(list(all_groups())), nargs=-1, required=True)
@click.pass_context
def stop_cmd(ctx: click.Context, daemon: bool, group: str) -> None:
from chia.cmds.beta_funcs import warn_if_beta_enabled
root_path = ctx.obj["root_path"]
config = load_config(root_path, "config.yaml")
warn_if_beta_enabled(config)
sys.exit(asyncio.run(async_stop(root_path, config, group, daemon)))

View File

@ -23,7 +23,7 @@ from chia.plotters.plotters import get_available_plotters
from chia.plotting.util import add_plot_directory
from chia.server.server import ssl_context_for_root, ssl_context_for_server
from chia.ssl.create_ssl import get_mozilla_ca_crt
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config
from chia.util.errors import KeychainRequiresMigration, KeychainCurrentPassphraseIsInvalid
from chia.util.json_util import dict_to_json_str
@ -1331,7 +1331,7 @@ async def async_run_daemon(root_path: Path, wait_for_unlock: bool = False) -> in
chia_init(root_path, should_check_keys=(not wait_for_unlock))
config = load_config(root_path, "config.yaml")
setproctitle("chia_daemon")
initialize_logging("daemon", config["logging"], root_path)
initialize_service_logging("daemon", config)
crt_path = root_path / config["daemon_ssl"]["private_crt"]
key_path = root_path / config["daemon_ssl"]["private_key"]
ca_crt_path = root_path / config["private_ssl_ca"]["crt"]

View File

@ -219,7 +219,7 @@ def plot_bladebit(args, chia_root_path, root_path):
call_args.append("-m")
call_args.append(args.finaldir)
try:
asyncio.run(run_plotter(call_args, progress))
asyncio.run(run_plotter(chia_root_path, args.plotter, call_args, progress))
except Exception as e:
print(f"Exception while plotting: {e} {type(e)}")
print(f"Traceback: {traceback.format_exc()}")

View File

@ -236,7 +236,7 @@ def plot_madmax(args, chia_root_path: Path, plotters_root_path: Path):
call_args.append("-k")
call_args.append(str(args.size))
try:
asyncio.run(run_plotter(call_args, progress))
asyncio.run(run_plotter(chia_root_path, args.plotter, call_args, progress))
except Exception as e:
print(f"Exception while plotting: {type(e)} {e}")
print(f"Traceback: {traceback.format_exc()}")

View File

@ -1,7 +1,30 @@
import asyncio
import contextlib
import json
import signal
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Iterator, Optional, TextIO
from chia.cmds.init_funcs import chia_full_version_str
from chia.util.config import lock_and_load_config
@contextlib.contextmanager
def get_optional_beta_plot_log_file(root_path: Path, plotter: str) -> Iterator[Optional[TextIO]]:
beta_log_path: Optional[Path] = None
with lock_and_load_config(root_path, "config.yaml") as config:
if "beta" in config:
file_name = f"{plotter}_{datetime.now().strftime('%m_%d_%Y__%H_%M_%S')}.log"
beta_log_path = Path(config["beta"]["path"]) / chia_full_version_str() / "plotting" / file_name
beta_log_path.parent.mkdir(parents=True, exist_ok=True)
if beta_log_path is not None:
with open(beta_log_path, "w") as file:
yield file
else:
yield None
# https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/
@ -22,7 +45,7 @@ def parse_stdout(out, progress):
print(f"Progress update: {v}", flush=True)
async def run_plotter(args, progress_dict):
async def run_plotter(root_path, plotter, args, progress_dict):
orig_sigint_handler = signal.getsignal(signal.SIGINT)
installed_sigint_handler = False
process = await asyncio.create_subprocess_exec(
@ -37,27 +60,43 @@ async def run_plotter(args, progress_dict):
signal.signal(signal.SIGINT, sigint_handler)
installed_sigint_handler = True
try:
await asyncio.wait(
[
_read_stream(
process.stdout,
lambda x: parse_stdout(x.decode("UTF8"), progress_dict),
),
_read_stream(
process.stderr,
lambda x: print("STDERR: {}".format(x.decode("UTF8"))),
),
]
)
with get_optional_beta_plot_log_file(root_path, plotter) as log_file:
if log_file is not None:
log_file.write(json.dumps(args) + "\n")
await process.wait()
except Exception as e:
print(f"Caught exception while invoking plotter: {e}")
finally:
# Restore the original SIGINT handler
if installed_sigint_handler:
signal.signal(signal.SIGINT, orig_sigint_handler)
def process_stdout_line(line_bytes: bytes) -> None:
line_str = line_bytes.decode("UTF8")
parse_stdout(line_str, progress_dict)
if log_file is not None:
log_file.write(line_str)
def process_stderr_line(line_bytes: bytes) -> None:
err_str = f"STDERR: {line_bytes.decode('UTF8')}"
print(err_str)
if log_file is not None:
log_file.write(err_str)
try:
await asyncio.wait(
[
_read_stream(
process.stdout,
process_stdout_line,
),
_read_stream(
process.stderr,
process_stderr_line,
),
]
)
await process.wait()
except Exception as e:
print(f"Caught exception while invoking plotter: {e}")
finally:
# Restore the original SIGINT handler
if installed_sigint_handler:
signal.signal(signal.SIGINT, orig_sigint_handler)
def run_command(args, exc_description, *, check=True, **kwargs) -> subprocess.CompletedProcess:

View File

@ -11,7 +11,7 @@ from chia.seeder.crawler import Crawler
from chia.seeder.crawler_api import CrawlerAPI
from chia.server.outbound_message import NodeType
from chia.server.start_service import RpcInfo, Service, async_run
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config, load_config_cli
from chia.util.default_root import DEFAULT_ROOT_PATH
@ -67,11 +67,7 @@ async def async_main() -> int:
config[SERVICE_NAME] = service_config
overrides = service_config["network_overrides"]["constants"][service_config["selected_network"]]
updated_constants = DEFAULT_CONSTANTS.replace_str_to_bytes(**overrides)
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
service = create_full_node_crawler_service(DEFAULT_ROOT_PATH, config, updated_constants)
await service.setup_process_global_state()
await service.run()

View File

@ -10,7 +10,7 @@ from chia.rpc.farmer_rpc_api import FarmerRpcApi
from chia.server.outbound_message import NodeType
from chia.server.start_service import RpcInfo, Service, async_run
from chia.types.peer_info import PeerInfo
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config, load_config_cli
from chia.util.default_root import DEFAULT_ROOT_PATH
from chia.util.keychain import Keychain
@ -72,11 +72,7 @@ async def async_main() -> int:
config[SERVICE_NAME] = service_config
config_pool = load_config_cli(DEFAULT_ROOT_PATH, "config.yaml", "pool")
config["pool"] = config_pool
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
service = create_farmer_service(DEFAULT_ROOT_PATH, config, config_pool, DEFAULT_CONSTANTS)
await service.setup_process_global_state()
await service.run()

View File

@ -12,7 +12,7 @@ from chia.full_node.full_node_api import FullNodeAPI
from chia.rpc.full_node_rpc_api import FullNodeRpcApi
from chia.server.outbound_message import NodeType
from chia.server.start_service import RpcInfo, Service, async_run
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config, load_config_cli
from chia.util.default_root import DEFAULT_ROOT_PATH
from chia.util.ints import uint16
@ -72,11 +72,7 @@ async def async_main() -> int:
config[SERVICE_NAME] = service_config
overrides = service_config["network_overrides"]["constants"][service_config["selected_network"]]
updated_constants = DEFAULT_CONSTANTS.replace_str_to_bytes(**overrides)
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
service = create_full_node_service(DEFAULT_ROOT_PATH, config, updated_constants)
await service.setup_process_global_state()
await service.run()

View File

@ -10,7 +10,7 @@ from chia.rpc.harvester_rpc_api import HarvesterRpcApi
from chia.server.outbound_message import NodeType
from chia.server.start_service import RpcInfo, Service, async_run
from chia.types.peer_info import PeerInfo
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config, load_config_cli
from chia.util.default_root import DEFAULT_ROOT_PATH
@ -60,11 +60,7 @@ async def async_main() -> int:
config = load_config(DEFAULT_ROOT_PATH, "config.yaml")
service_config = load_config_cli(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME)
config[SERVICE_NAME] = service_config
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
farmer_peer = PeerInfo(service_config["farmer_peer"]["host"], service_config["farmer_peer"]["port"])
service = create_harvester_service(DEFAULT_ROOT_PATH, config, DEFAULT_CONSTANTS, farmer_peer)
await service.setup_process_global_state()

View File

@ -6,7 +6,7 @@ from chia.introducer.introducer import Introducer
from chia.introducer.introducer_api import IntroducerAPI
from chia.server.outbound_message import NodeType
from chia.server.start_service import Service, async_run
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config, load_config_cli
from chia.util.default_root import DEFAULT_ROOT_PATH
@ -50,11 +50,7 @@ async def async_main() -> int:
service_config = load_config_cli(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME)
config[SERVICE_NAME] = service_config
service = create_introducer_service(DEFAULT_ROOT_PATH, config)
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
await service.setup_process_global_state()
await service.run()

View File

@ -11,7 +11,7 @@ from chia.server.start_service import RpcInfo, Service, async_run
from chia.timelord.timelord import Timelord
from chia.timelord.timelord_api import TimelordAPI
from chia.types.peer_info import PeerInfo
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config, load_config_cli
from chia.util.default_root import DEFAULT_ROOT_PATH
@ -66,11 +66,7 @@ async def async_main() -> int:
config = load_config(DEFAULT_ROOT_PATH, "config.yaml")
service_config = load_config_cli(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME)
config[SERVICE_NAME] = service_config
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
service = create_timelord_service(DEFAULT_ROOT_PATH, config, DEFAULT_CONSTANTS)
await service.setup_process_global_state()
await service.run()

View File

@ -10,7 +10,7 @@ from chia.rpc.wallet_rpc_api import WalletRpcApi
from chia.server.outbound_message import NodeType
from chia.server.start_service import RpcInfo, Service, async_run
from chia.types.peer_info import PeerInfo
from chia.util.chia_logging import initialize_logging
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config_cli, load_config
from chia.util.default_root import DEFAULT_ROOT_PATH
from chia.util.keychain import Keychain
@ -99,11 +99,7 @@ async def async_main() -> int:
service_config["selected_network"] = "testnet0"
else:
constants = DEFAULT_CONSTANTS
initialize_logging(
service_name=SERVICE_NAME,
logging_config=service_config["logging"],
root_path=DEFAULT_ROOT_PATH,
)
initialize_service_logging(service_name=SERVICE_NAME, config=config)
service = create_wallet_service(DEFAULT_ROOT_PATH, config, constants)
await service.setup_process_global_state()
await service.run()

View File

@ -1,20 +1,49 @@
import logging
from pathlib import Path
from typing import Dict
from typing import Any, Dict, Optional
import colorlog
from concurrent_log_handler import ConcurrentRotatingFileHandler
from logging.handlers import SysLogHandler
from chia.cmds.init_funcs import chia_full_version_str
from chia.util.path import path_from_root
from chia.util.default_root import DEFAULT_ROOT_PATH
def initialize_logging(service_name: str, logging_config: Dict, root_path: Path):
log_path = path_from_root(root_path, logging_config.get("log_filename", "log/debug.log"))
log_date_format = "%Y-%m-%dT%H:%M:%S"
def get_beta_logging_config() -> Dict[str, Any]:
return {
"log_filename": f"{chia_full_version_str()}/chia-blockchain/beta.log",
"log_level": "DEBUG",
"log_stdout": False,
"log_maxfilesrotation": 100,
"log_maxbytesrotation": 100 * 1024 * 1024,
"log_use_gzip": True,
}
def get_file_log_handler(
formatter: logging.Formatter, root_path: Path, logging_config: Dict[str, object]
) -> ConcurrentRotatingFileHandler:
log_path = path_from_root(root_path, str(logging_config.get("log_filename", "log/debug.log")))
log_path.parent.mkdir(parents=True, exist_ok=True)
maxrotation = logging_config.get("log_maxfilesrotation", 7)
maxbytesrotation = logging_config.get("log_maxbytesrotation", 50 * 1024 * 1024)
use_gzip = logging_config.get("log_use_gzip", False)
handler = ConcurrentRotatingFileHandler(
log_path, "a", maxBytes=maxbytesrotation, backupCount=maxrotation, use_gzip=use_gzip
)
handler.setFormatter(formatter)
return handler
def initialize_logging(service_name: str, logging_config: Dict, root_path: Path, beta_root_path: Optional[Path] = None):
file_name_length = 33 - len(service_name)
log_date_format = "%Y-%m-%dT%H:%M:%S"
file_log_formatter = logging.Formatter(
fmt=f"%(asctime)s.%(msecs)03d {service_name} %(name)-{file_name_length}s: %(levelname)-8s %(message)s",
datefmt=log_date_format,
)
if logging_config["log_stdout"]:
handler = colorlog.StreamHandler()
handler.setFormatter(
@ -30,16 +59,7 @@ def initialize_logging(service_name: str, logging_config: Dict, root_path: Path)
logger.addHandler(handler)
else:
logger = logging.getLogger()
maxrotation = logging_config.get("log_maxfilesrotation", 7)
maxbytesrotation = logging_config.get("log_maxbytesrotation", 50 * 1024 * 1024)
handler = ConcurrentRotatingFileHandler(log_path, "a", maxBytes=maxbytesrotation, backupCount=maxrotation)
handler.setFormatter(
logging.Formatter(
fmt=f"%(asctime)s.%(msecs)03d {service_name} %(name)-{file_name_length}s: %(levelname)-8s %(message)s",
datefmt=log_date_format,
)
)
logger.addHandler(handler)
logger.addHandler(get_file_log_handler(file_log_formatter, root_path, logging_config))
if logging_config.get("log_syslog", False):
log_syslog_host = logging_config.get("log_syslog_host", "localhost")
@ -65,3 +85,24 @@ def initialize_logging(service_name: str, logging_config: Dict, root_path: Path)
logger.setLevel(logging.INFO)
else:
logger.setLevel(logging.INFO)
if beta_root_path is not None:
logger.addHandler(get_file_log_handler(file_log_formatter, beta_root_path, get_beta_logging_config()))
def initialize_service_logging(service_name: str, config: Dict[str, Any]) -> None:
logging_root_path = DEFAULT_ROOT_PATH
if service_name == "daemon":
# TODO: Maybe introduce a separate `daemon` section in the config instead of having `daemon_port`, `logging`
# and the daemon related stuff as top level entries.
logging_config = config["logging"]
else:
logging_config = config[service_name]["logging"]
beta_config = config.get("beta", {})
beta_config_path = beta_config.get("path") if beta_config.get("enabled", False) else None
initialize_logging(
service_name=service_name,
logging_config=logging_config,
root_path=logging_root_path,
beta_root_path=beta_config_path,
)

View File

@ -1,4 +1,5 @@
from enum import Enum
from pathlib import Path
from typing import Any, List
@ -281,3 +282,14 @@ class KeychainLabelExists(KeychainLabelError):
def __init__(self, label: str, fingerprint: int) -> None:
super().__init__(label, f"label {label!r} already exists for fingerprint {str(fingerprint)!r}")
self.fingerprint = fingerprint
##
# Miscellaneous errors
##
class InvalidPathError(Exception):
def __init__(self, path: Path, error_message: str):
super().__init__(f"{error_message}: {str(path)!r}")
self.path = path

View File

@ -119,6 +119,7 @@ logging: &logging
log_level: "WARNING" # Can be CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET
log_maxfilesrotation: 7 # Max files in rotation. Default value 7 if the key is not set
log_maxbytesrotation: 52428800 # Max bytes logged before rotating logs
log_use_gzip: False # Use gzip to compress rotated logs
log_syslog: False # If True, outputs to SysLog host and port specified
log_syslog_host: "localhost" # Send logging messages to a remote or local Unix syslog
log_syslog_port: 514 # UDP port of the remote or local Unix syslog

View File

@ -1,6 +1,8 @@
import dataclasses
from pathlib import Path
from typing import Any, Dict, Sequence, Union
from chia.util.errors import InvalidPathError
from chia.util.ints import uint16
from chia.util.streamable import Streamable, recurse_jsonify, streamable
@ -91,3 +93,15 @@ def get_list_or_len(list_in: Sequence[object], length: bool) -> Union[int, Seque
def dataclass_to_json_dict(instance: Any) -> Dict[str, Any]:
ret: Dict[str, Any] = recurse_jsonify(instance)
return ret
def validate_directory_writable(path: Path) -> None:
write_test_path = path / ".write_test"
try:
with write_test_path.open("w"):
pass
write_test_path.unlink()
except FileNotFoundError:
raise InvalidPathError(path, "Directory doesn't exist")
except OSError:
raise InvalidPathError(path, "Directory not writable")

View File

@ -0,0 +1,335 @@
import zipfile
from pathlib import Path
from typing import Callable, Optional
import pytest
from click.testing import CliRunner, Result
from chia.cmds.beta_funcs import default_beta_root_path
from chia.cmds.chia import cli
from chia.util.config import lock_and_load_config, save_config
def configure(root_path: Path, *args: str) -> Result:
return CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"configure",
*args,
],
)
def configure_interactive(root_path: Path, user_input: Optional[str] = None) -> Result:
return CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"configure",
],
input=user_input,
)
def enable(root_path: Path, *args: str) -> Result:
return CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"enable",
"--force",
*args,
],
)
def enable_interactive(root_path: Path, user_input: Optional[str] = None) -> Result:
return CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"enable",
],
input=user_input,
)
def prepare_submission(root_path: Path, user_input: Optional[str] = None) -> Result:
return CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"prepare_submission",
],
input=user_input,
)
def generate_example_submission_data(beta_root_path: Path, versions: int, logs: int) -> None:
for version in range(versions):
version_path = beta_root_path / str(version)
version_path.mkdir()
chia_blockchain_logs = version_path / "chia-blockchain"
plotting_logs = version_path / "plotting"
chia_blockchain_logs.mkdir()
plotting_logs.mkdir()
for i in range(logs):
with open(chia_blockchain_logs / f"beta_{i}.log", "w"):
pass
with open(chia_blockchain_logs / f"beta_{i + 10}.gz", "w"):
pass
with open(plotting_logs / f"plot_{i}.log", "w"):
pass
def generate_beta_config(root_path: Path, enabled: bool, beta_path: Path) -> None:
with lock_and_load_config(root_path, "config.yaml") as config:
config["beta"] = {
"enabled": enabled,
"path": str(beta_path),
}
save_config(root_path, "config.yaml", config)
@pytest.mark.parametrize("option", ["--path", "-p"])
def test_configure(root_path_populated_with_config: Path, option: str) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
beta_path.mkdir()
generate_beta_config(root_path, True, beta_path)
result = configure(root_path, option, str(beta_path))
assert result.exit_code == 0
with lock_and_load_config(root_path, "config.yaml") as config:
assert config["beta"] == {
"enabled": True,
"path": str(beta_path),
}
def test_configure_no_beta_config(root_path_populated_with_config: Path) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
beta_path.mkdir()
with lock_and_load_config(root_path, "config.yaml") as config:
assert "beta" not in config
result = configure(root_path, "--path", str(beta_path))
assert result.exit_code == 1
assert "beta test mode is not enabled, enable it first with `chia beta enable`" in result.output
@pytest.mark.parametrize("accept_existing_path", [True, False])
def test_beta_configure_interactive(root_path_populated_with_config: Path, accept_existing_path: bool) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
generate_beta_config(root_path, True, root_path_populated_with_config)
result = configure_interactive(root_path, f"{'' if accept_existing_path else str(beta_path)}\ny\n")
assert result.exit_code == 0
assert "beta config updated" in result.output
with lock_and_load_config(root_path, "config.yaml") as config:
assert config["beta"] == {
"enabled": True,
"path": str(root_path_populated_with_config if accept_existing_path else beta_path),
}
@pytest.mark.parametrize("option", ["--path", "-p"])
def test_beta_enable(root_path_populated_with_config: Path, option: str) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
beta_path.mkdir()
with lock_and_load_config(root_path, "config.yaml") as config:
assert "beta" not in config
result = enable(root_path, option, str(beta_path))
assert result.exit_code == 0
assert f"beta test mode enabled with path {str(beta_path)!r}" in result.output
with lock_and_load_config(root_path, "config.yaml") as config:
assert config["beta"] == {
"enabled": True,
"path": str(beta_path),
}
@pytest.mark.parametrize("enabled", [True, False])
def test_beta_enable_preconfigured(root_path_populated_with_config: Path, enabled: bool) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
beta_path.mkdir()
generate_beta_config(root_path, enabled, beta_path)
result = enable_interactive(root_path, "y\n")
if enabled:
assert result.exit_code == 1
assert "beta test mode is already enabled" in result.output
else:
assert result.exit_code == 0
assert f"beta test mode enabled with path {str(beta_path)!r}" in result.output
with lock_and_load_config(root_path, "config.yaml") as config:
assert config["beta"] == {
"enabled": True,
"path": str(beta_path),
}
@pytest.mark.parametrize("accept_default_path", [True, False])
def test_beta_enable_interactive(root_path_populated_with_config: Path, accept_default_path: bool) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
with lock_and_load_config(root_path, "config.yaml") as config:
assert "beta" not in config
result = enable_interactive(root_path, f"y\n{'' if accept_default_path else str(beta_path)}\ny\n")
assert result.exit_code == 0
assert (
f"beta test mode enabled with path {str(default_beta_root_path() if accept_default_path else beta_path)!r}"
in result.output
)
with lock_and_load_config(root_path, "config.yaml") as config:
assert config["beta"] == {
"enabled": True,
"path": str(default_beta_root_path() if accept_default_path else beta_path),
}
def test_beta_enable_interactive_decline_warning(root_path_populated_with_config: Path) -> None:
root_path = root_path_populated_with_config
with lock_and_load_config(root_path, "config.yaml") as config:
assert "beta" not in config
result = enable_interactive(root_path, "n\n")
assert result.exit_code == 1
assert result.output[-9:-1] == "Aborted!"
@pytest.mark.parametrize("write_test", [True, False])
@pytest.mark.parametrize("command", [configure, enable])
def test_beta_invalid_directories(
root_path_populated_with_config: Path, write_test: bool, command: Callable[[Path, str, str], Result]
) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
if write_test:
(beta_path / ".write_test").mkdir(parents=True) # `.write_test` is used in validate_directory_writable
if command == configure:
generate_beta_config(root_path, True, root_path_populated_with_config)
result = command(root_path, "--path", str(beta_path))
assert result.exit_code == 1
if write_test:
assert f"Directory not writable: {str(beta_path)!r}" in result.output
else:
assert f"Directory doesn't exist: {str(beta_path)!r}" in result.output
@pytest.mark.parametrize("enabled", [True, False])
def test_beta_disable(root_path_populated_with_config: Path, enabled: bool) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
generate_beta_config(root_path, enabled, beta_path)
result = CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"disable",
],
)
if enabled:
assert result.exit_code == 0
assert "beta test mode disabled" in result.output
else:
assert result.exit_code == 1
assert "beta test mode is not enabled" in result.output
with lock_and_load_config(root_path, "config.yaml") as config:
assert config["beta"] == {
"enabled": False,
"path": str(beta_path),
}
@pytest.mark.parametrize(
"versions, logs, choice, exit_code, output",
[
(0, 0, 1, 1, "No beta logs found"),
(1, 0, 1, 1, "No logs files found"),
(2, 10, 3, 1, "Invalid choice: 3"),
(2, 10, 0, 1, "Invalid choice: 0"),
(2, 10, -1, 1, "Invalid choice: -1"),
(4, 3, 2, 0, "Done. You can find the prepared submission data"),
],
)
def test_prepare_submission(
root_path_populated_with_config: Path, versions: int, logs: int, choice: int, exit_code: int, output: str
) -> None:
root_path = root_path_populated_with_config
beta_path = root_path / "beta"
beta_path.mkdir()
generate_beta_config(root_path, True, beta_path)
generate_example_submission_data(beta_path, versions, logs)
result = prepare_submission(root_path, f"{choice}\n")
assert result.exit_code == exit_code
assert output in result.output
if exit_code == 0:
submission_file = list(beta_path.rglob("*.zip"))[0]
assert submission_file.name.startswith(f"submission_{choice - 1}")
with zipfile.ZipFile(submission_file) as zip_file:
all_files = [Path(info.filename) for info in zip_file.filelist]
for version in range(versions):
chia_blockchain_logs = Path("chia-blockchain")
plotting_logs = Path("plotting")
for i in range(logs):
assert chia_blockchain_logs / f"beta_{i}.log" in all_files
assert chia_blockchain_logs / f"beta_{i + 10}.gz" in all_files
assert plotting_logs / f"plot_{i}.log" in all_files
@pytest.mark.parametrize(
"enabled, path",
[
(True, Path("path_1")),
(False, Path("path_2")),
],
)
def test_beta_status(root_path_populated_with_config: Path, enabled: bool, path: Path) -> None:
root_path = root_path_populated_with_config
generate_beta_config(root_path, enabled, path)
result = CliRunner().invoke(
cli,
[
"--root-path",
str(root_path),
"beta",
"status",
],
)
assert result.exit_code == 0
assert f"enabled: {enabled}" in result.output
assert f"path: {str(path)}" in result.output

View File

@ -1,5 +1,6 @@
import pytest
from chia.util.misc import format_bytes
from chia.util.errors import InvalidPathError
from chia.util.misc import format_bytes, validate_directory_writable
from chia.util.misc import format_minutes
@ -50,3 +51,20 @@ class TestMisc:
assert format_minutes(525600) == "1 year"
assert format_minutes(1007400) == "1 year and 11 months"
assert format_minutes(5256000) == "10 years"
def test_validate_directory_writable(tmp_path) -> None:
write_test_path = tmp_path / ".write_test" # `.write_test` is used in validate_directory_writable
validate_directory_writable(tmp_path)
assert not write_test_path.exists()
subdir = tmp_path / "subdir"
with pytest.raises(InvalidPathError, match="Directory doesn't exist") as exc_info:
validate_directory_writable(subdir)
assert exc_info.value.path == subdir
assert not write_test_path.exists()
(tmp_path / ".write_test").mkdir()
with pytest.raises(InvalidPathError, match="Directory not writable") as exc_info:
validate_directory_writable(tmp_path)
assert exc_info.value.path == tmp_path