|
9 | 9 | from pathlib import Path |
10 | 10 | from typing import List |
11 | 11 |
|
12 | | -import fire |
| 12 | +import click |
13 | 13 | import requests |
14 | 14 | import tomlkit |
15 | 15 |
|
|
38 | 38 | ECHO_SERVER_PORT = 26659 |
39 | 39 |
|
40 | 40 |
|
41 | | -class CLI: |
42 | | - def gen( |
43 | | - self, |
44 | | - outdir: str, |
45 | | - hostname_template=HOSTNAME_TEMPLATE, |
46 | | - options={}, |
47 | | - ): |
48 | | - print("options", options) |
49 | | - validators = options.get("validators", 3) |
50 | | - fullnodes = options.get("fullnodes", 7) |
51 | | - num_accounts = options.get("num_accounts", 10) |
52 | | - num_txs = options.get("num_txs", 1000) |
53 | | - config_patch = options.get("config", {}) |
54 | | - app_patch = options.get("app", {}) |
55 | | - genesis_patch = options.get("genesis", {}) |
56 | | - outdir = Path(outdir) |
57 | | - cli = ChainCommand(LOCAL_CRONOSD_PATH) |
58 | | - (outdir / VALIDATOR_GROUP).mkdir(parents=True, exist_ok=True) |
59 | | - (outdir / FULLNODE_GROUP).mkdir(parents=True, exist_ok=True) |
60 | | - |
61 | | - peers = [] |
62 | | - for i in range(validators): |
63 | | - print("init validator", i) |
64 | | - ip = hostname_template.format(index=i) |
65 | | - peers.append(init_node_local(cli, outdir, VALIDATOR_GROUP, i, ip)) |
66 | | - for i in range(fullnodes): |
67 | | - print("init fullnode", i) |
68 | | - ip = hostname_template.format(index=i + validators) |
69 | | - peers.append(init_node_local(cli, outdir, FULLNODE_GROUP, i, ip)) |
70 | | - |
71 | | - print("prepare genesis") |
72 | | - # use a full node directory to prepare the genesis file |
73 | | - genesis = gen_genesis(cli, outdir / FULLNODE_GROUP / "0", peers, genesis_patch) |
74 | | - |
75 | | - print("patch genesis") |
76 | | - # write genesis file and patch config files |
77 | | - for i in range(validators): |
78 | | - patch_configs_local( |
79 | | - peers, genesis, outdir, VALIDATOR_GROUP, i, config_patch, app_patch |
80 | | - ) |
81 | | - for i in range(fullnodes): |
82 | | - patch_configs_local( |
83 | | - peers, |
84 | | - genesis, |
85 | | - outdir, |
86 | | - FULLNODE_GROUP, |
87 | | - i, |
88 | | - config_patch, |
89 | | - app_patch, |
90 | | - ) |
91 | | - |
92 | | - print("write config") |
93 | | - cfg = { |
94 | | - "validators": validators, |
95 | | - "fullnodes": fullnodes, |
96 | | - "num_accounts": num_accounts, |
97 | | - "num_txs": num_txs, |
98 | | - "validator-generate-load": options.get("validator-generate-load", True), |
99 | | - } |
100 | | - (outdir / "config.json").write_text(json.dumps(cfg)) |
101 | | - |
102 | | - def patchimage( |
103 | | - self, |
104 | | - toimage, |
105 | | - src, |
106 | | - dst="/data", |
107 | | - fromimage="ghcr.io/crypto-org-chain/cronos-testground:latest", |
108 | | - ): |
109 | | - """ |
110 | | - combine data directory with an exiting image to produce a new image |
111 | | - """ |
112 | | - with tempfile.TemporaryDirectory() as tmpdir: |
113 | | - tmpdir = Path(tmpdir) |
114 | | - shutil.copytree(src, tmpdir / "out") |
115 | | - content = f"""FROM {fromimage} |
| 41 | +@click.group() |
| 42 | +def cli(): |
| 43 | + pass |
| 44 | + |
| 45 | + |
| 46 | +def validate_json(ctx, param, value): |
| 47 | + try: |
| 48 | + return json.loads(value) |
| 49 | + except json.JSONDecodeError: |
| 50 | + raise click.BadParameter("must be a valid JSON string") |
| 51 | + |
| 52 | + |
| 53 | +@cli.command() |
| 54 | +@click.argument("outdir") |
| 55 | +@click.option("--hostname-template", default=HOSTNAME_TEMPLATE) |
| 56 | +@click.option("--validators", default=3) |
| 57 | +@click.option("--fullnodes", default=7) |
| 58 | +@click.option("--num-accounts", default=10) |
| 59 | +@click.option("--num-txs", default=1000) |
| 60 | +@click.option("--config-patch", default="{}", callback=validate_json) |
| 61 | +@click.option("--app-patch", default="{}", callback=validate_json) |
| 62 | +@click.option("--genesis-patch", default="{}", callback=validate_json) |
| 63 | +@click.option("--validator-generate-load/--no-validator-generate-load", default=True) |
| 64 | +def gen(**kwargs): |
| 65 | + return _gen(**kwargs) |
| 66 | + |
| 67 | + |
| 68 | +@cli.command() |
| 69 | +@click.argument("options", callback=validate_json) |
| 70 | +def generic_gen(options: dict): |
| 71 | + return _gen(**options) |
| 72 | + |
| 73 | + |
| 74 | +def _gen( |
| 75 | + outdir: str, |
| 76 | + hostname_template: str = HOSTNAME_TEMPLATE, |
| 77 | + validators: int = 3, |
| 78 | + fullnodes: int = 7, |
| 79 | + num_accounts: int = 10, |
| 80 | + num_txs: int = 1000, |
| 81 | + validator_generate_load: bool = True, |
| 82 | + config_patch: dict = None, |
| 83 | + app_patch: dict = None, |
| 84 | + genesis_patch: dict = None, |
| 85 | +): |
| 86 | + config_patch = config_patch or {} |
| 87 | + app_patch = app_patch or {} |
| 88 | + genesis_patch = genesis_patch or {} |
| 89 | + |
| 90 | + outdir = Path(outdir) |
| 91 | + cli = ChainCommand(LOCAL_CRONOSD_PATH) |
| 92 | + (outdir / VALIDATOR_GROUP).mkdir(parents=True, exist_ok=True) |
| 93 | + (outdir / FULLNODE_GROUP).mkdir(parents=True, exist_ok=True) |
| 94 | + |
| 95 | + config_patch = ( |
| 96 | + json.loads(config_patch) if isinstance(config_patch, str) else config_patch |
| 97 | + ) |
| 98 | + app_patch = json.loads(app_patch) if isinstance(app_patch, str) else app_patch |
| 99 | + genesis_patch = ( |
| 100 | + json.loads(genesis_patch) if isinstance(genesis_patch, str) else genesis_patch |
| 101 | + ) |
| 102 | + |
| 103 | + peers = [] |
| 104 | + for i in range(validators): |
| 105 | + print("init validator", i) |
| 106 | + ip = hostname_template.format(index=i) |
| 107 | + peers.append(init_node_local(cli, outdir, VALIDATOR_GROUP, i, ip)) |
| 108 | + for i in range(fullnodes): |
| 109 | + print("init fullnode", i) |
| 110 | + ip = hostname_template.format(index=i + validators) |
| 111 | + peers.append(init_node_local(cli, outdir, FULLNODE_GROUP, i, ip)) |
| 112 | + |
| 113 | + print("prepare genesis") |
| 114 | + # use a full node directory to prepare the genesis file |
| 115 | + genesis = gen_genesis(cli, outdir / FULLNODE_GROUP / "0", peers, genesis_patch) |
| 116 | + |
| 117 | + print("patch genesis") |
| 118 | + # write genesis file and patch config files |
| 119 | + for i in range(validators): |
| 120 | + patch_configs_local( |
| 121 | + peers, genesis, outdir, VALIDATOR_GROUP, i, config_patch, app_patch |
| 122 | + ) |
| 123 | + for i in range(fullnodes): |
| 124 | + patch_configs_local( |
| 125 | + peers, |
| 126 | + genesis, |
| 127 | + outdir, |
| 128 | + FULLNODE_GROUP, |
| 129 | + i, |
| 130 | + config_patch, |
| 131 | + app_patch, |
| 132 | + ) |
| 133 | + |
| 134 | + print("write config") |
| 135 | + cfg = { |
| 136 | + "validators": validators, |
| 137 | + "fullnodes": fullnodes, |
| 138 | + "num_accounts": num_accounts, |
| 139 | + "num_txs": num_txs, |
| 140 | + "validator-generate-load": validator_generate_load, |
| 141 | + } |
| 142 | + (outdir / "config.json").write_text(json.dumps(cfg)) |
| 143 | + |
| 144 | + |
| 145 | +@cli.command() |
| 146 | +@click.argument("toimage") |
| 147 | +@click.argument("src") |
| 148 | +@click.option("--dst", default="/data") |
| 149 | +@click.option( |
| 150 | + "--fromimage", default="ghcr.io/crypto-org-chain/cronos-testground:latest" |
| 151 | +) |
| 152 | +def patchimage(toimage, src, dst, fromimage): |
| 153 | + """ |
| 154 | + combine data directory with an exiting image to produce a new image |
| 155 | + """ |
| 156 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 157 | + tmpdir = Path(tmpdir) |
| 158 | + shutil.copytree(src, tmpdir / "out") |
| 159 | + content = f"""FROM {fromimage} |
116 | 160 | ADD ./out {dst} |
117 | 161 | """ |
118 | | - print(content) |
119 | | - (tmpdir / "Dockerfile").write_text(content) |
120 | | - subprocess.run(["docker", "build", "-t", toimage, tmpdir]) |
121 | | - |
122 | | - def run( |
123 | | - self, |
124 | | - outdir: str = "/outputs", |
125 | | - datadir: str = "/data", |
126 | | - cronosd=CONTAINER_CRONOSD_PATH, |
127 | | - global_seq=None, |
128 | | - ): |
129 | | - run_echo_server(ECHO_SERVER_PORT) |
130 | | - |
131 | | - datadir = Path(datadir) |
132 | | - cfg = json.loads((datadir / "config.json").read_text()) |
133 | | - |
134 | | - if global_seq is None: |
135 | | - global_seq = node_index() |
136 | | - |
137 | | - validators = cfg["validators"] |
138 | | - group = VALIDATOR_GROUP if global_seq < validators else FULLNODE_GROUP |
139 | | - group_seq = global_seq if group == VALIDATOR_GROUP else global_seq - validators |
140 | | - print("node role", global_seq, group, group_seq) |
141 | | - |
142 | | - home = datadir / group / str(group_seq) |
143 | | - |
144 | | - # wait for persistent peers to be ready |
145 | | - wait_for_peers(home) |
146 | | - |
147 | | - print("start node") |
148 | | - logfile = open(home / "node.log", "ab", buffering=0) |
149 | | - proc = subprocess.Popen( |
150 | | - [cronosd, "start", "--home", str(home)], |
151 | | - stdout=logfile, |
152 | | - ) |
| 162 | + print(content) |
| 163 | + (tmpdir / "Dockerfile").write_text(content) |
| 164 | + subprocess.run(["docker", "build", "-t", toimage, tmpdir]) |
153 | 165 |
|
154 | | - cli = ChainCommand(cronosd) |
155 | | - wait_for_port(26657) |
156 | | - wait_for_port(8545) |
157 | | - wait_for_block(cli, 3) |
158 | 166 |
|
159 | | - if group == FULLNODE_GROUP or cfg.get("validator-generate-load", True): |
160 | | - wait_for_w3() |
161 | | - generate_load( |
162 | | - cli, cfg["num_accounts"], cfg["num_txs"], home=home, output="json" |
163 | | - ) |
| 167 | +@cli.command() |
| 168 | +@click.option("--outdir", default="/outputs") |
| 169 | +@click.option("--datadir", default="/data") |
| 170 | +@click.option("--cronosd", default=CONTAINER_CRONOSD_PATH) |
| 171 | +@click.option("--global-seq", default=None) |
| 172 | +def run(outdir: str, datadir: str, cronosd, global_seq): |
| 173 | + run_echo_server(ECHO_SERVER_PORT) |
164 | 174 |
|
165 | | - # node quit when the chain is idle or halted for a while |
166 | | - detect_idle_halted(20, 20) |
| 175 | + datadir = Path(datadir) |
| 176 | + cfg = json.loads((datadir / "config.json").read_text()) |
167 | 177 |
|
168 | | - with (home / "block_stats.log").open("w") as logfile: |
169 | | - dump_block_stats(logfile) |
| 178 | + if global_seq is None: |
| 179 | + global_seq = node_index() |
170 | 180 |
|
171 | | - proc.kill() |
172 | | - proc.wait(20) |
| 181 | + validators = cfg["validators"] |
| 182 | + group = VALIDATOR_GROUP if global_seq < validators else FULLNODE_GROUP |
| 183 | + group_seq = global_seq if group == VALIDATOR_GROUP else global_seq - validators |
| 184 | + print("node role", global_seq, group, group_seq) |
173 | 185 |
|
174 | | - # collect outputs |
175 | | - output = Path("/data.tar.bz2") |
176 | | - with tarfile.open(output, "x:bz2") as tar: |
177 | | - tar.add(home, arcname="data", filter=output_filter(group, group_seq)) |
178 | | - outdir = Path(outdir) |
179 | | - if outdir.exists(): |
180 | | - assert outdir.is_dir() |
181 | | - filename = outdir / f"{group}_{group_seq}.tar.bz2" |
182 | | - filename.unlink(missing_ok=True) |
183 | | - shutil.copy(output, filename) |
| 186 | + home = datadir / group / str(group_seq) |
| 187 | + |
| 188 | + # wait for persistent peers to be ready |
| 189 | + wait_for_peers(home) |
| 190 | + |
| 191 | + print("start node") |
| 192 | + logfile = open(home / "node.log", "ab", buffering=0) |
| 193 | + proc = subprocess.Popen( |
| 194 | + [cronosd, "start", "--home", str(home)], |
| 195 | + stdout=logfile, |
| 196 | + ) |
| 197 | + |
| 198 | + cli = ChainCommand(cronosd) |
| 199 | + wait_for_port(26657) |
| 200 | + wait_for_port(8545) |
| 201 | + wait_for_block(cli, 3) |
| 202 | + |
| 203 | + if group == FULLNODE_GROUP or cfg.get("validator-generate-load", True): |
| 204 | + wait_for_w3() |
| 205 | + generate_load( |
| 206 | + cli, cfg["num_accounts"], cfg["num_txs"], home=home, output="json" |
| 207 | + ) |
| 208 | + |
| 209 | + # node quit when the chain is idle or halted for a while |
| 210 | + detect_idle_halted(20, 20) |
| 211 | + |
| 212 | + with (home / "block_stats.log").open("w") as logfile: |
| 213 | + dump_block_stats(logfile) |
| 214 | + |
| 215 | + proc.kill() |
| 216 | + proc.wait(20) |
| 217 | + |
| 218 | + # collect outputs |
| 219 | + output = Path("/data.tar.bz2") |
| 220 | + with tarfile.open(output, "x:bz2") as tar: |
| 221 | + tar.add(home, arcname="data", filter=output_filter(group, group_seq)) |
| 222 | + outdir = Path(outdir) |
| 223 | + if outdir.exists(): |
| 224 | + assert outdir.is_dir() |
| 225 | + filename = outdir / f"{group}_{group_seq}.tar.bz2" |
| 226 | + filename.unlink(missing_ok=True) |
| 227 | + shutil.copy(output, filename) |
184 | 228 |
|
185 | 229 |
|
186 | 230 | def output_filter(group, group_seq: int): |
@@ -321,9 +365,5 @@ def dump_block_stats(fp): |
321 | 365 | print("block", i, txs, timestamp, file=fp) |
322 | 366 |
|
323 | 367 |
|
324 | | -def main(): |
325 | | - fire.Fire(CLI) |
326 | | - |
327 | | - |
328 | 368 | if __name__ == "__main__": |
329 | | - main() |
| 369 | + cli() |
0 commit comments