|
| 1 | +import json |
| 2 | +import os |
| 3 | +import tempfile |
| 4 | +import time |
| 5 | + |
| 6 | +import click |
| 7 | +import yaml |
| 8 | +from rich import print |
| 9 | +from rich.console import Console |
| 10 | +from rich.prompt import Confirm, Prompt |
| 11 | +from rich.table import Table |
| 12 | + |
| 13 | +from .k8s import ( |
| 14 | + apply_kubernetes_yaml, |
| 15 | + delete_namespace, |
| 16 | + get_default_namespace, |
| 17 | + get_mission, |
| 18 | + get_pods, |
| 19 | +) |
| 20 | +from .process import run_command, stream_command |
| 21 | + |
| 22 | +console = Console() |
| 23 | + |
| 24 | + |
| 25 | +def get_active_scenarios(): |
| 26 | + """Get list of active scenarios""" |
| 27 | + commanders = get_mission("commander") |
| 28 | + return [c.metadata.name for c in commanders] |
| 29 | + |
| 30 | + |
| 31 | +@click.command() |
| 32 | +@click.argument("scenario_name", required=False) |
| 33 | +def stop(scenario_name): |
| 34 | + """Stop a running scenario or all scenarios""" |
| 35 | + active_scenarios = get_active_scenarios() |
| 36 | + |
| 37 | + if not active_scenarios: |
| 38 | + console.print("[bold red]No active scenarios found.[/bold red]") |
| 39 | + return |
| 40 | + |
| 41 | + if not scenario_name: |
| 42 | + table = Table(title="Active Scenarios", show_header=True, header_style="bold magenta") |
| 43 | + table.add_column("Number", style="cyan", justify="right") |
| 44 | + table.add_column("Scenario Name", style="green") |
| 45 | + |
| 46 | + for idx, name in enumerate(active_scenarios, 1): |
| 47 | + table.add_row(str(idx), name) |
| 48 | + |
| 49 | + console.print(table) |
| 50 | + |
| 51 | + choices = [str(i) for i in range(1, len(active_scenarios) + 1)] + ["a", "q"] |
| 52 | + choice = Prompt.ask( |
| 53 | + "[bold yellow]Enter the number of the scenario to stop, 'a' to stop all, or 'q' to quit[/bold yellow]", |
| 54 | + choices=choices, |
| 55 | + show_choices=False, |
| 56 | + ) |
| 57 | + |
| 58 | + if choice == "q": |
| 59 | + console.print("[bold blue]Operation cancelled.[/bold blue]") |
| 60 | + return |
| 61 | + elif choice == "a": |
| 62 | + if Confirm.ask("[bold red]Are you sure you want to stop all scenarios?[/bold red]"): |
| 63 | + stop_all_scenarios(active_scenarios) |
| 64 | + else: |
| 65 | + console.print("[bold blue]Operation cancelled.[/bold blue]") |
| 66 | + return |
| 67 | + |
| 68 | + scenario_name = active_scenarios[int(choice) - 1] |
| 69 | + |
| 70 | + if scenario_name not in active_scenarios: |
| 71 | + console.print(f"[bold red]No active scenario found with name: {scenario_name}[/bold red]") |
| 72 | + return |
| 73 | + |
| 74 | + stop_scenario(scenario_name) |
| 75 | + |
| 76 | + |
| 77 | +def stop_scenario(scenario_name): |
| 78 | + """Stop a single scenario""" |
| 79 | + cmd = f"kubectl delete pod {scenario_name}" |
| 80 | + if stream_command(cmd): |
| 81 | + console.print(f"[bold green]Successfully stopped scenario: {scenario_name}[/bold green]") |
| 82 | + else: |
| 83 | + console.print(f"[bold red]Failed to stop scenario: {scenario_name}[/bold red]") |
| 84 | + |
| 85 | + |
| 86 | +def stop_all_scenarios(scenarios): |
| 87 | + """Stop all active scenarios""" |
| 88 | + with console.status("[bold yellow]Stopping all scenarios...[/bold yellow]"): |
| 89 | + for scenario in scenarios: |
| 90 | + stop_scenario(scenario) |
| 91 | + console.print("[bold green]All scenarios have been stopped.[/bold green]") |
| 92 | + |
| 93 | + |
| 94 | +def list_active_scenarios(): |
| 95 | + """List all active scenarios""" |
| 96 | + commanders = get_mission("commander") |
| 97 | + if not commanders: |
| 98 | + print("No active scenarios found.") |
| 99 | + return |
| 100 | + |
| 101 | + console = Console() |
| 102 | + table = Table(title="Active Scenarios", show_header=True, header_style="bold magenta") |
| 103 | + table.add_column("Name", style="cyan") |
| 104 | + table.add_column("Status", style="green") |
| 105 | + |
| 106 | + for commander in commanders: |
| 107 | + table.add_row(commander.metadata.name, commander.status.phase.lower()) |
| 108 | + |
| 109 | + console.print(table) |
| 110 | + |
| 111 | + |
| 112 | +@click.command() |
| 113 | +def down(): |
| 114 | + """Bring down a running warnet""" |
| 115 | + console.print("[bold yellow]Bringing down the warnet...[/bold yellow]") |
| 116 | + |
| 117 | + # Delete warnet-logging namespace |
| 118 | + if delete_namespace("warnet-logging"): |
| 119 | + console.print("[green]Warnet logging deleted[/green]") |
| 120 | + else: |
| 121 | + console.print("[red]Warnet logging NOT deleted[/red]") |
| 122 | + |
| 123 | + # Uninstall tanks |
| 124 | + tanks = get_mission("tank") |
| 125 | + with console.status("[yellow]Uninstalling tanks...[/yellow]"): |
| 126 | + for tank in tanks: |
| 127 | + cmd = f"helm uninstall {tank.metadata.name}" |
| 128 | + if stream_command(cmd): |
| 129 | + console.print(f"[green]Uninstalled tank: {tank.metadata.name}[/green]") |
| 130 | + else: |
| 131 | + console.print(f"[red]Failed to uninstall tank: {tank.metadata.name}[/red]") |
| 132 | + |
| 133 | + # Clean up scenarios and other pods |
| 134 | + pods = get_pods() |
| 135 | + with console.status("[yellow]Cleaning up remaining pods...[/yellow]"): |
| 136 | + for pod in pods.items: |
| 137 | + cmd = f"kubectl delete pod {pod.metadata.name}" |
| 138 | + if stream_command(cmd): |
| 139 | + console.print(f"[green]Deleted pod: {pod.metadata.name}[/green]") |
| 140 | + else: |
| 141 | + console.print(f"[red]Failed to delete pod: {pod.metadata.name}[/red]") |
| 142 | + |
| 143 | + console.print("[bold green]Warnet has been brought down.[/bold green]") |
| 144 | + |
| 145 | + |
| 146 | +def get_active_network(namespace): |
| 147 | + """Get the name of the active network (Helm release) in the given namespace""" |
| 148 | + cmd = f"helm list --namespace {namespace} --output json" |
| 149 | + result = run_command(cmd) |
| 150 | + if result: |
| 151 | + import json |
| 152 | + |
| 153 | + releases = json.loads(result) |
| 154 | + if releases: |
| 155 | + # Assuming the first release is the active network |
| 156 | + return releases[0]["name"] |
| 157 | + return None |
| 158 | + |
| 159 | + |
| 160 | +@click.command(context_settings={"ignore_unknown_options": True}) |
| 161 | +@click.argument("scenario_file", type=click.Path(exists=True, file_okay=True, dir_okay=False)) |
| 162 | +@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) |
| 163 | +def run(scenario_file: str, additional_args: tuple[str]): |
| 164 | + """Run a scenario from a file""" |
| 165 | + scenario_path = os.path.abspath(scenario_file) |
| 166 | + scenario_name = os.path.splitext(os.path.basename(scenario_path))[0] |
| 167 | + |
| 168 | + with open(scenario_path) as file: |
| 169 | + scenario_text = file.read() |
| 170 | + |
| 171 | + name = f"commander-{scenario_name.replace('_', '')}-{int(time.time())}" |
| 172 | + namespace = get_default_namespace() |
| 173 | + tankpods = get_mission("tank") |
| 174 | + tanks = [ |
| 175 | + { |
| 176 | + "tank": tank.metadata.name, |
| 177 | + "chain": "regtest", |
| 178 | + "rpc_host": tank.status.pod_ip, |
| 179 | + "rpc_port": 18443, |
| 180 | + "rpc_user": "user", |
| 181 | + "rpc_password": "password", |
| 182 | + "init_peers": [], |
| 183 | + } |
| 184 | + for tank in tankpods |
| 185 | + ] |
| 186 | + kubernetes_objects = [ |
| 187 | + { |
| 188 | + "apiVersion": "v1", |
| 189 | + "kind": "ConfigMap", |
| 190 | + "metadata": { |
| 191 | + "name": "warnetjson", |
| 192 | + "namespace": namespace, |
| 193 | + }, |
| 194 | + "data": {"warnet.json": json.dumps(tanks)}, |
| 195 | + }, |
| 196 | + { |
| 197 | + "apiVersion": "v1", |
| 198 | + "kind": "ConfigMap", |
| 199 | + "metadata": { |
| 200 | + "name": "scenariopy", |
| 201 | + "namespace": namespace, |
| 202 | + }, |
| 203 | + "data": {"scenario.py": scenario_text}, |
| 204 | + }, |
| 205 | + { |
| 206 | + "apiVersion": "v1", |
| 207 | + "kind": "Pod", |
| 208 | + "metadata": { |
| 209 | + "name": name, |
| 210 | + "namespace": namespace, |
| 211 | + "labels": {"mission": "commander"}, |
| 212 | + }, |
| 213 | + "spec": { |
| 214 | + "restartPolicy": "Never", |
| 215 | + "containers": [ |
| 216 | + { |
| 217 | + "name": name, |
| 218 | + "image": "bitcoindevproject/warnet-commander:latest", |
| 219 | + "args": additional_args, |
| 220 | + "volumeMounts": [ |
| 221 | + { |
| 222 | + "name": "warnetjson", |
| 223 | + "mountPath": "warnet.json", |
| 224 | + "subPath": "warnet.json", |
| 225 | + }, |
| 226 | + { |
| 227 | + "name": "scenariopy", |
| 228 | + "mountPath": "scenario.py", |
| 229 | + "subPath": "scenario.py", |
| 230 | + }, |
| 231 | + ], |
| 232 | + } |
| 233 | + ], |
| 234 | + "volumes": [ |
| 235 | + {"name": "warnetjson", "configMap": {"name": "warnetjson"}}, |
| 236 | + {"name": "scenariopy", "configMap": {"name": "scenariopy"}}, |
| 237 | + ], |
| 238 | + }, |
| 239 | + }, |
| 240 | + ] |
| 241 | + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: |
| 242 | + yaml.dump_all(kubernetes_objects, temp_file) |
| 243 | + temp_file_path = temp_file.name |
| 244 | + |
| 245 | + if apply_kubernetes_yaml(temp_file_path): |
| 246 | + print(f"Successfully started scenario: {scenario_name}") |
| 247 | + print(f"Commander pod name: {name}") |
| 248 | + else: |
| 249 | + print(f"Failed to start scenario: {scenario_name}") |
| 250 | + |
| 251 | + os.unlink(temp_file_path) |
0 commit comments