Skip to content

Commit df19f2a

Browse files
committed
update hooks and simln plugin
1 parent 141ed96 commit df19f2a

File tree

4 files changed

+267
-26
lines changed

4 files changed

+267
-26
lines changed

resources/plugins/simln/charts/simln/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ image:
44
tag: "d8c165d"
55
pullPolicy: IfNotPresent
66
command: ["sh", "-c"]
7-
args: ["while true; do sleep 3600; done"]
7+
args: ["cd /config; sim-cli"]
88
defaultDataDir: /app/data

resources/plugins/simln/simln.py

Lines changed: 223 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,229 @@
1-
from hooks_api import post_status, pre_status
1+
import json
2+
import logging
3+
import random
4+
from pathlib import Path
5+
from subprocess import run
6+
from time import sleep
27

8+
from warnet.hooks import _get_plugin_directory as get_plugin_directory
9+
from warnet.k8s import get_pods_with_label
10+
from warnet.process import run_command
11+
from warnet.status import _get_tank_status as network_status
312

4-
@pre_status
5-
def print_something_first():
6-
print("The simln plugin is enabled.")
13+
log = logging.getLogger("simln")
14+
log.setLevel(logging.DEBUG)
15+
console_handler = logging.StreamHandler()
16+
console_handler.setLevel(logging.DEBUG)
17+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
18+
console_handler.setFormatter(formatter)
19+
log.addHandler(console_handler)
720

21+
lightning_selector = "mission=lightning"
822

9-
@post_status
10-
def print_something_afterwards():
11-
print("The simln plugin executes after `status` has run.")
1223

24+
# @post_deploy
25+
def deploy_helm():
26+
print("SimLN Plugin ⚡")
27+
plugin_dir = get_plugin_directory()
28+
print(f"DIR: {plugin_dir}")
29+
command = f"helm upgrade --install simln {plugin_dir}/simln/charts/simln"
30+
helm_result = run_command(command)
31+
print(helm_result)
1332

