Skip to content

Commit afc29c6

Browse files
committed
rpc: export simln data
1 parent 8fa0640 commit afc29c6

File tree

8 files changed

+98
-46
lines changed

8 files changed

+98
-46
lines changed

src/backends/backend_interface.py

+7
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ def get_tank_ipv4(self, index: int) -> str:
109109
"""
110110
raise NotImplementedError("This method should be overridden by child class")
111111

112+
@abstractmethod
113+
def get_lnnode_hostname(self, index: int) -> str:
114+
"""
115+
Get the hostname assigned to a lnnode attached to a tank from the backend
116+
"""
117+
raise NotImplementedError("This method should be overridden by child class")
118+
112119
@abstractmethod
113120
def wait_for_healthy_tanks(self, warnet, timeout=60) -> bool:
114121
"""

src/backends/compose/compose_backend.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,12 @@ def add_services(self, tank: Tank, compose):
356356
"networks": [tank.network_name],
357357
}
358358

359+
def get_lnnode_hostname(self, index: int) -> str:
360+
return self.get_container_name(index, ServiceType.LIGHTNING)
361+
359362
def add_lnd_service(self, tank, compose):
360363
services = compose["services"]
361-
ln_container_name = self.get_container_name(tank.index, ServiceType.LIGHTNING)
364+
ln_container_name = self.get_lnnode_hostname(tank.index)
362365
ln_cb_container_name = self.get_container_name(tank.index, ServiceType.CIRCUITBREAKER)
363366
bitcoin_container_name = self.get_container_name(tank.index, ServiceType.BITCOIN)
364367
# These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]`
@@ -475,5 +478,10 @@ def service_from_json(self, service_name: str) -> object:
475478
"devices": obj.get("devices", []),
476479
"command": obj.get("args", []),
477480
"environment": obj.get("environment", []),
481+
"restart": "on-failure",
478482
"networks": [self.network_name]
479483
}
484+
485+
def restart_service_container(self, service_name: str):
486+
container = self.client.containers.get(self.get_service_container_name(service_name))
487+
container.restart()

src/backends/kubernetes/kubernetes_backend.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -442,12 +442,15 @@ def remove_prometheus_service_monitors(self, tanks):
442442
except ResourceNotFoundError:
443443
continue
444444

445+
def get_lnnode_hostname(self, index: int) -> str:
446+
return f"lightning-{index}.{self.namespace}"
447+
445448
def create_lnd_container(
446449
self, tank, bitcoind_service_name, volume_mounts
447450
) -> client.V1Container:
448451
# These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]`
449452
bitcoind_rpc_host = f"{bitcoind_service_name}.{self.namespace}"
450-
lightning_dns = f"lightning-{tank.index}.{self.namespace}"
453+
lightning_dns = self.get_lnnode_hostname(tank.index)
451454
args = tank.lnnode.get_conf(lightning_dns, bitcoind_rpc_host)
452455
self.log.debug(f"Creating lightning container for tank {tank.index} using {args=:}")
453456
lightning_container = client.V1Container(

src/cli/network.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,11 @@ def connected(network: str):
143143

144144
@network.command()
145145
@click.option("--network", default="warnet", show_default=True)
146-
def export(network):
146+
@click.option("--activity", type=str)
147+
def export(network: str, activity: str):
147148
"""
148-
Export all [network] data for sim-ln to subdirectory
149+
Export all [network] data for a "simln" service running in a container
150+
on the network. Optionally add JSON string [activity] to simln config.
151+
Returns True on success.
149152
"""
150-
print(rpc_call("network_export", {"network": network}))
153+
print(rpc_call("network_export", {"network": network, "activity": activity}))

src/warnet/lnnode.py

+27-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import os
1+
import io
2+
import tarfile
23

34
from backends import BackendInterface, ServiceType
45
from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json
@@ -115,30 +116,38 @@ def generate_cli_command(self, command: list[str]):
115116
raise Exception(f"Unsupported LN implementation: {self.impl}")
116117
return cmd
117118

118-
def export(self, config, subdir):
119-
container_name = self.backend.get_container_name(self.tank.index, ServiceType.LIGHTNING)
120-
macaroon_filename = f"{container_name}_admin.macaroon"
121-
cert_filename = f"{container_name}_tls.cert"
122-
macaroon_path = os.path.join(subdir, macaroon_filename)
123-
cert_path = os.path.join(subdir, cert_filename)
119+
def export(self, config: object, tar_file):
120+
# Retrieve the credentials
124121
macaroon = self.backend.get_file(
125122
self.tank.index,
126123
ServiceType.LIGHTNING,
127124
"/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon",
128125
)
129-
cert = self.backend.get_file(self.tank.index, ServiceType.LIGHTNING, "/root/.lnd/tls.cert")
130-
131-
with open(macaroon_path, "wb") as f:
132-
f.write(macaroon)
133-
134-
with open(cert_path, "wb") as f:
135-
f.write(cert)
126+
cert = self.backend.get_file(
127+
self.tank.index,
128+
ServiceType.LIGHTNING,
129+
"/root/.lnd/tls.cert"
130+
)
131+
name = f"ln-{self.tank.index}"
132+
macaroon_filename = f"{name}_admin.macaroon"
133+
cert_filename = f"{name}_tls.cert"
134+
host = self.backend.get_lnnode_hostname(self.tank.index)
135+
136+
# Add the files to the in-memory tar archive
137+
tarinfo1 = tarfile.TarInfo(name=macaroon_filename)
138+
tarinfo1.size = len(macaroon)
139+
fileobj1 = io.BytesIO(macaroon)
140+
tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1)
141+
tarinfo2 = tarfile.TarInfo(name=cert_filename)
142+
tarinfo2.size = len(cert)
143+
fileobj2 = io.BytesIO(cert)
144+
tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2)
136145

