diff --git a/README.md b/README.md index 3811429..2aad60d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ xrpld-netgen create:network [OPTIONS] - `--nodedb_type` - Database type: "Memory" or "NuDB" (default: "NuDB") - `--local` - Create local network without Docker (runs natively) - `--binary_name` - Custom xrpld binary name (default: "xrpld") +- `--name` - Optional cluster base name: creates `workspace/{name}-cluster` instead of defaulting to the build version (or Git branch id). Start with `xrpld-netgen up --name {name}-cluster`. - `--build_server` - Build server URL (auto-detected by protocol) **Examples:** @@ -239,12 +240,17 @@ xrpld-netgen up:standalone [OPTIONS] - `--server` - Build server URL (optional) - `--public_key` - Validator list public key - `--import_key` - Import validator list key +- `--name` - Optional directory slug: files go under `workspace/{protocol}-{name}` instead of `workspace/{protocol}-{version}`. Tear down with `down:standalone --name {protocol}-{name}`. **Examples:** ```bash # Create standalone with current version xrpld-netgen up:standalone --protocol xahau +# Custom folder name (same version, different workspace directory) +xrpld-netgen up:standalone --protocol xahau --name my-ledger +xrpld-netgen down:standalone --name xahau-my-ledger + # Create with specific version and IPFS xrpld-netgen up:standalone --protocol xahau --version 2025.7.9-release+1951 --ipfs diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 31654d2..eb44db8 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -2,12 +2,15 @@ # coding: utf-8 import pytest +from xrpld_netgen.main import standalone_workspace_dirname from xrpld_netgen.utils.misc import ( + docker_compose_top_level_name, generate_ports, get_node_port, sha512_half, get_node_db_path, get_relational_db, + sanitize_cluster_name, ) @@ -15,7 +18,9 @@ class TestGeneratePorts: """Test port generation for different node types""" def test_generate_ports_validator_first(self): - rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(1, "validator") + rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports( + 1, "validator" + ) assert rpc_public == 5107 assert rpc_admin == 5105 assert ws_public == 6108 @@ -23,7 +28,9 @@ def test_generate_ports_validator_first(self): assert peer == 51335 def test_generate_ports_validator_second(self): - rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(2, "validator") + rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports( + 2, "validator" + ) assert rpc_public == 5207 assert rpc_admin == 5205 assert ws_public == 6208 @@ -47,7 +54,9 @@ def test_generate_ports_peer_second(self): assert peer == 51255 def test_generate_ports_standalone(self): - rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(0, "standalone") + rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports( + 0, "standalone" + ) assert rpc_public == 5007 assert rpc_admin == 5005 assert ws_public == 6008 @@ -129,6 +138,36 @@ def test_get_node_db_path_rwdb_network(self): assert path == "/var/lib/xrpld/db" +class TestStandaloneWorkspaceDirname: + def test_default_uses_version(self): + assert ( + standalone_workspace_dirname("xahau", "2025.1.0", None) == "xahau-2025.1.0" + ) + + def test_custom_slug(self): + assert ( + standalone_workspace_dirname("xahau", "2025.1.0", "dev-a") == "xahau-dev-a" + ) + + +class TestDockerComposeTopLevelName: + def test_lowercase_and_dots(self): + assert docker_compose_top_level_name("My.Net") == "my-net" + + +class TestSanitizeClusterName: + def test_trims_and_keeps_safe_chars(self): + assert sanitize_cluster_name(" my-net_1 ") == "my-net_1" + + def test_rejects_path_separators(self): + with pytest.raises(ValueError, match="path"): + sanitize_cluster_name("a/b") + + def test_rejects_parent_dir(self): + with pytest.raises(ValueError, match="path"): + sanitize_cluster_name("x..y") + + class TestGetRelationalDb: """Test getting relational database backend configuration""" diff --git a/xrpld_netgen/cli.py b/xrpld_netgen/cli.py index 6378d12..5ea1275 100644 --- a/xrpld_netgen/cli.py +++ b/xrpld_netgen/cli.py @@ -40,6 +40,7 @@ create_standalone_binary, create_standalone_image, start_local, + standalone_workspace_dirname, ) from xrpld_netgen.network import ( create_network, @@ -67,6 +68,7 @@ _XRPL_RELEASE_FALLBACK: str = "3.1.1" _XAHAU_RELEASE_FALLBACK: str = "2025.7.9-release+1951" + def main(): parser = argparse.ArgumentParser( description="A python cli to build xrpld networks and standalone ledgers." @@ -213,6 +215,17 @@ def main(): help="The name of the xrpld binary for local networks (default: xrpld)", default="xrpld", ) + parser_cn.add_argument( + "--name", + type=str, + required=False, + default=None, + help=( + "Optional cluster base name: creates workspace/{name}-cluster " + "(default: build version or branch id). " + "Start with: xrpld-netgen up --name {name}-cluster" + ), + ) # update:node parser_un = subparsers.add_parser("update:node", help="Update Node Version") parser_un.add_argument( @@ -343,6 +356,17 @@ def main(): choices=["Memory", "NuDB"], default="NuDB", ) + parser_us.add_argument( + "--name", + type=str, + required=False, + default=None, + help=( + "Optional workspace directory slug: deploys to workspace/{protocol}-{name} " + "(default: build version). Use the same value with down:standalone --name " + "{protocol}-{name}." + ), + ) # down:standalone parser_ds = subparsers.add_parser("down:standalone", help="Down Standalone") parser_ds.add_argument("--name", required=False, help="The name of the network") @@ -406,7 +430,8 @@ def main(): print(f" - Network Name: {NAME}") print(f" - Protocol: {PROTOCOL}") print(f" - Build Version: {BUILD_VERSION}") - return run_stop([f"{basedir}/{PROTOCOL}-{BUILD_VERSION}/stop.sh"]) + rel = standalone_workspace_dirname(PROTOCOL, BUILD_VERSION, None) + return run_stop([f"{basedir}/{rel}/stop.sh"]) # MANAGE NETWORK/STANDALONE if args.command == "up": @@ -487,6 +512,7 @@ def main(): NODEDB_TYPE = args.nodedb_type LOCAL = args.local BINARY_NAME = args.binary_name + NETWORK_LABEL = (args.name or "").strip() or None import_vl_key: str = ( "ED87E0EA91AAFFA130B78B75D2CC3E53202AA1BD8AB3D5E7BAC530C8440E328501" @@ -537,6 +563,8 @@ def main(): if LOCAL: print(f" - Binary Name: {BINARY_NAME}") print(" - Deployment: Local (native processes, no Docker for nodes)") + if NETWORK_LABEL is not None: + print(f" - Cluster name: {NETWORK_LABEL}") if LOCAL: # Create local network without Docker @@ -553,6 +581,7 @@ def main(): GENESIS, QUORUM, NODEDB_TYPE, + NETWORK_LABEL, ) else: # Create traditional Docker-based network @@ -568,6 +597,7 @@ def main(): GENESIS, QUORUM, NODEDB_TYPE, + NETWORK_LABEL, ) if args.command == "update:node": @@ -615,6 +645,7 @@ def main(): BUILD_VERSION = args.version IPFS_SERVER = args.ipfs NODEDB_TYPE = args.nodedb_type + STANDALONE_LABEL = (args.name or "").strip() or None if PROTOCOL == "xahau" and not IMPORT_KEY: IMPORT_KEY: str = ( @@ -651,6 +682,8 @@ def main(): print(f" - Build Version: {BUILD_VERSION}") print(f" - IPFS Server: {IPFS_SERVER}") print(f" - Node DB: {NODEDB_TYPE}") + if STANDALONE_LABEL is not None: + print(f" - Standalone directory slug: {STANDALONE_LABEL}") if BUILD_TYPE == "image": create_standalone_image( @@ -664,6 +697,7 @@ def main(): BUILD_VERSION, IPFS_SERVER, NODEDB_TYPE, + STANDALONE_LABEL, ) else: create_standalone_binary( @@ -677,10 +711,14 @@ def main(): BUILD_VERSION, IPFS_SERVER, NODEDB_TYPE, + STANDALONE_LABEL, ) + deploy_dir = standalone_workspace_dirname( + PROTOCOL, BUILD_VERSION, STANDALONE_LABEL + ) run_start( - [f"{basedir}/{PROTOCOL}-{BUILD_VERSION}/start.sh"], + [f"{basedir}/{deploy_dir}/start.sh"], PROTOCOL, BUILD_VERSION, "standalone", diff --git a/xrpld_netgen/main.py b/xrpld_netgen/main.py index 27b83c9..34ac697 100644 --- a/xrpld_netgen/main.py +++ b/xrpld_netgen/main.py @@ -5,7 +5,7 @@ import yaml import shutil import json -from typing import List, Any, Dict +from typing import List, Any, Dict, Optional from xrpld_netgen.xrpld_cfg import gen_config, XrpldBuild from xrpld_netgen.utils.deploy_kit import ( @@ -27,6 +27,8 @@ read_json, get_node_db_path, get_relational_db, + sanitize_cluster_name, + docker_compose_top_level_name, ) from xrpld_netgen.libs.xrpld import ( update_amendments, @@ -45,6 +47,14 @@ os.makedirs(basedir, exist_ok=True) +def standalone_workspace_dirname( + protocol: str, version_key: str, network_name: Optional[str] = None +) -> str: + """Directory name under workspace: ``{protocol}-{slug}`` (slug defaults to version).""" + slug = sanitize_cluster_name(network_name) if network_name else version_key + return f"{protocol}-{slug}" + + def generate_validator_config(protocol: str, network: str) -> str: try: config = read_json(f"{package_dir}/deploykit/config.json") @@ -62,7 +72,8 @@ def generate_validator_config(protocol: str, network: str) -> str: def create_xrpl_standalone_folder( binary: bool, - name: str, + deploy_slug: str, + binary_tag: str, image: str, feature_content: str, network_id: int, @@ -73,7 +84,7 @@ def create_xrpl_standalone_folder( log_level: str = "trace", nodedb_type: str = "NuDB", ): - cfg_path = f"{basedir}/{protocol}-{name}/config" + cfg_path = f"{basedir}/{protocol}-{deploy_slug}/config" rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(0, "standalone") vl_config: Dict[str, Any] = generate_validator_config(protocol, net_type) @@ -89,7 +100,7 @@ def create_xrpl_standalone_folder( configs: List[XrpldBuild] = gen_config( False, protocol, - name, + deploy_slug, vl_config["network_id"], 0, rpc_public, @@ -113,7 +124,7 @@ def create_xrpl_standalone_folder( vl_config["ips"], vl_config["ips_fixed"], ) - os.makedirs(f"{basedir}/{protocol}-{name}/config", exist_ok=True) + os.makedirs(f"{basedir}/{protocol}-{deploy_slug}/config", exist_ok=True) save_local_config(protocol, cfg_path, configs[0].data, configs[1].data) print(f"✅ {bcolors.CYAN}Creating config") @@ -121,7 +132,7 @@ def create_xrpl_standalone_folder( print(json.dumps(features_json, indent=4)) genesis_json: Any = update_amendments(features_json, protocol) write_file( - f"{basedir}/{protocol}-{name}/genesis.json", + f"{basedir}/{protocol}-{deploy_slug}/genesis.json", json.dumps(genesis_json, indent=4, sort_keys=True), ) print(f"✅ {bcolors.CYAN}Updating features") @@ -130,7 +141,7 @@ def create_xrpl_standalone_folder( protocol, False, binary, - name, + binary_tag, image, rpc_public, rpc_admin, @@ -141,12 +152,12 @@ def create_xrpl_standalone_folder( "", "-a", ) - with open(f"{basedir}/{protocol}-{name}/Dockerfile", "w") as file: + with open(f"{basedir}/{protocol}-{deploy_slug}/Dockerfile", "w") as file: file.write(dockerfile) shutil.copyfile( f"{package_dir}/deploykit/{protocol}.entrypoint", - f"{basedir}/{protocol}-{name}/entrypoint", + f"{basedir}/{protocol}-{deploy_slug}/entrypoint", ) print(f"✅ {bcolors.CYAN}Building docker container...") pwd_str: str = "${PWD}" @@ -184,9 +195,13 @@ def create_standalone_image( build_name: str, add_ipfs: bool = False, nodedb_type: str = "NuDB", + network_name: Optional[str] = None, ) -> None: - name: str = build_name - os.makedirs(f"{basedir}/{protocol}-{name}", exist_ok=True) + binary_tag: str = build_name + deploy_slug: str = ( + sanitize_cluster_name(network_name) if network_name else binary_tag + ) + os.makedirs(f"{basedir}/{protocol}-{deploy_slug}", exist_ok=True) owner = "XRPLF" repo = "rippled" content_bytes = download_file_at_commit_or_tag( @@ -196,7 +211,8 @@ def create_standalone_image( image: str = f"{build_system}/xrpld:{build_name}" create_xrpl_standalone_folder( False, - name, + deploy_slug, + binary_tag, image, content, network_id, @@ -239,23 +255,29 @@ def create_standalone_image( "services": services, "networks": {"standalone-network": {"driver": "bridge"}}, } + if network_name: + compose = { + "name": docker_compose_top_level_name(deploy_slug), + **compose, + } - with open(f"{basedir}/{protocol}-{name}/docker-compose.yml", "w") as f: + with open(f"{basedir}/{protocol}-{deploy_slug}/docker-compose.yml", "w") as f: yaml.dump(compose, f, default_flow_style=False) write_file( - f"{basedir}/{protocol}-{name}/start.sh", - build_start_sh(basedir, protocol, name), # noqa: E501 + f"{basedir}/{protocol}-{deploy_slug}/start.sh", + build_start_sh(basedir, protocol, deploy_slug), # noqa: E501 ) - os.chmod(f"{basedir}/{protocol}-{name}/start.sh", 0o755) - stop_sh_content: str = build_stop_sh(basedir, protocol, name, 0, 0, True) - write_file(f"{basedir}/{protocol}-{name}/stop.sh", stop_sh_content) - os.chmod(f"{basedir}/{protocol}-{name}/stop.sh", 0o755) + os.chmod(f"{basedir}/{protocol}-{deploy_slug}/start.sh", 0o755) + stop_sh_content: str = build_stop_sh(basedir, protocol, deploy_slug, 0, 0, True) + write_file(f"{basedir}/{protocol}-{deploy_slug}/stop.sh", stop_sh_content) + os.chmod(f"{basedir}/{protocol}-{deploy_slug}/stop.sh", 0o755) def create_xahau_standalone_folder( binary: bool, - name: str, + deploy_slug: str, + binary_tag: str, image: str, feature_content: str, network_id: int, @@ -266,7 +288,7 @@ def create_xahau_standalone_folder( log_level: str = "trace", nodedb_type: str = "NuDB", ): - cfg_path = f"{basedir}/{protocol}-{name}/config" + cfg_path = f"{basedir}/{protocol}-{deploy_slug}/config" rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(0, "standalone") vl_config: Dict[str, Any] = generate_validator_config(protocol, net_type) @@ -282,7 +304,7 @@ def create_xahau_standalone_folder( configs: List[XrpldBuild] = gen_config( False, protocol, - name, + deploy_slug, vl_config["network_id"], 0, rpc_public, @@ -306,14 +328,14 @@ def create_xahau_standalone_folder( vl_config["ips"], vl_config["ips_fixed"], ) - os.makedirs(f"{basedir}/{protocol}-{name}/config", exist_ok=True) + os.makedirs(f"{basedir}/{protocol}-{deploy_slug}/config", exist_ok=True) save_local_config(protocol, cfg_path, configs[0].data, configs[1].data) print(f"✅ {bcolors.CYAN}Creating config") features_json: Dict[str, Any] = parse_amendments(feature_content) genesis_json: Any = update_amendments(features_json, protocol) write_file( - f"{basedir}/{protocol}-{name}/genesis.json", + f"{basedir}/{protocol}-{deploy_slug}/genesis.json", json.dumps(genesis_json, indent=4, sort_keys=True), ) print(f"✅ {bcolors.CYAN}Updating features") @@ -324,7 +346,7 @@ def create_xahau_standalone_folder( protocol, False, binary, - name, + binary_tag, image, rpc_public, rpc_admin, @@ -335,12 +357,12 @@ def create_xahau_standalone_folder( "", "-a", ) - with open(f"{basedir}/{protocol}-{name}/Dockerfile", "w") as file: + with open(f"{basedir}/{protocol}-{deploy_slug}/Dockerfile", "w") as file: file.write(dockerfile) shutil.copyfile( f"{package_dir}/deploykit/{protocol}.entrypoint", - f"{basedir}/{protocol}-{name}/entrypoint", + f"{basedir}/{protocol}-{deploy_slug}/entrypoint", ) print(f"✅ {bcolors.CYAN}Building docker container...") pwd_str: str = "${PWD}" @@ -378,23 +400,32 @@ def create_standalone_binary( build_version: str, add_ipfs: bool = False, nodedb_type: str = "NuDB", + network_name: Optional[str] = None, ) -> None: - name: str = build_version - os.makedirs(f"{basedir}/{protocol}-{name}", exist_ok=True) + binary_tag: str = build_version + deploy_slug: str = ( + sanitize_cluster_name(network_name) if network_name else binary_tag + ) + os.makedirs(f"{basedir}/{protocol}-{deploy_slug}", exist_ok=True) # Usage owner = "Xahau" repo = "xahaud" commit_hash = get_commit_hash_from_server_version(build_server, build_version) content_bytes = download_file_at_commit_or_tag( - owner, repo, commit_hash, "src/ripple/protocol/impl/Feature.cpp", "include/xrpl/protocol/detail/features.macro" + owner, + repo, + commit_hash, + "src/ripple/protocol/impl/Feature.cpp", + "include/xrpl/protocol/detail/features.macro", ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{protocol}-{name}/{protocol}d.{name}") + download_binary(url, f"{basedir}/{protocol}-{deploy_slug}/{protocol}d.{binary_tag}") image: str = "ubuntu:jammy" create_xahau_standalone_folder( True, - name, + deploy_slug, + binary_tag, image, content, network_id, @@ -437,18 +468,23 @@ def create_standalone_binary( "services": services, "networks": {"standalone-network": {"driver": "bridge"}}, } + if network_name: + compose = { + "name": docker_compose_top_level_name(deploy_slug), + **compose, + } - with open(f"{basedir}/{protocol}-{name}/docker-compose.yml", "w") as f: + with open(f"{basedir}/{protocol}-{deploy_slug}/docker-compose.yml", "w") as f: yaml.dump(compose, f, default_flow_style=False) write_file( - f"{basedir}/{protocol}-{name}/start.sh", - build_start_sh(basedir, protocol, name), # noqa: E501 + f"{basedir}/{protocol}-{deploy_slug}/start.sh", + build_start_sh(basedir, protocol, deploy_slug), # noqa: E501 ) - os.chmod(f"{basedir}/{protocol}-{name}/start.sh", 0o755) - stop_sh_content: str = build_stop_sh(basedir, protocol, name, 0, 0, True) - write_file(f"{basedir}/{protocol}-{name}/stop.sh", stop_sh_content) - os.chmod(f"{basedir}/{protocol}-{name}/stop.sh", 0o755) + os.chmod(f"{basedir}/{protocol}-{deploy_slug}/start.sh", 0o755) + stop_sh_content: str = build_stop_sh(basedir, protocol, deploy_slug, 0, 0, True) + write_file(f"{basedir}/{protocol}-{deploy_slug}/stop.sh", stop_sh_content) + os.chmod(f"{basedir}/{protocol}-{deploy_slug}/stop.sh", 0o755) def create_local_folder( @@ -512,7 +548,7 @@ def create_local_folder( cpp_path = "../src/ripple/protocol/impl/Feature.cpp" macro_path = "../include/xrpl/protocol/detail/features.macro" features_path = cpp_path if os.path.exists(cpp_path) else macro_path - + if protocol == "xahau": content: str = get_feature_lines_from_path(features_path) features_json: Dict[str, Any] = parse_amendments(content) diff --git a/xrpld_netgen/network.py b/xrpld_netgen/network.py index 92389e5..12d7995 100644 --- a/xrpld_netgen/network.py +++ b/xrpld_netgen/network.py @@ -5,7 +5,7 @@ import yaml import shutil import json -from typing import List, Any, Dict +from typing import List, Any, Dict, Optional from dotenv import load_dotenv from xrpld_netgen.xrpld_cfg import gen_config, XrpldBuild @@ -20,6 +20,7 @@ build_network_start_sh, build_local_network_start_sh, build_local_network_stop_sh, + network_docker_binary_basename, ) from xrpld_netgen.libs.github import ( get_commit_hash_from_server_version, @@ -38,6 +39,8 @@ read_json, get_node_db_path, get_relational_db, + sanitize_cluster_name, + docker_compose_top_level_name, ) from xrpld_netgen.libs.xrpld import ( @@ -76,7 +79,8 @@ def generate_validator_config(protocol: str, network: str): def create_node_folders( binary: bool, - name: str, + cluster_slug: str, + binary_tag: str, image: str, feature_content: str, num_validators: int, @@ -93,7 +97,7 @@ def create_node_folders( nodedb_type: str = "NuDB", ): # Create cluster directory and keystore inside it - cluster_dir = f"{basedir}/{name}-cluster" + cluster_dir = f"{basedir}/{cluster_slug}-cluster" os.makedirs(cluster_dir, exist_ok=True) # Create directories for validator nodes @@ -139,7 +143,7 @@ def create_node_folders( for i in range(1, num_validators + 1): ips_dir = ips[i - 1] if ansible else f"vnode{i}" node_dir = f"vnode{i}" - cfg_path = f"{basedir}/{name}-cluster/{node_dir}/config" + cfg_path = f"{basedir}/{cluster_slug}-cluster/{node_dir}/config" # GENERATE PORTS rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports( i, "validator" @@ -148,7 +152,7 @@ def create_node_folders( configs: List[XrpldBuild] = gen_config( ansible, protocol, - name, + cluster_slug, network_id, i, rpc_public, @@ -173,8 +177,10 @@ def create_node_folders( [ips for ips in ips_fixed if ips != f"{ips_dir} {peer}"], ) - os.makedirs(f"{basedir}/{name}-cluster/{node_dir}", exist_ok=True) - os.makedirs(f"{basedir}/{name}-cluster/{node_dir}/config", exist_ok=True) + os.makedirs(f"{basedir}/{cluster_slug}-cluster/{node_dir}", exist_ok=True) + os.makedirs( + f"{basedir}/{cluster_slug}-cluster/{node_dir}/config", exist_ok=True + ) save_local_config(protocol, cfg_path, configs[0].data, configs[1].data) print(f"✅ {bcolors.CYAN}Created validator: {i} config") @@ -191,12 +197,12 @@ def create_node_folders( genesis_json: Any = update_amendments(features_json, protocol) write_file( - f"{basedir}/{name}-cluster/{node_dir}/genesis.json", + f"{basedir}/{cluster_slug}-cluster/{node_dir}/genesis.json", json.dumps(genesis_json, indent=4, sort_keys=True), ) write_file( - f"{basedir}/{name}-cluster/{node_dir}/features.json", + f"{basedir}/{cluster_slug}-cluster/{node_dir}/features.json", json.dumps(features_json, indent=4, sort_keys=True), ) @@ -206,7 +212,7 @@ def create_node_folders( protocol, True, binary, - name, + binary_tag, image, rpc_public, rpc_admin, @@ -217,12 +223,14 @@ def create_node_folders( quorum, "", ) - with open(f"{basedir}/{name}-cluster/{node_dir}/Dockerfile", "w") as file: + with open( + f"{basedir}/{cluster_slug}-cluster/{node_dir}/Dockerfile", "w" + ) as file: file.write(dockerfile) shutil.copyfile( f"{package_dir}/deploykit/network.entrypoint", - f"{basedir}/{name}-cluster/{node_dir}/entrypoint", + f"{basedir}/{cluster_slug}-cluster/{node_dir}/entrypoint", ) print(f"✅ {bcolors.CYAN}Built validator: {i} docker container...") @@ -246,17 +254,17 @@ def create_node_folders( f"./vnode{i}/log:/opt/ripple/log", f"./vnode{i}/lib:/opt/ripple/lib", ], - "networks": [f"{name}-network"], + "networks": [f"{cluster_slug}-network"], } for i in range(1, num_peers + 1): node_dir = f"pnode{i}" - cfg_path = f"{basedir}/{name}-cluster/{node_dir}/config" + cfg_path = f"{basedir}/{cluster_slug}-cluster/{node_dir}/config" rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(i, "peer") configs: List[XrpldBuild] = gen_config( ansible, protocol, - name, + cluster_slug, network_id, i, rpc_public, @@ -281,8 +289,10 @@ def create_node_folders( ips_fixed, ) # print(f'CONFIG: {configs}') - os.makedirs(f"{basedir}/{name}-cluster/{node_dir}", exist_ok=True) - os.makedirs(f"{basedir}/{name}-cluster/{node_dir}/config", exist_ok=True) + os.makedirs(f"{basedir}/{cluster_slug}-cluster/{node_dir}", exist_ok=True) + os.makedirs( + f"{basedir}/{cluster_slug}-cluster/{node_dir}/config", exist_ok=True + ) save_local_config(protocol, cfg_path, configs[0].data, configs[1].data) print(f"✅ {bcolors.CYAN}Created peer: {i} config") @@ -296,12 +306,12 @@ def create_node_folders( genesis_json: Any = update_amendments(features_json, protocol) write_file( - f"{basedir}/{name}-cluster/{node_dir}/genesis.json", + f"{basedir}/{cluster_slug}-cluster/{node_dir}/genesis.json", json.dumps(genesis_json, indent=4, sort_keys=True), ) write_file( - f"{basedir}/{name}-cluster/{node_dir}/features.json", + f"{basedir}/{cluster_slug}-cluster/{node_dir}/features.json", json.dumps(features_json, indent=4, sort_keys=True), ) @@ -311,7 +321,7 @@ def create_node_folders( protocol, True, binary, - name, + binary_tag, image, rpc_public, rpc_admin, @@ -322,12 +332,14 @@ def create_node_folders( quorum, "", ) - with open(f"{basedir}/{name}-cluster/{node_dir}/Dockerfile", "w") as file: + with open( + f"{basedir}/{cluster_slug}-cluster/{node_dir}/Dockerfile", "w" + ) as file: file.write(dockerfile) shutil.copyfile( f"{package_dir}/deploykit/network.entrypoint", - f"{basedir}/{name}-cluster/{node_dir}/entrypoint", + f"{basedir}/{cluster_slug}-cluster/{node_dir}/entrypoint", ) print(f"✅ {bcolors.CYAN}Built peer: {i} docker container...") @@ -351,7 +363,7 @@ def create_node_folders( f"./pnode{i}/log:/opt/ripple/log", f"./pnode{i}/lib:/opt/ripple/lib", ], - "networks": [f"{name}-network"], + "networks": [f"{cluster_slug}-network"], } return manifests @@ -369,32 +381,50 @@ def create_network( genesis: bool = False, quorum: int = None, nodedb_type: str = "NuDB", + network_name: Optional[str] = None, ) -> None: if protocol == "xahau": - name: str = build_version - os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) + name_binary = build_version + cluster_slug = ( + sanitize_cluster_name(network_name) if network_name else name_binary + ) + os.makedirs(f"{basedir}/{cluster_slug}-cluster", exist_ok=True) # Usage owner = "Xahau" repo = "xahaud" commit_hash = get_commit_hash_from_server_version(build_server, build_version) content_bytes = download_file_at_commit_or_tag( - owner, repo, commit_hash, "src/ripple/protocol/impl/Feature.cpp", "include/xrpl/protocol/detail/features.macro" + owner, + repo, + commit_hash, + "src/ripple/protocol/impl/Feature.cpp", + "include/xrpl/protocol/detail/features.macro", ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{name}-cluster/xrpld.{build_version}") + bin_root = network_docker_binary_basename("xahau", name_binary) + download_binary(url, f"{basedir}/{cluster_slug}-cluster/{bin_root}") image: str = "ubuntu:jammy" - if protocol == "xrpl": + elif protocol == "xrpl": if build_server.startswith("https://github.com/"): owner: str = build_server.split("https://github.com/")[1].split("/")[0] # Extract branch name from URL (supports both rippled and xrpld repo names) - name: str = build_server.split(f"https://github.com/{owner}/")[1] - name = name.split("/tree/")[1] if "/tree/" in name else name - name = name.replace("/", "-") - os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) + name_binary = build_server.split(f"https://github.com/{owner}/")[1] + name_binary = ( + name_binary.split("/tree/")[1] + if "/tree/" in name_binary + else name_binary + ) + name_binary = name_binary.replace("/", "-") + cluster_slug: str = ( + sanitize_cluster_name(network_name) if network_name else name_binary + ) + os.makedirs(f"{basedir}/{cluster_slug}-cluster", exist_ok=True) repo = "rippled" - copy_file("./xrpld", f"{basedir}/{name}-cluster/xrpld.{name}") + copy_file( + "./xrpld", f"{basedir}/{cluster_slug}-cluster/xrpld.{name_binary}" + ) content_bytes = download_file_at_commit_or_tag( owner, repo, @@ -404,8 +434,11 @@ def create_network( content = get_feature_lines_from_content(content_bytes) image: str = "ubuntu:jammy" else: - name: str = build_version - os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) + name_binary = build_version + cluster_slug = ( + sanitize_cluster_name(network_name) if network_name else name_binary + ) + os.makedirs(f"{basedir}/{cluster_slug}-cluster", exist_ok=True) owner = "XRPLF" repo = "rippled" content_bytes = download_file_at_commit_or_tag( @@ -413,10 +446,12 @@ def create_network( ) content = get_feature_lines_from_content(content_bytes) image: str = f"{build_server}/{build_version}" + else: + raise ValueError(f"Unsupported protocol: {protocol}") # Change to cluster directory for VL key creation original_dir = os.getcwd() - os.chdir(f"{basedir}/{name}-cluster") + os.chdir(f"{basedir}/{cluster_slug}-cluster") try: client = PublisherClient() @@ -468,7 +503,8 @@ def create_network( keys = client.get_keys() manifests: List[str] = create_node_folders( True, - name, + cluster_slug, + name_binary, image, content, num_validators, @@ -492,7 +528,7 @@ def create_network( }, "container_name": "vl", "ports": ["80:80"], - "networks": [f"{name}-network"], + "networks": [f"{cluster_slug}-network"], "healthcheck": { "test": ["CMD", "curl", "-f", "http://localhost/vl.json"], "interval": "5s", @@ -510,43 +546,50 @@ def create_network( f"VUE_APP_WSS_ENDPOINT=ws://0.0.0.0:{6016}", ], "ports": ["4000:4000"], - "networks": [f"{name}-network"], + "networks": [f"{cluster_slug}-network"], } compose = { "version": "3.9", "services": services, - "networks": {f"{name}-network": {"driver": "bridge"}}, + "networks": {f"{cluster_slug}-network": {"driver": "bridge"}}, } - with open(f"{basedir}/{name}-cluster/docker-compose.yml", "w") as f: + if network_name: + compose = { + "name": docker_compose_top_level_name(cluster_slug), + **compose, + } + with open(f"{basedir}/{cluster_slug}-cluster/docker-compose.yml", "w") as f: yaml.dump(compose, f, default_flow_style=False) write_file( - f"{basedir}/{name}-cluster/start.sh", - build_network_start_sh(name, num_validators, num_peers), # noqa: E501 + f"{basedir}/{cluster_slug}-cluster/start.sh", + build_network_start_sh(protocol, name_binary, num_validators, num_peers), # noqa: E501 ) stop_sh_content: str = build_network_stop_sh( - name, + protocol, + name_binary, num_validators, num_peers, ) - write_file(f"{basedir}/{name}-cluster/stop.sh", stop_sh_content) + write_file(f"{basedir}/{cluster_slug}-cluster/stop.sh", stop_sh_content) - os.makedirs(f"{basedir}/{name}-cluster/vl", exist_ok=True) + os.makedirs(f"{basedir}/{cluster_slug}-cluster/vl", exist_ok=True) for manifest in manifests: client.add_validator(manifest) - client.sign_unl(f"{basedir}/{name}-cluster/vl/vl.json") + client.sign_unl(f"{basedir}/{cluster_slug}-cluster/vl/vl.json") shutil.copyfile( f"{package_dir}/deploykit/nginx.dockerfile", - f"{basedir}/{name}-cluster/vl/Dockerfile", + f"{basedir}/{cluster_slug}-cluster/vl/Dockerfile", ) finally: # Change back to original directory os.chdir(original_dir) - os.chmod(f"{basedir}/{name}-cluster/start.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/stop.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/xrpld.{name}", 0o755) + bin_chmod = network_docker_binary_basename(protocol, name_binary) + os.chmod(f"{basedir}/{cluster_slug}-cluster/start.sh", 0o755) + os.chmod(f"{basedir}/{cluster_slug}-cluster/stop.sh", 0o755) + os.chmod(f"{basedir}/{cluster_slug}-cluster/{bin_chmod}", 0o755) def update_node_binary( @@ -593,9 +636,7 @@ def enable_node_amendment( port: int = get_node_port(int(node_id), node_type) json_str: str = json.dumps(command) escaped_str = json_str.replace('"', '\\"') - command: str = ( - f'curl -X POST -H "Content-Type: application/json" -d "{escaped_str}" http://localhost:{port}' # noqa: E501 - ) + command: str = f'curl -X POST -H "Content-Type: application/json" -d "{escaped_str}" http://localhost:{port}' # noqa: E501 run_command(f"{basedir}/{name}", command) @@ -613,32 +654,50 @@ def create_ansible( nodedb_type: str = "NuDB", vips: List[str] = [], pips: List[str] = [], + network_name: Optional[str] = None, ) -> None: if protocol == "xahau": - name: str = build_version - os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) + name_binary = build_version + cluster_slug = ( + sanitize_cluster_name(network_name) if network_name else name_binary + ) + os.makedirs(f"{basedir}/{cluster_slug}-cluster", exist_ok=True) # Usage owner = "Xahau" repo = "xahaud" commit_hash = get_commit_hash_from_server_version(build_server, build_version) content_bytes = download_file_at_commit_or_tag( - owner, repo, commit_hash, "src/ripple/protocol/impl/Feature.cpp", "include/xrpl/protocol/detail/features.macro" + owner, + repo, + commit_hash, + "src/ripple/protocol/impl/Feature.cpp", + "include/xrpl/protocol/detail/features.macro", ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{name}-cluster/xrpld.{build_version}") + bin_root = network_docker_binary_basename("xahau", name_binary) + download_binary(url, f"{basedir}/{cluster_slug}-cluster/{bin_root}") image: str = "ubuntu:jammy" - if protocol == "xrpl": + elif protocol == "xrpl": if build_server.startswith("https://github.com/"): repo: str = "rippled" owner: str = build_server.split("https://github.com/")[1].split("/")[0] # Extract branch name from URL (supports both rippled and xrpld repo names) - name: str = build_server.split(f"https://github.com/{owner}/")[1] - name = name.split("/tree/")[1] if "/tree/" in name else name - name = name.replace("/", "-") - os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) - copy_file("./xrpld", f"{basedir}/{name}-cluster/xrpld.{name}") + name_binary = build_server.split(f"https://github.com/{owner}/")[1] + name_binary = ( + name_binary.split("/tree/")[1] + if "/tree/" in name_binary + else name_binary + ) + name_binary = name_binary.replace("/", "-") + cluster_slug: str = ( + sanitize_cluster_name(network_name) if network_name else name_binary + ) + os.makedirs(f"{basedir}/{cluster_slug}-cluster", exist_ok=True) + copy_file( + "./xrpld", f"{basedir}/{cluster_slug}-cluster/xrpld.{name_binary}" + ) content_bytes = download_file_at_commit_or_tag( owner, repo, @@ -648,8 +707,11 @@ def create_ansible( content = get_feature_lines_from_content(content_bytes) image: str = "ubuntu:jammy" else: - name: str = build_version - os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) + name_binary = build_version + cluster_slug = ( + sanitize_cluster_name(network_name) if network_name else name_binary + ) + os.makedirs(f"{basedir}/{cluster_slug}-cluster", exist_ok=True) owner = "XRPLF" repo = "rippled" content_bytes = download_file_at_commit_or_tag( @@ -657,10 +719,12 @@ def create_ansible( ) content = get_feature_lines_from_content(content_bytes) image: str = f"{build_server}/{build_version}" + else: + raise ValueError(f"Unsupported protocol: {protocol}") # Change to cluster directory for VL key creation original_dir = os.getcwd() - os.chdir(f"{basedir}/{name}-cluster") + os.chdir(f"{basedir}/{cluster_slug}-cluster") try: client = PublisherClient() @@ -712,7 +776,8 @@ def create_ansible( keys = client.get_keys() manifests: List[str] = create_node_folders( True, - name, + cluster_slug, + name_binary, image, content, num_validators, @@ -736,7 +801,7 @@ def create_ansible( }, "container_name": "vl", "ports": ["80:80"], - "networks": [f"{name}-network"], + "networks": [f"{cluster_slug}-network"], "healthcheck": { "test": ["CMD", "curl", "-f", "http://localhost/vl.json"], "interval": "5s", @@ -753,65 +818,74 @@ def create_ansible( "PORT=4000", ], "ports": ["4000:4000"], - "networks": [f"{name}-network"], + "networks": [f"{cluster_slug}-network"], } compose = { "version": "3.9", "services": services, - "networks": {f"{name}-network": {"driver": "bridge"}}, + "networks": {f"{cluster_slug}-network": {"driver": "bridge"}}, } - with open(f"{basedir}/{name}-cluster/docker-compose.yml", "w") as f: + if network_name: + compose = { + "name": docker_compose_top_level_name(cluster_slug), + **compose, + } + with open(f"{basedir}/{cluster_slug}-cluster/docker-compose.yml", "w") as f: yaml.dump(compose, f, default_flow_style=False) write_file( - f"{basedir}/{name}-cluster/start.sh", - build_network_start_sh(name, num_validators, num_peers), # noqa: E501 + f"{basedir}/{cluster_slug}-cluster/start.sh", + build_network_start_sh(protocol, name_binary, num_validators, num_peers), # noqa: E501 ) - stop_sh_content: str = build_network_stop_sh(name, num_validators, num_peers) - write_file(f"{basedir}/{name}-cluster/stop.sh", stop_sh_content) + stop_sh_content: str = build_network_stop_sh( + protocol, name_binary, num_validators, num_peers + ) + write_file(f"{basedir}/{cluster_slug}-cluster/stop.sh", stop_sh_content) - os.makedirs(f"{basedir}/{name}-cluster/vl", exist_ok=True) + os.makedirs(f"{basedir}/{cluster_slug}-cluster/vl", exist_ok=True) for manifest in manifests: client.add_validator(manifest) - client.sign_unl(f"{basedir}/{name}-cluster/vl/vl.json") + client.sign_unl(f"{basedir}/{cluster_slug}-cluster/vl/vl.json") shutil.copyfile( f"{package_dir}/deploykit/nginx.dockerfile", - f"{basedir}/{name}-cluster/vl/Dockerfile", + f"{basedir}/{cluster_slug}-cluster/vl/Dockerfile", ) finally: # Change back to original directory os.chdir(original_dir) - os.chmod(f"{basedir}/{name}-cluster/start.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/stop.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/xrpld.{name}", 0o755) + bin_chmod = network_docker_binary_basename(protocol, name_binary) + os.chmod(f"{basedir}/{cluster_slug}-cluster/start.sh", 0o755) + os.chmod(f"{basedir}/{cluster_slug}-cluster/stop.sh", 0o755) + os.chmod(f"{basedir}/{cluster_slug}-cluster/{bin_chmod}", 0o755) - os.makedirs(f"{basedir}/{name}-cluster/ansible", exist_ok=True) - os.makedirs(f"{basedir}/{name}-cluster/ansible/host_vars", exist_ok=True) + os.makedirs(f"{basedir}/{cluster_slug}-cluster/ansible", exist_ok=True) + os.makedirs(f"{basedir}/{cluster_slug}-cluster/ansible/host_vars", exist_ok=True) shutil.copytree( f"{package_dir}/deploykit/ansible", - f"{basedir}/{name}-cluster/ansible", + f"{basedir}/{cluster_slug}-cluster/ansible", dirs_exist_ok=True, ) image_name: str = build_version.replace("-", ".") image_name: str = image_name.replace("+", ".") ssh_port: int = os.environ.get("SSH_PORT", 20) + ansible_docker_bin = network_docker_binary_basename(protocol, name_binary) for k, v in services.items(): if k[:5] == "vnode": index: int = int(k[5:]) c_name: str = v["container_name"] ports: List[str] = v["ports"] vars = DockerVars( - f"{basedir}/{name}-cluster/{c_name}/config/", + f"{basedir}/{cluster_slug}-cluster/{c_name}/config/", ssh_port, [ - f'RPC_PUBLIC: {ports[0].split(":")[0]}', - f'RPC_ADMIN: {ports[1].split(":")[0]}', - f'WS_PUBLIC: {ports[2].split(":")[0]}', - f'WS_ADMIN: {ports[3].split(":")[0]}', - f'PEER: {ports[4].split(":")[0]}', + f"RPC_PUBLIC: {ports[0].split(':')[0]}", + f"RPC_ADMIN: {ports[1].split(':')[0]}", + f"WS_PUBLIC: {ports[2].split(':')[0]}", + f"WS_ADMIN: {ports[3].split(':')[0]}", + f"PEER: {ports[4].split(':')[0]}", ], int(ports[2].split(":")[-1]), int(ports[4].split(":")[-1]), @@ -833,39 +907,41 @@ def create_ansible( ], ) create_ansible_vars_file( - f"{basedir}/{name}-cluster/ansible/host_vars", vips[index - 1], vars + f"{basedir}/{cluster_slug}-cluster/ansible/host_vars", + vips[index - 1], + vars, ) run_command( - f"{basedir}/{name}-cluster", - f"cp xrpld.{name} {basedir}/{name}-cluster/vnode1", + f"{basedir}/{cluster_slug}-cluster", + f"cp {ansible_docker_bin} {basedir}/{cluster_slug}-cluster/vnode1", ) run_command( - f"{basedir}/{name}-cluster/vnode1", + f"{basedir}/{cluster_slug}-cluster/vnode1", "docker build -f Dockerfile --platform linux/x86_64" f" --tag transia/cluster:{image_name} .", ) run_command( - f"{basedir}/{name}-cluster/vnode1", + f"{basedir}/{cluster_slug}-cluster/vnode1", f"docker push transia/cluster:{image_name}", ) run_command( - f"{basedir}/{name}-cluster/vnode1", - f"rm -r xrpld.{name}", + f"{basedir}/{cluster_slug}-cluster/vnode1", + f"rm -f {ansible_docker_bin}", ) if k[:5] == "pnode": index: int = int(k[5:]) c_name: str = v["container_name"] ports: List[str] = v["ports"] vars = DockerVars( - f"{basedir}/{name}-cluster/{c_name}/config/", + f"{basedir}/{cluster_slug}-cluster/{c_name}/config/", ssh_port, [ - f'RPC_PUBLIC: {ports[0].split(":")[0]}', - f'RPC_ADMIN: {ports[1].split(":")[0]}', - f'WS_PUBLIC: {ports[2].split(":")[0]}', - f'WS_ADMIN: {ports[3].split(":")[0]}', - f'PEER: {ports[4].split(":")[0]}', + f"RPC_PUBLIC: {ports[0].split(':')[0]}", + f"RPC_ADMIN: {ports[1].split(':')[0]}", + f"WS_PUBLIC: {ports[2].split(':')[0]}", + f"WS_ADMIN: {ports[3].split(':')[0]}", + f"PEER: {ports[4].split(':')[0]}", ], int(ports[2].split(":")[-1]), int(ports[4].split(":")[-1]), @@ -887,7 +963,9 @@ def create_ansible( ], ) create_ansible_vars_file( - f"{basedir}/{name}-cluster/ansible/host_vars", pips[index - 1], vars + f"{basedir}/{cluster_slug}-cluster/ansible/host_vars", + pips[index - 1], + vars, ) hosts_content: str = """ # this is a basic file putting different hosts into categories @@ -907,7 +985,7 @@ def create_ansible( hosts_content += "[peer]\n" hosts_content += f"{pips[0]} ansible_port={ssh} ansible_user={user} ansible_ssh_private_key_file={ssh_key} vars_file=host_vars/{pips[0]}.yml \n" # noqa: E501 - write_file(f"{basedir}/{name}-cluster/ansible/hosts.txt", hosts_content) + write_file(f"{basedir}/{cluster_slug}-cluster/ansible/hosts.txt", hosts_content) def stop_network(name: str, remove: bool = False): @@ -1119,6 +1197,7 @@ def create_local_network( genesis: bool = False, quorum: int = None, nodedb_type: str = "NuDB", + network_name: Optional[str] = None, ) -> None: """ Creates a local multi-node network configuration that runs natively without Docker. @@ -1127,8 +1206,8 @@ def create_local_network( The user should run this command from their build directory (e.g., xrpld-quantum/build) where the xrpld binary is located. """ - # Use a simple name for local networks - name: str = f"local-{protocol}" + default_local = f"local-{protocol}" + name: str = sanitize_cluster_name(network_name) if network_name else default_local # Create cluster in current working directory instead of package directory cluster_dir = f"{os.getcwd()}/{name}-cluster" os.makedirs(cluster_dir, exist_ok=True) @@ -1144,7 +1223,9 @@ def create_local_network( elif os.path.exists(macro_path): content = get_feature_lines_from_path(macro_path) else: - print(f"{bcolors.RED}Error: Cannot find features file at {local_path} or {macro_path}") + print( + f"{bcolors.RED}Error: Cannot find features file at {local_path} or {macro_path}" + ) print(f"Please run this command from your build directory.{bcolors.END}") return @@ -1265,6 +1346,11 @@ def create_local_network( "services": services, "networks": {f"{name}-network": {"driver": "bridge"}}, } + if network_name: + compose = { + "name": docker_compose_top_level_name(name), + **compose, + } with open(f"{cluster_dir}/docker-compose.yml", "w") as f: yaml.dump(compose, f, default_flow_style=False) diff --git a/xrpld_netgen/utils/deploy_kit.py b/xrpld_netgen/utils/deploy_kit.py index 6e75977..7a39e0d 100644 --- a/xrpld_netgen/utils/deploy_kit.py +++ b/xrpld_netgen/utils/deploy_kit.py @@ -281,29 +281,38 @@ def build_local_start_sh( ) +def network_docker_binary_basename(protocol: str, binary_tag: str) -> str: + """Basename matching create_dockerfile COPY line ({protocol}d.{tag}).""" + return f"{protocol}d.{binary_tag}" + + def build_network_start_sh( - name: str, + protocol: str, + binary_tag: str, num_validators: int, num_peers: int, ): + bin_base = network_docker_binary_basename(protocol, binary_tag) start_sh_content = "#! /bin/bash \n" + start_sh_content += 'cd "$(dirname "$0")"\n' for i in range(1, num_validators + 1): - start_sh_content += f"cp xrpld.{name} vnode{i}/xrpld.{name}\n" + start_sh_content += f"cp {bin_base} vnode{i}/{bin_base}\n" for i in range(1, num_peers + 1): - start_sh_content += f"cp xrpld.{name} pnode{i}/xrpld.{name}\n" + start_sh_content += f"cp {bin_base} pnode{i}/{bin_base}\n" start_sh_content += ( - "docker compose -f docker-compose.yml" - " up --build --force-recreate -d" + "docker compose -f docker-compose.yml up --build --force-recreate -d" ) return start_sh_content def build_network_stop_sh( - name: str, + protocol: str, + binary_tag: str, num_validators: int, num_peers: int, ) -> str: + bin_base = network_docker_binary_basename(protocol, binary_tag) stop_sh_content = "#! /bin/bash\n" stop_sh_content += "REMOVE_FLAG=false \n" stop_sh_content += """ @@ -317,17 +326,19 @@ def build_network_stop_sh( stop_sh_content += "\n" stop_sh_content += 'if [ "$REMOVE_FLAG" = true ]; then \n' if num_validators > 0 and num_peers > 0: - stop_sh_content += "docker compose -f docker-compose.yml down --remove-orphans\n" # noqa: E501 + stop_sh_content += ( + "docker compose -f docker-compose.yml down --remove-orphans\n" # noqa: E501 + ) for i in range(1, num_validators + 1): stop_sh_content += f"rm -r vnode{i}/lib\n" stop_sh_content += f"rm -r vnode{i}/log\n" - stop_sh_content += f"rm -r vnode{i}/xrpld.{name}\n" + stop_sh_content += f"rm -f vnode{i}/{bin_base}\n" for i in range(1, num_peers + 1): stop_sh_content += f"rm -r pnode{i}/lib\n" stop_sh_content += f"rm -r pnode{i}/log\n" - stop_sh_content += f"rm -r pnode{i}/xrpld.{name}\n" + stop_sh_content += f"rm -f pnode{i}/{bin_base}\n" stop_sh_content += "else \n" if num_validators > 0 and num_peers > 0: @@ -350,54 +361,51 @@ def build_local_network_start_sh( start_sh_content += "# Start Explorer and VL services in Docker\n" start_sh_content += "echo 'Starting Docker services (Explorer & VL)...'\n" start_sh_content += ( - "docker compose -f docker-compose.yml" - " up --build --force-recreate -d\n\n" + "docker compose -f docker-compose.yml up --build --force-recreate -d\n\n" ) start_sh_content += "# Wait for services to be ready\n" start_sh_content += "sleep 2\n\n" # Get the absolute path to the cluster directory first start_sh_content += "# Get the absolute path to the cluster directory\n" - start_sh_content += "CLUSTER_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\n" + start_sh_content += 'CLUSTER_DIR="$(cd "$(dirname "$0")" && pwd)"\n\n' # Find and copy binary to each node folder start_sh_content += "# Locate xrpld binary\n" - start_sh_content += f"if [ -f \"$CLUSTER_DIR/../{binary_name}\" ]; then\n" - start_sh_content += f" BINARY_PATH=\"$CLUSTER_DIR/../{binary_name}\"\n" + start_sh_content += f'if [ -f "$CLUSTER_DIR/../{binary_name}" ]; then\n' + start_sh_content += f' BINARY_PATH="$CLUSTER_DIR/../{binary_name}"\n' start_sh_content += f"elif command -v {binary_name} &> /dev/null; then\n" start_sh_content += f" BINARY_PATH=$(command -v {binary_name})\n" start_sh_content += "else\n" start_sh_content += f" echo 'Error: {binary_name} binary not found!'\n" start_sh_content += f" echo 'Please ensure {binary_name} is either:'\n" start_sh_content += ( - " echo ' 1. In the parent directory:" - f" $CLUSTER_DIR/../{binary_name}'\n" + f" echo ' 1. In the parent directory: $CLUSTER_DIR/../{binary_name}'\n" ) start_sh_content += ( - " echo ' 2. In your PATH" - f" (e.g., /usr/local/bin/{binary_name})'\n" + f" echo ' 2. In your PATH (e.g., /usr/local/bin/{binary_name})'\n" ) start_sh_content += " exit 1\n" start_sh_content += "fi\n\n" - start_sh_content += "echo \"Using binary: $BINARY_PATH\"\n\n" + start_sh_content += 'echo "Using binary: $BINARY_PATH"\n\n' start_sh_content += "# Copy xrpld binary to each node (if not already present)\n" for i in range(1, num_validators + 1): start_sh_content += ( - f"if [ ! -f \"vnode{i}/{binary_name}\" ]" - " || [ \"$BINARY_PATH\" -nt" - f" \"vnode{i}/{binary_name}\" ]; then\n" + f'if [ ! -f "vnode{i}/{binary_name}" ]' + ' || [ "$BINARY_PATH" -nt' + f' "vnode{i}/{binary_name}" ]; then\n' ) - start_sh_content += f" cp \"$BINARY_PATH\" vnode{i}/{binary_name}\n" + start_sh_content += f' cp "$BINARY_PATH" vnode{i}/{binary_name}\n' start_sh_content += f" echo 'Copied binary to vnode{i}'\n" start_sh_content += "fi\n" for i in range(1, num_peers + 1): start_sh_content += ( - f"if [ ! -f \"pnode{i}/{binary_name}\" ]" - " || [ \"$BINARY_PATH\" -nt" - f" \"pnode{i}/{binary_name}\" ]; then\n" + f'if [ ! -f "pnode{i}/{binary_name}" ]' + ' || [ "$BINARY_PATH" -nt' + f' "pnode{i}/{binary_name}" ]; then\n' ) - start_sh_content += f" cp \"$BINARY_PATH\" pnode{i}/{binary_name}\n" + start_sh_content += f' cp "$BINARY_PATH" pnode{i}/{binary_name}\n' start_sh_content += f" echo 'Copied binary to pnode{i}'\n" start_sh_content += "fi\n" start_sh_content += "\n" @@ -405,26 +413,26 @@ def build_local_network_start_sh( start_sh_content += "# Start validator nodes in background\n" for i in range(1, num_validators + 1): start_sh_content += f"echo 'Starting vnode{i} in background...'\n" - start_sh_content += f"cd \"$CLUSTER_DIR/vnode{i}\"\n" + start_sh_content += f'cd "$CLUSTER_DIR/vnode{i}"\n' start_sh_content += ( f"nohup ./{binary_name} --conf config/xrpld.cfg" " --ledgerfile config/genesis.json" " > /dev/null 2>&1 &\n" ) - start_sh_content += f"echo $! > \"$CLUSTER_DIR/vnode{i}/xrpld.pid\"\n" - start_sh_content += "cd \"$CLUSTER_DIR\"\n" + start_sh_content += f'echo $! > "$CLUSTER_DIR/vnode{i}/xrpld.pid"\n' + start_sh_content += 'cd "$CLUSTER_DIR"\n' start_sh_content += "\n# Start peer nodes in background\n" for i in range(1, num_peers + 1): start_sh_content += f"echo 'Starting pnode{i} in background...'\n" - start_sh_content += f"cd \"$CLUSTER_DIR/pnode{i}\"\n" + start_sh_content += f'cd "$CLUSTER_DIR/pnode{i}"\n' start_sh_content += ( f"nohup ./{binary_name} --conf config/xrpld.cfg" " --ledgerfile config/genesis.json" " > /dev/null 2>&1 &\n" ) - start_sh_content += f"echo $! > \"$CLUSTER_DIR/pnode{i}/xrpld.pid\"\n" - start_sh_content += "cd \"$CLUSTER_DIR\"\n" + start_sh_content += f'echo $! > "$CLUSTER_DIR/pnode{i}/xrpld.pid"\n' + start_sh_content += 'cd "$CLUSTER_DIR"\n' start_sh_content += "\n# Wait for nodes to start\n" start_sh_content += "sleep 3\n\n" @@ -441,7 +449,7 @@ def build_local_network_start_sh( start_sh_content += "echo 'Each node is running in the background.'\n" start_sh_content += ( "echo 'Use \"xrpld-netgen logs:local" - " --node \" to view logs" + ' --node " to view logs' " (e.g., --node vnode1).'\n" ) start_sh_content += "echo 'Use \"./stop.sh\" to stop all nodes.'\n" @@ -475,13 +483,13 @@ def build_local_network_stop_sh( stop_sh_content += "echo 'Stopping all xrpld nodes...'\n\n" stop_sh_content += "# Get the absolute path to the cluster directory\n" - stop_sh_content += "CLUSTER_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\n" + stop_sh_content += 'CLUSTER_DIR="$(cd "$(dirname "$0")" && pwd)"\n\n' stop_sh_content += "# Stop validator nodes\n" for i in range(1, num_validators + 1): stop_sh_content += f"echo 'Stopping vnode{i}...'\n" - stop_sh_content += f"if [ -f \"$CLUSTER_DIR/vnode{i}/xrpld.pid\" ]; then\n" - stop_sh_content += f" PID=$(cat \"$CLUSTER_DIR/vnode{i}/xrpld.pid\")\n" + stop_sh_content += f'if [ -f "$CLUSTER_DIR/vnode{i}/xrpld.pid" ]; then\n' + stop_sh_content += f' PID=$(cat "$CLUSTER_DIR/vnode{i}/xrpld.pid")\n' stop_sh_content += " if ps -p $PID > /dev/null 2>&1; then\n" stop_sh_content += " kill $PID 2>/dev/null || true\n" stop_sh_content += " sleep 1\n" @@ -490,19 +498,19 @@ def build_local_network_stop_sh( stop_sh_content += " kill -9 $PID 2>/dev/null || true\n" stop_sh_content += " fi\n" stop_sh_content += " fi\n" - stop_sh_content += f" rm -f \"$CLUSTER_DIR/vnode{i}/xrpld.pid\"\n" + stop_sh_content += f' rm -f "$CLUSTER_DIR/vnode{i}/xrpld.pid"\n' stop_sh_content += "fi\n" stop_sh_content += ( "# Fallback: Find and kill any xrpld" f" process running in vnode{i} directory\n" ) - stop_sh_content += f"pkill -9 -f \"vnode{i}/xrpld\" 2>/dev/null || true\n" + stop_sh_content += f'pkill -9 -f "vnode{i}/xrpld" 2>/dev/null || true\n' stop_sh_content += "\n# Stop peer nodes\n" for i in range(1, num_peers + 1): stop_sh_content += f"echo 'Stopping pnode{i}...'\n" - stop_sh_content += f"if [ -f \"$CLUSTER_DIR/pnode{i}/xrpld.pid\" ]; then\n" - stop_sh_content += f" PID=$(cat \"$CLUSTER_DIR/pnode{i}/xrpld.pid\")\n" + stop_sh_content += f'if [ -f "$CLUSTER_DIR/pnode{i}/xrpld.pid" ]; then\n' + stop_sh_content += f' PID=$(cat "$CLUSTER_DIR/pnode{i}/xrpld.pid")\n' stop_sh_content += " if ps -p $PID > /dev/null 2>&1; then\n" stop_sh_content += " kill $PID 2>/dev/null || true\n" stop_sh_content += " sleep 1\n" @@ -511,13 +519,13 @@ def build_local_network_stop_sh( stop_sh_content += " kill -9 $PID 2>/dev/null || true\n" stop_sh_content += " fi\n" stop_sh_content += " fi\n" - stop_sh_content += f" rm -f \"$CLUSTER_DIR/pnode{i}/xrpld.pid\"\n" + stop_sh_content += f' rm -f "$CLUSTER_DIR/pnode{i}/xrpld.pid"\n' stop_sh_content += "fi\n" stop_sh_content += ( "# Fallback: Find and kill any xrpld" f" process running in pnode{i} directory\n" ) - stop_sh_content += f"pkill -9 -f \"pnode{i}/xrpld\" 2>/dev/null || true\n" + stop_sh_content += f'pkill -9 -f "pnode{i}/xrpld" 2>/dev/null || true\n' stop_sh_content += "\n# Wait for processes to terminate\n" stop_sh_content += "sleep 2\n\n" @@ -530,13 +538,11 @@ def build_local_network_stop_sh( # Clean up directories if --remove flag is used for i in range(1, num_validators + 1): stop_sh_content += ( - f" rm -rf vnode{i}/lib vnode{i}/log" - f" vnode{i}/xrpld vnode{i}/db\n" + f" rm -rf vnode{i}/lib vnode{i}/log vnode{i}/xrpld vnode{i}/db\n" ) for i in range(1, num_peers + 1): stop_sh_content += ( - f" rm -rf pnode{i}/lib pnode{i}/log" - f" pnode{i}/xrpld pnode{i}/db\n" + f" rm -rf pnode{i}/lib pnode{i}/log pnode{i}/xrpld pnode{i}/db\n" ) stop_sh_content += "else\n" diff --git a/xrpld_netgen/utils/misc.py b/xrpld_netgen/utils/misc.py index 512c714..179b4c0 100644 --- a/xrpld_netgen/utils/misc.py +++ b/xrpld_netgen/utils/misc.py @@ -22,6 +22,32 @@ class bcolors: END = "\033[0m" +def sanitize_cluster_name(raw: str) -> str: + """Normalize a user-supplied name for workspace dirs and Docker networks.""" + s = raw.strip() + if not s: + raise ValueError("Cluster name must not be empty.") + if ".." in s or "/" in s or "\\" in s: + raise ValueError("Cluster name must not contain path separators or '..'.") + out: List[str] = [] + for c in s: + if c.isalnum() or c in "._-": + out.append(c) + elif c.isspace(): + out.append("-") + else: + out.append("-") + collapsed = "".join(out).strip("-_.") + if not collapsed: + raise ValueError("Cluster name has no valid characters.") + return collapsed + + +def docker_compose_top_level_name(slug: str) -> str: + """Normalize a directory slug for Compose ``name`` (project name).""" + return slug.lower().replace(".", "-") + + def remove_directory(directory_path: str) -> None: try: name: str = directory_path.split("/")[-1] @@ -68,10 +94,7 @@ def run_start(cmd: List[str], protocol: str, version: str, type: str): ) sys.exit(1) except FileNotFoundError: - print( - f"{bcolors.RED}❌ The file {cmd[0]} does not exist or cannot be " - f"found." - ) + print(f"{bcolors.RED}❌ The file {cmd[0]} does not exist or cannot be found.") sys.exit(1) except OSError as e: print(f"{bcolors.RED}❌ An OS error occurred: {e}") @@ -98,10 +121,7 @@ def run_stop(cmd: List[str]): ) sys.exit(1) except FileNotFoundError: - print( - f"{bcolors.RED}❌ The file {cmd[0]} does not exist or cannot be " - f"found." - ) + print(f"{bcolors.RED}❌ The file {cmd[0]} does not exist or cannot be found.") sys.exit(1) except OSError as e: print(f"{bcolors.RED}❌ An OS error occurred: {e}") @@ -214,8 +234,7 @@ def run_local_logs(node: str = None): log_path = "config/debug.log" if not os.path.exists(log_path): print( - f"{bcolors.RED}Error: Log file not found at " - f"{log_path}{bcolors.END}" + f"{bcolors.RED}Error: Log file not found at {log_path}{bcolors.END}" ) print() print( @@ -331,7 +350,9 @@ def download_json(url: str, destination_dir: str) -> Dict[str, Any]: raise ValueError(f"Failed to download file from {url}") -def save_local_config(protocol: str, cfg_path: str, cfg_out: str, validators_out: str) -> None: +def save_local_config( + protocol: str, cfg_path: str, cfg_out: str, validators_out: str +) -> None: with open(f"{cfg_path}/{protocol}d.cfg", "w") as text_file: text_file.write(cfg_out)