14-
def run():
15-
print("Running the simln plugin")
33+
34+
def run_simln():
35+
init_network()
36+
fund_wallets()
37+
wait_for_predicate(everyone_has_a_host)
38+
log.info(warnet("bitcoin rpc tank-0000 -generate 7"))
39+
# warnet("ln open-all-channels")
40+
manual_open_channels()
41+
log.info(warnet("bitcoin rpc tank-0000 -generate 7"))
42+
wait_for_gossip_sync(2)
43+
log.info("done waiting")
44+
pods = get_pods_with_label(lightning_selector)
45+
pod_a = pods[0].metadata.name
46+
pod_b = pods[1].metadata.name
47+
sample_activity = [
48+
{"source": pod_a, "destination": pod_b, "interval_secs": 1, "amount_msat": 2000}
49+
]
50+
log.info(f"Activity: {sample_activity}")
51+
generate_activity(sample_activity)
52+
log.info("Sent command. Done.")
53+
54+
55+
def generate_activity(activity: list[dict]):
56+
random_digits = "".join(random.choices("0123456789", k=10))
57+
plugin_dir = get_plugin_directory()
58+
generate_nodes_file(activity, plugin_dir / Path("simln/charts/simln/files/sim.json"))
59+
command = f"helm upgrade --install simln-{random_digits} {plugin_dir}/simln/charts/simln"
60+
log.info(f"generate activity: {command}")
61+
run_command(command)
62+
63+
64+
def init_network():
65+
log.info("Initializing network")
66+
wait_for_all_tanks_status(target="running")
67+
68+
warnet("bitcoin rpc tank-0000 createwallet miner")
69+
warnet("bitcoin rpc tank-0000 -generate 110")
70+
wait_for_predicate(lambda: int(warnet("bitcoin rpc tank-0000 getblockcount")) > 100)
71+
72+
def wait_for_all_ln_rpc():
73+
lns = get_pods_with_label(lightning_selector)
74+
for v1_pod in lns:
75+
ln = v1_pod.metadata.name
76+
try:
77+
warnet(f"ln rpc {ln} getinfo")
78+
except Exception:
79+
log.info(f"LN node {ln} not ready for rpc yet")
80+
return False
81+
return True
82+
83+
wait_for_predicate(wait_for_all_ln_rpc)
84+
85+
86+
def fund_wallets():
87+
log.info("Funding wallets")
88+
outputs = ""
89+
lns = get_pods_with_label(lightning_selector)
90+
for v1_pod in lns:
91+
lnd = v1_pod.metadata.name
92+
addr = json.loads(warnet(f"ln rpc {lnd} newaddress p2wkh"))["address"]
93+
outputs += f',"{addr}":10'
94+
# trim first comma
95+
outputs = outputs[1:]
96+
log.info(warnet("bitcoin rpc tank-0000 sendmany '' '{" + outputs + "}'"))
97+
log.info(warnet("bitcoin rpc tank-0000 -generate 1"))
98+
99+
100+
def everyone_has_a_host() -> bool:
101+
pods = get_pods_with_label(lightning_selector)
102+
host_havers = 0
103+
for pod in pods:
104+
name = pod.metadata.name
105+
result = warnet(f"ln host {name}")
106+
if len(result) > 1:
107+
host_havers += 1
108+
return host_havers == len(pods)
109+
110+
111+
def wait_for_predicate(predicate, timeout=5 * 60, interval=5):
112+
log.info(
113+
f"Waiting for predicate ({predicate.__name__}) with timeout {timeout}s and interval {interval}s"
114+
)
115+
while timeout > 0:
116+
try:
117+
if predicate():
118+
return
119+
except Exception:
120+
pass
121+
sleep(interval)
122+
timeout -= interval
123+
import inspect
124+
125+
raise Exception(
126+
f"Timed out waiting for Truth from predicate: {inspect.getsource(predicate).strip()}"
127+
)
128+
129+
130+
def wait_for_all_tanks_status(target="running", timeout=20 * 60, interval=5):
131+
"""Poll the warnet server for container status
132+
Block until all tanks are running
133+
"""
134+
135+
def check_status():
136+
tanks = network_status()
137+
stats = {"total": 0}
138+
# "Probably" means all tanks are stopped and deleted
139+
if len(tanks) == 0:
140+
return True
141+
for tank in tanks:
142+
status = tank["status"]
143+
stats["total"] += 1
144+
stats[status] = stats.get(status, 0) + 1
145+
log.info(f"Waiting for all tanks to reach '{target}': {stats}")
146+
return target in stats and stats[target] == stats["total"]
147+
148+
wait_for_predicate(check_status, timeout, interval)
149+
150+
151+
def wait_for_gossip_sync(expected):
152+
log.info(f"Waiting for sync (expecting {expected})...")
153+
current = 0
154+
while current < expected:
155+
current = 0
156+
pods = get_pods_with_label(lightning_selector)
157+
for v1_pod in pods:
158+
node = v1_pod.metadata.name
159+
chs = json.loads(run_command(f"warnet ln rpc {node} describegraph"))["edges"]
160+
log.info(f"{node}: {len(chs)} channels")
161+
current += len(chs)
162+
sleep(1)
163+
log.info("Synced")
164+
165+
166+
def warnet(cmd):
167+
log.info(f"Executing warnet command: {cmd}")
168+
command = ["warnet"] + cmd.split()
169+
proc = run(command, capture_output=True)
170+
if proc.stderr:
171+
raise Exception(proc.stderr.decode().strip())
172+
return proc.stdout.decode()
173+
174+
175+
def generate_nodes_file(activity, output_file: Path = Path("nodes.json")):
176+
nodes = []
177+
178+
for i in get_pods_with_label(lightning_selector):
179+
name = i.metadata.name
180+
node = {
181+
"id": name,
182+
"address": f"https://{name}:10009",
183+
"macaroon": "/config/admin.macaroon",
184+
"cert": "/config/tls.cert",
185+
}
186+
nodes.append(node)
187+
188+
data = {"nodes": nodes, "activity": activity}
189+
190+
with open(output_file, "w") as f:
191+
json.dump(data, f, indent=2)
192+
193+
194+
def manual_open_channels():
195+
def wait_for_two_txs():
196+
wait_for_predicate(
197+
lambda: json.loads(warnet("bitcoin rpc tank-0000 getmempoolinfo"))["size"] == 2
198+
)
199+
200+
# 0 -> 1 -> 2
201+
pk1 = warnet("ln pubkey tank-0001-ln")
202+
pk2 = warnet("ln pubkey tank-0002-ln")
203+
log.info(f"pk1: {pk1}")
204+
log.info(f"pk2: {pk2}")
205+
206+
host1 = ""
207+
host2 = ""
208+
209+
while not host1 or not host2:
210+
if not host1:
211+
host1 = warnet("ln host tank-0001-ln")
212+
if not host2:
213+
host2 = warnet("ln host tank-0002-ln")
214+
sleep(1)
215+
216+
print(
217+
warnet(
218+
f"ln rpc tank-0000-ln openchannel --node_key {pk1} --local_amt 100000 --connect {host1}"
219+
)
220+
)
221+
print(
222+
warnet(
223+
f"ln rpc tank-0001-ln openchannel --node_key {pk2} --local_amt 100000 --connect {host2}"
224+
)
225+
)
226+
227+
wait_for_two_txs()
228+
229+
warnet("bitcoin rpc tank-0000 -generate 10")