137146
config["nodes"].append(
138147
{
139-
"id": container_name,
140-
"address": f"https://{self.ipv4}:{self.rpc_port}",
141-
"macaroon": macaroon_path,
142-
"cert": cert_path,
148+
"id": name,
149+
"address": f"https://{host}:{self.rpc_port}",
150+
"macaroon": f"/simln/{macaroon_filename}",
151+
"cert": f"/simln/{cert_filename}",
143152
}
144153
)

src/warnet/server.py

+41-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
22
import base64
3+
import io
34
import json
45
import logging
56
import logging.config
@@ -10,6 +11,7 @@
1011
import signal
1112
import subprocess
1213
import sys
14+
import tarfile
1315
import tempfile
1416
import threading
1517
import time
@@ -268,20 +270,47 @@ def tank_messages(self, network: str, node_a: int, node_b: int) -> str:
268270
self.logger.error(msg)
269271
raise ServerError(message=msg) from e
270272

271-
def network_export(self, network: str) -> str:
273+
def network_export(self, network: str, activity: str | None) -> bool:
272274
"""
273-
Export all data for sim-ln to subdirectory
275+
Export all data for a simln container running on the network
274276
"""
275-
try:
276-
wn = self.get_warnet(network)
277-
subdir = os.path.join(wn.config_dir, "simln")
278-
os.makedirs(subdir, exist_ok=True)
279-
wn.export(subdir)
280-
return subdir
281-
except Exception as e:
282-
msg = f"Error exporting network: {e}"
283-
self.logger.error(msg)
284-
raise ServerError(message=msg) from e
277+
wn = self.get_warnet(network)
278+
if "simln" not in wn.services:
279+
raise Exception("No simln service in network")
280+
281+
# JSON object that will eventually be written to simln config file
282+
config = {"nodes": []}
283+
if activity:
284+
config["activity"] = json.loads(activity)
285+
# In-memory file to build tar archive
286+
tar_buffer = io.BytesIO()
287+
with tarfile.open(fileobj=tar_buffer, mode="w") as tar_file:
288+
# tank LN nodes add their credentials to tar archive
289+
wn.export(config, tar_file)
290+
# write config file
291+
config_bytes = json.dumps(config).encode('utf-8')
292+
config_stream = io.BytesIO(config_bytes)
293+
tarinfo = tarfile.TarInfo(name="sim.json")
294+
tarinfo.size = len(config_bytes)
295+
tar_file.addfile(tarinfo=tarinfo, fileobj=config_stream)
296+
297+
# Write the archive to the RPC server's config directory
298+
source_file = wn.config_dir / "simln.tar"
299+
with open(source_file, "wb") as output:
300+
tar_buffer.seek(0)
301+
output.write(tar_buffer.read())
302+
303+
if self.backend == "compose":
304+
# Extract the archive into a subdirectory that is already
305+
# shared with the simln container as a volume
306+
subprocess.run(["tar", "-xf", source_file, "-C", wn.config_dir / 'simln'])
307+
# Force quick restart of the container instead of waiting
308+
# for the exponential backoff to come around
309+
wn.container_interface.restart_service_container("simln")
310+
if self.backend == "k8s":
311+
# Copy the archive to the "emptydir" volume in the simln pod
312+
wn.container_interface.write_service_config(source_file, "simln", "/simln/")
313+
return True
285314

286315
def scenarios_available(self) -> list[tuple]:
287316
"""

src/warnet/tank.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,6 @@ def apply_network_conditions(self):
169169
f"Error applying network conditions to tank {self.index}: `{self.netem}` ({e})"
170170
)
171171

172-
def export(self, config, subdir):
172+
def export(self, config: object, tar_file):
173173
if self.lnnode is not None:
174-
self.lnnode.export(config, subdir)
174+
self.lnnode.export(config, tar_file)

src/warnet/warnet.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import base64
66
import json
77
import logging
8-
import os
98
import shutil
109
from pathlib import Path
1110

@@ -239,15 +238,9 @@ def write_prometheus_config(self):
239238
except Exception as e:
240239
logger.error(f"An error occurred while writing to {prometheus_path}: {e}")
241240

242-
def export(self, subdir):
243-
if self.backend != "compose":
244-
raise NotImplementedError("Export is only supported for compose backend")
245-
config = {"nodes": []}
241+
def export(self, config: object, tar_file):
246242
for tank in self.tanks:
247-
tank.export(config, subdir)
248-
config_path = os.path.join(subdir, "sim.json")
249-
with open(config_path, "a") as f:
250-
json.dump(config, f)
243+
tank.export(config, tar_file)
251244

252245
def wait_for_health(self):
253246
self.container_interface.wait_for_healthy_tanks(self)

0 commit comments

Comments
 (0)