backup: Add backup-cli tool and use it to initialize the backups

This commit is contained in:
Christian Decker
2020-04-04 14:25:33 +02:00
parent c049069cff
commit 4e19c32444
4 changed files with 86 additions and 34 deletions

39
backup/backup-cli Executable file
View File

@@ -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()

View File

@@ -1,14 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from pyln.client import Plugin
from pprint import pprint
from collections import namedtuple from collections import namedtuple
from pyln.client import Plugin
from typing import Mapping, Type
from urllib.parse import urlparse from urllib.parse import urlparse
import struct import json
import os
from typing import Mapping, Type, Optional
import logging import logging
import os
import struct
import sys import sys
from binascii import hexlify
plugin = Plugin() plugin = Plugin()
@@ -25,7 +24,8 @@ root.addHandler(handler)
# A change that was proposed by c-lightning that needs saving to the # A change that was proposed by c-lightning that needs saving to the
# backup. `version` is the database version before the transaction was # backup. `version` is the database version before the transaction was
# applied. # applied.
Change = namedtuple('Change',['version', 'transaction']) Change = namedtuple('Change', ['version', 'transaction'])
class Backend(object): class Backend(object):
def __init__(self, destination: str): def __init__(self, destination: str):
@@ -40,6 +40,7 @@ class Backend(object):
def initialize(self) -> bool: def initialize(self) -> bool:
raise NotImplementedError raise NotImplementedError
class FileBackend(Backend): class FileBackend(Backend):
def __init__(self, destination: str): def __init__(self, destination: str):
self.version = None self.version = None
@@ -116,9 +117,15 @@ class FileBackend(Backend):
self.prev_version, self.offsets[1] = 0, 0 self.prev_version, self.offsets[1] = 0, 0
return True return True
backend_map: Mapping[str, Type[Backend]] = {
def resolve_backend_class(backend_url):
backend_map: Mapping[str, Type[Backend]] = {
'file': FileBackend, 'file': FileBackend,
} }
p = urlparse(backend_url)
backend_cl = backend_map.get(p.scheme, None)
return backend_cl
def abort(reason: str) -> None: def abort(reason: str) -> None:
plugin.log(reason) plugin.log(reason)
@@ -165,7 +172,7 @@ def on_db_write(writes, data_version, plugin, **kwargs):
change = Change(data_version, writes) change = Change(data_version, writes)
if not hasattr(plugin, 'backend'): if not hasattr(plugin, 'backend'):
plugin.early_writes.append(change) plugin.early_writes.append(change)
return True return {"result": "continue"}
else: else:
return apply_write(plugin, change) return apply_write(plugin, change)
@@ -175,7 +182,8 @@ def apply_write(plugin, change):
assert(check_first_write(plugin, change.version)) assert(check_first_write(plugin, change.version))
plugin.initialized = True plugin.initialized = True
return plugin.backend.add_entry(change) if plugin.backend.add_change(change):
return {"result": "continue"}
@plugin.init() @plugin.init()
@@ -185,6 +193,17 @@ def on_init(options: Mapping[str, str], plugin: Plugin, **kwargs):
plugin.db_path = configs['wallet'] plugin.db_path = configs['wallet']
destination = options['backup-destination'] 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'): if not plugin.db_path.startswith('sqlite3'):
abort("The backup plugin only works with the sqlite3 database.") 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.") 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. # Let's initialize the backed. First we need to figure out which backend to use.
p = urlparse(destination) backend_cl = resolve_backend_class(destination)
backend_cl = backend_map.get(p.scheme, None)
if backend_cl is None: if backend_cl is None:
abort("Could not find a backend for scheme {p.scheme}".format(p=p)) abort("Could not find a backend for scheme {p.scheme}".format(p=p))

Binary file not shown.

View File

@@ -3,20 +3,23 @@ from pyln.client import RpcError
from pyln.testing.fixtures import * from pyln.testing.fixtures import *
import os import os
import time import time
import shutil
import subprocess
plugin_dir = os.path.dirname(__file__) 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): def test_start(node_factory, directory):
bdest = os.path.join(directory, 'backup.dbak')
opts = { opts = {
'plugin': plugin_path, '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"), subprocess.check_call([cli_path, "init", directory, directory])
os.path.join(directory, "backup.dbak")
)
l1 = node_factory.get_node(options=opts) l1 = node_factory.get_node(options=opts)
l1.daemon.wait_for_log(r'backup.py') 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. inbetween the hook call and the DB transaction.
""" """
bdest = os.path.join(directory, 'backup.dbak')
opts = { opts = {
'plugin': plugin_path, 'plugin': plugin_path,
'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') 'backup-destination': 'file://' + bdest,
} }
shutil.copyfile( subprocess.check_call([cli_path, "init", directory, directory])
os.path.join(plugin_dir, 'fixtures', "backup.dbak"),
os.path.join(directory, "backup.dbak")
)
l1 = node_factory.get_node(options=opts) l1 = node_factory.get_node(options=opts)
l1.stop() l1.stop()
@@ -71,10 +72,7 @@ def test_failing_restore(nf, directory):
'plugin': plugin_path, 'plugin': plugin_path,
'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') 'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak')
} }
shutil.copyfile( subprocess.check_call([cli_path, "init", directory, directory])
os.path.join(plugin_dir, 'fixtures', "backup.dbak"),
os.path.join(directory, "backup.dbak")
)
l1 = node_factory.get_node(options=opts) l1 = node_factory.get_node(options=opts)
l1.stop() l1.stop()
@@ -97,10 +95,7 @@ def test_intermittent_backup(node_factory, directory):
'plugin': plugin_path, 'plugin': plugin_path,
'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak') 'backup-destination': 'file://' + os.path.join(directory, 'backup.dbak')
} }
shutil.copyfile( subprocess.check_call([cli_path, "init", directory, directory])
os.path.join(plugin_dir, 'fixtures', "backup.dbak"),
os.path.join(directory, "backup.dbak")
)
l1 = node_factory.get_node(options=opts) l1 = node_factory.get_node(options=opts)
# Now start without the plugin. This should work fine. # Now start without the plugin. This should work fine.