diff --git a/backup/backup-cli b/backup/backup-cli new file mode 100755 index 0000000..9206a2a --- /dev/null +++ b/backup/backup-cli @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +from backup import FileBackend +import os +import click +import json + + +@click.command() +@click.argument("lightning-dir", type=click.Path(exists=True)) +@click.argument("backup-dir", type=click.Path(exists=True)) +def init(lightning_dir, backup_dir): + destination = 'file://' + os.path.join(backup_dir, 'backup.dbak') + backend = FileBackend(destination) + backend.version, backend.prev_version = 0, 0 + backend.offsets = [512, 0] + backend.version_count = 0 + backend.write_metadata() + + lock_file = os.path.join(lightning_dir, "backup.lock") + + with open(lock_file, "w") as f: + f.write(json.dumps({ + 'backend_url': destination, + })) + + print("Initialized backup backend {destination}, you can now start c-lightning".format( + destination=destination, + )) + + +@click.group() +def cli(): + pass + + +cli.add_command(init) + +if __name__ == "__main__": + cli() diff --git a/backup/backup.py b/backup/backup.py index 2c0bbfa..c4a514e 100755 --- a/backup/backup.py +++ b/backup/backup.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 -from pyln.client import Plugin -from pprint import pprint from collections import namedtuple +from pyln.client import Plugin +from typing import Mapping, Type from urllib.parse import urlparse -import struct -import os -from typing import Mapping, Type, Optional +import json import logging +import os +import struct import sys -from binascii import hexlify plugin = Plugin() @@ -25,7 +24,8 @@ root.addHandler(handler) # A change that was proposed by c-lightning that needs saving to the # backup. `version` is the database version before the transaction was # applied. -Change = namedtuple('Change',['version', 'transaction']) +Change = namedtuple('Change', ['version', 'transaction']) + class Backend(object): def __init__(self, destination: str): @@ -40,6 +40,7 @@ class Backend(object): def initialize(self) -> bool: raise NotImplementedError + class FileBackend(Backend): def __init__(self, destination: str): self.version = None @@ -116,9 +117,15 @@ class FileBackend(Backend): self.prev_version, self.offsets[1] = 0, 0 return True -backend_map: Mapping[str, Type[Backend]] = { - 'file': FileBackend, -} + +def resolve_backend_class(backend_url): + backend_map: Mapping[str, Type[Backend]] = { + 'file': FileBackend, + } + p = urlparse(backend_url) + backend_cl = backend_map.get(p.scheme, None) + return backend_cl + def abort(reason: str) -> None: plugin.log(reason) @@ -145,7 +152,7 @@ def check_first_write(plugin, data_version): backend.version, data_version )) - if backend.version == data_version - 1: + if backend.version == data_version - 1: logging.info("Versions match up") return True @@ -165,7 +172,7 @@ def on_db_write(writes, data_version, plugin, **kwargs): change = Change(data_version, writes) if not hasattr(plugin, 'backend'): plugin.early_writes.append(change) - return True + return {"result": "continue"} else: return apply_write(plugin, change) @@ -175,7 +182,8 @@ def apply_write(plugin, change): assert(check_first_write(plugin, change.version)) plugin.initialized = True - return plugin.backend.add_entry(change) + if plugin.backend.add_change(change): + return {"result": "continue"} @plugin.init() @@ -185,6 +193,17 @@ def on_init(options: Mapping[str, str], plugin: Plugin, **kwargs): plugin.db_path = configs['wallet'] destination = options['backup-destination'] + # Ensure that we don't inadventently switch the destination + if os.path.exists("backup.lock"): + d = json.load(open("backup.lock", 'r')) + if destination is None or destination == 'null': + destination = d['backend_url'] + elif destination != d['backend_url']: + abort( + "The destination specified as option does not match the one " + "specified in backup.lock. Please check your settings" + ) + if not plugin.db_path.startswith('sqlite3'): abort("The backup plugin only works with the sqlite3 database.") @@ -192,8 +211,7 @@ def on_init(options: Mapping[str, str], plugin: Plugin, **kwargs): abort("You must specify a backup destination, possibly on a secondary disk.") # Let's initialize the backed. First we need to figure out which backend to use. - p = urlparse(destination) - backend_cl = backend_map.get(p.scheme, None) + backend_cl = resolve_backend_class(destination) if backend_cl is None: abort("Could not find a backend for scheme {p.scheme}".format(p=p)) diff --git a/backup/fixtures/backup.dbak b/backup/fixtures/backup.dbak deleted file mode 100644 index 06b3e4e..0000000 Binary files a/backup/fixtures/backup.dbak and /dev/null differ diff --git a/backup/test_backup.py b/backup/test_backup.py index db2bf19..52ca17d 100644 --- a/backup/test_backup.py +++ b/backup/test_backup.py @@ -3,20 +3,23 @@ from pyln.client import RpcError from pyln.testing.fixtures import * import os import time +import shutil +import subprocess + plugin_dir = os.path.dirname(__file__) -plugin_path = os.path.join(plugin_dir, "backup.py") +plugin_path = os.path.join(plugin_dir, "backup.py") +cli_path = os.path.join(os.path.dirname(__file__), "backup-cli") def test_start(node_factory, directory): + bdest = os.path.join(directory, 'backup.dbak') opts = { 'plugin': plugin_path, - 'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') + 'backup-destination': 'file://' + bdest, } - shutil.copyfile( - os.path.join(plugin_dir, 'fixtures', "backup.dbak"), - os.path.join(directory, "backup.dbak") - ) + + subprocess.check_call([cli_path, "init", directory, directory]) l1 = node_factory.get_node(options=opts) l1.daemon.wait_for_log(r'backup.py') @@ -37,14 +40,12 @@ def test_tx_abort(node_factory, directory): inbetween the hook call and the DB transaction. """ + bdest = os.path.join(directory, 'backup.dbak') opts = { 'plugin': plugin_path, - 'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') + 'backup-destination': 'file://' + bdest, } - shutil.copyfile( - os.path.join(plugin_dir, 'fixtures', "backup.dbak"), - os.path.join(directory, "backup.dbak") - ) + subprocess.check_call([cli_path, "init", directory, directory]) l1 = node_factory.get_node(options=opts) l1.stop() @@ -71,10 +72,7 @@ def test_failing_restore(nf, directory): 'plugin': plugin_path, 'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') } - shutil.copyfile( - os.path.join(plugin_dir, 'fixtures', "backup.dbak"), - os.path.join(directory, "backup.dbak") - ) + subprocess.check_call([cli_path, "init", directory, directory]) l1 = node_factory.get_node(options=opts) l1.stop() @@ -97,10 +95,7 @@ def test_intermittent_backup(node_factory, directory): 'plugin': plugin_path, 'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') } - shutil.copyfile( - os.path.join(plugin_dir, 'fixtures', "backup.dbak"), - os.path.join(directory, "backup.dbak") - ) + subprocess.check_call([cli_path, "init", directory, directory]) l1 = node_factory.get_node(options=opts) # Now start without the plugin. This should work fine.