src/warnet/hooks.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,19 @@ def plugin():
3636
pass
3737

3838

39+
@click.group(name="util")
40+
def util():
41+
"""Plugin utility functions"""
42+
pass
43+
44+
45+
plugin.add_command(util)
46+
47+
3948
@plugin.command()
4049
def ls():
41-
"""List all available plugins and whether they are activated."""
42-
plugin_dir = get_plugin_directory()
50+
"""List all available plugins and whether they are activated"""
51+
plugin_dir = _get_plugin_directory()
4352

4453
if not plugin_dir:
4554
click.secho("Could not determine the plugin directory location.")
@@ -57,8 +66,8 @@ def ls():
5766
@plugin.command()
5867
@click.argument("plugin", type=str, default="")
5968
def toggle(plugin: str):
60-
"""Turn a plugin on or off"""
61-
plugin_dir = get_plugin_directory()
69+
"""Toggle a plugin on or off"""
70+
plugin_dir = _get_plugin_directory()
6271

6372
if plugin == "":
6473
plugin_list = get_plugins_with_status(plugin_dir)
@@ -67,15 +76,19 @@ def toggle(plugin: str):
6776
]
6877

6978
plugins_tag = "plugins"
70-
q = [
71-
inquirer.List(
72-
name=plugins_tag,
73-
message="Toggle a plugin, or ctrl-c to cancel",
74-
choices=formatted_list,
75-
)
76-
]
77-
selected = inquirer.prompt(q, theme=GreenPassion())
78-
plugin = selected[plugins_tag].split("|")[0].strip()
79+
try:
80+
q = [
81+
inquirer.List(
82+
name=plugins_tag,
83+
message="Toggle a plugin, or ctrl-c to cancel",
84+
choices=formatted_list,
85+
)
86+
]
87+
selected = inquirer.prompt(q, theme=GreenPassion())
88+
plugin = selected[plugins_tag].split("|")[0].strip()
89+
except TypeError:
90+
# user cancels and `selected[plugins_tag] fails with TypeError
91+
sys.exit(0)
7992

8093
plugin_settings = read_yaml(plugin_dir / Path(plugin) / "plugin.yaml")
8194
updated_settings = copy.deepcopy(plugin_settings)
@@ -87,6 +100,15 @@ def toggle(plugin: str):
87100
@click.argument("plugin", type=str)
88101
@click.argument("function", type=str)
89102
def run(plugin: str, function: str):
103+
"""Run a command available in a plugin"""
104+
plugin_dir = _get_plugin_directory()
105+
plugins = get_plugins_with_status(plugin_dir)
106+
for plugin_path, status in plugins:
107+
if plugin_path.stem == plugin and not status:
108+
click.secho(f"The plugin '{plugin_path.stem}' is not enabled", fg="yellow")
109+
click.secho("Please toggle it on to run commands.")
110+
sys.exit(0)
111+
90112
module = imported_modules.get(f"plugins.{plugin}")
91113
if hasattr(module, function):
92114
func = getattr(module, function)
@@ -193,7 +215,7 @@ def post_{hook}(func):
193215
def load_user_modules() -> bool:
194216
was_successful_load = False
195217

196-
plugin_dir = get_plugin_directory()
218+
plugin_dir = _get_plugin_directory()
197219

198220
if not plugin_dir or not plugin_dir.is_dir():
199221
return was_successful_load
@@ -243,7 +265,7 @@ def find_hooks(module_name: str, func_name: str):
243265
return pre_hooks, post_hooks
244266

245267

246-
def get_plugin_directory() -> Optional[Path]:
268+
def _get_plugin_directory() -> Optional[Path]:
247269
user_dir = os.getenv(WARNET_USER_DIR_ENV_VAR)
248270

249271
plugin_dir = Path(user_dir) / PLUGINS_LABEL if user_dir else Path.cwd() / PLUGINS_LABEL
@@ -254,6 +276,11 @@ def get_plugin_directory() -> Optional[Path]:
254276
return None
255277

256278

279+
@util.command()
280+
def get_plugin_directory():
281+
click.secho(_get_plugin_directory())
282+
283+
257284
def get_version(package_name: str) -> str:
258285
try:
259286
return version(package_name)

src/warnet/k8s.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,4 +560,4 @@ def get_pods_with_label(label_selector: str, namespace: Optional[str] = None) ->
560560
return v1_pods
561561
except client.exceptions.ApiException as e:
562562
print(f"Error fetching pods: {e}")
563-
return []
563+
return []

0 commit comments

Comments
 (0)