From 21bd8791cf11d120d490c2cbf5b8517bebdd3003 Mon Sep 17 00:00:00 2001 From: Marko Semet Date: Thu, 16 Apr 2020 22:21:29 +0200 Subject: [PATCH] Add backup. --- home_backup/__main__.py | 30 ++++++- home_backup/client.py | 13 +++ home_backup/sys_service/snapshot.py | 2 +- home_backup/user_service/backups.py | 125 ++++++++++++++++++++++++---- home_backup/user_service/config.py | 15 ++-- home_backup/user_service/rpc.py | 6 +- 6 files changed, 163 insertions(+), 28 deletions(-) diff --git a/home_backup/__main__.py b/home_backup/__main__.py index 31aeaa7..fd37d7f 100644 --- a/home_backup/__main__.py +++ b/home_backup/__main__.py @@ -12,6 +12,13 @@ def main(args): parser = argparse.ArgumentParser("home-backup", description="Manage backup tools") sub_parser = parser.add_subparsers(dest="action") + parser_backup_add = sub_parser.add_parser("backup-add", help="Add backup.") + parser_backup_add.add_argument("--type", nargs="?", type=str, default="borgbackup", help="The backup type.\nDefault: borgbackup") + parser_backup_add.add_argument("--remote", nargs="?", type=str, default=None, help="The remote of the backup (required by borgbackup).") + parser_backup_add.add_argument("name", nargs=1, type=str, help="The name of the backup.") + parser_backup_list = sub_parser.add_parser("backup-list", help="List all backups.") + parser_backup_delete = sub_parser.add_parser("backup-delete", help="Delete a backup.") + parser_remote_add = sub_parser.add_parser("remote-add", help="Add remote for backups.") parser_remote_add.add_argument("--type", nargs="?", type=str, default="borgbackup", help="The remote type.\nDefault: borgbackup") parser_remote_add.add_argument("--target", nargs="?", type=str, default=None, help="The target of the remote (required by borgbackup).") @@ -43,8 +50,27 @@ def main(args): elif result.action == "user-server": asyncio.run(user_service.rpc.run_deamon(fork=result.fork)) # TODO: Change default path of user and system socket - # Client actions - if result.action == "remote-add": + # Backup + if result.action == "backup-add": + # Check backup type + backup_type = result.type + if backup_type not in ("borgbackup",): + raise ValueError("Unknown backup type %s." & repr(backup_type)) + + # Parse type + info = {} + if backup_type == "borgbackup": + if result.remote is None: + raise ValueError("Remote is required for borg backup.") + info["remote"] = result.remote + else: + raise NotImplementedError("Type %s isn't supported well." % repr(backup_type)) + + # Add backup + client.run_command(client.backup_add_gen(name=result.name[0], btype=backup_type, info=info)) + + # Remote + elif result.action == "remote-add": # Check remote type remote_type = result.type if remote_type not in ("borgbackup",): diff --git a/home_backup/client.py b/home_backup/client.py index 9b38523..4186f2b 100644 --- a/home_backup/client.py +++ b/home_backup/client.py @@ -19,6 +19,19 @@ def run_command(async_func, user_path:str=None): await async_func(sock) asyncio.run(runner()) +# +# Backups +# +def backup_add_gen(name:str, btype:str, info): + async def backup_add(con:utils.Connection): + result = await con.call({"operation": "backup-add", "name": name, "type": btype, "info": info}) + if result["status"] != "success": + if result["status"] == "fail-already-exists": + print("Backup %s already exists." % name) + exit(1) + else: + raise RuntimeError("Wasn't able to add remote.") # TODO: Show error + return backup_add # # Remotes diff --git a/home_backup/sys_service/snapshot.py b/home_backup/sys_service/snapshot.py index 3d721e4..4355df7 100644 --- a/home_backup/sys_service/snapshot.py +++ b/home_backup/sys_service/snapshot.py @@ -15,4 +15,4 @@ async def create_snapshot(name:str, user:str): # List subvolumes subvols = btrfs.list_path_subvolumes(mount, user_path) print(repr(subvols)) - raise \ No newline at end of file + raise NotImplementedError() \ No newline at end of file diff --git a/home_backup/user_service/backups.py b/home_backup/user_service/backups.py index b74d91b..6432f8c 100644 --- a/home_backup/user_service/backups.py +++ b/home_backup/user_service/backups.py @@ -1,15 +1,18 @@ import asyncio +from . import remotes from .. import utils class Backup(): name:str + btype:str periode:int blocked:set to_backup:list + remote:remotes.Remote - def __init__(self, name:str, periode:int=None, blocked:list=[], to_backup:list=[]): + def __init__(self, name:str, btype:str="borgbackup", periode:int=None, blocked:list=[], to_backup:list=[], remote:remotes.Remote=None): # Check args utils.valid_name_check(name) if periode is not None and not isinstance(periode, int): @@ -25,12 +28,29 @@ class Backup(): raise TypeError("to_backup have to be a string.") if ":" in i: raise ValueError(": isn't allowed a char.") + if not isinstance(btype, str): + raise TypeError("btype has to be a string.") + if btype not in ("borgbackup",): + raise ValueError("%s is an unknown backup tool." % repr(btype)) + if remote is not None and not isinstance(remote, remotes.Remote): + raise TypeError("remote has to be an remote object.") + + # Check type + if btype == "borgbackup": + if remote is None: + raise ValueError("remote is required for borg backup.") + if remote.rtype != "borgbackup": + raise ValueError("remote has to be an borg backup target.") + else: + raise NotImplementedError("%s isn't an implemented type." % btype) # Set values self.name = name self.periode = periode - self.blocks = blocked + self.blocked = blocked self.to_backup = to_backup + self.btype = btype + self.remote = remote def get_next_scedule(self, latest, zero): if self.periode is not None: @@ -41,38 +61,111 @@ class Backup(): def dump_config(self): result = {} + result["type"] = self.btype if self.blocked: result["blocked"] = ",".join(self.blocked) if self.periode is not None: result["periode"] = str(self.periode) if self.to_backup: result["to_backup"] = ":".join(self.to_backup) + if self.remote: + result["remote"] = self.remote.name return result @staticmethod - def load_backup(name:str, config): + def load_backup(name:str, conf, remotes): # Load informations - config = dict(config.items()) + conf = dict(conf.items()) + + btype = conf["type"] + del conf["type"] periode = None - if "periode" in config: - periode = int(config["periode"]) - del config["periode"] + if "periode" in conf: + periode = int(conf["periode"]) + del conf["periode"] blocked = [] - if "blocked" in config: - blocked = config["blocked"].split(",") - del config["blocked"] + if "blocked" in conf: + blocked = conf["blocked"].split(",") + del conf["blocked"] to_backup = [] - if "to_backup" in config: - to_backup = config["to_backup"].split(":") - del config["to_backup"] + if "to_backup" in conf: + to_backup = conf["to_backup"].split(":") + del conf["to_backup"] + + remote = None + if "remote" in conf: + remote = conf["remote"] + if remote not in remotes.keys(): + raise ValueError("Remote %s doesn't exists but is required." & remote) + remote = remotes[remote] + del conf["remote"] # Generate backup - utils.check_empty_data_dict(config) + utils.check_empty_data_dict(conf) - return Backup(name=name, periode=periode, blocked=blocked, to_backup=to_backup) + return Backup(name=name, btype=btype, periode=periode, blocked=blocked, to_backup=to_backup, remote=remote) async def run_backup(self, subvolumes:list): - print("Subvolumes: %s" % repr(subvolumes)) \ No newline at end of file + print("Subvolumes: %s" % repr(subvolumes)) + +# RPC implementations +async def add_backup(data): + # Import config + from . import config + + # Load base values + name = data["name"] + del data["name"] + + info = data["info"] + if not isinstance(info, dict): + raise TypeError("info has to be an object.") + del data["info"] + + btype = data["type"] + del data["type"] + utils.check_empty_data_dict(data) + + # Load info + periode = None + if "periode" in info: + periode = int(info["periode"]) + del info["periode"] + + blocked = [] + if "blocked" in info: + blocked = info["blocked"] + del info["blocked"] + + to_backup = [] + if "to_backup" in info: + to_backup = info["to_backup"] + del info["to_backup"] + + remote = None + if "remote" in info: + remote = info["remote"] + del info["remote"] + + utils.check_empty_data_dict(info) + + # Add backup + async with config.config_lock: + # Check if backup exists + for _ in filter(lambda x: x.name == name, config.backups): + return {"status": "fail-already-exists"} + + # Search for remote + if remote is not None: + if remote not in config.remotes: + return {"status": "fail-remote-missing"} + remote = config.remotes[remote] + + # Add backup + backup = Backup(name=name, btype=btype, periode=periode, blocked=blocked, to_backup=to_backup, remote=remote) + config.backups.append(backup) + await config.save_config() + return {"status": "success"} \ No newline at end of file diff --git a/home_backup/user_service/config.py b/home_backup/user_service/config.py index 8179c3b..9c4245e 100644 --- a/home_backup/user_service/config.py +++ b/home_backup/user_service/config.py @@ -26,18 +26,19 @@ if os.path.exists(CONFIG_FILE): config = configparser.ConfigParser() config.read(CONFIG_FILE) - backups = [] remotes = {} for iID, i in filter(lambda x: x[0] != "DEFAULT", config.items()): - if iID.startswith("BACKUP|"): # Parse backup config - iID = iID[len("BACKUP|"):] - backups.append(Backup.load_backup(iID, i)) - elif iID.startswith("REMOTE|"): # Parse remote config + if iID.startswith("REMOTE|"): # Parse remote config iID = iID[len("REMOTE|"):] tmp = Remote.load_remote(iID, i) remotes[tmp.name] = tmp - else: - raise ValueError("Unknown config part %s." % repr(iID)) + + backups = [] + for iID, i in filter(lambda x: x[0] != "DEFAULT", config.items()): + if iID.startswith("BACKUP|"): # Parse backup config + iID = iID[len("BACKUP|"):] + backups.append(Backup.load_backup(iID, i, remotes)) + return backups, remotes backups, remotes = _parse_config() diff --git a/home_backup/user_service/rpc.py b/home_backup/user_service/rpc.py index 2df1f12..648e140 100644 --- a/home_backup/user_service/rpc.py +++ b/home_backup/user_service/rpc.py @@ -1,6 +1,6 @@ import asyncio import os -from . import config, remotes +from . import backups, config, remotes from .. import defaults, utils @@ -36,7 +36,9 @@ def gen_callback_func(master:BackupManager): del data["operation"] # Run operation - if operation == "remote-add": + if operation == "backup-add": + return await backups.add_backup(data) + elif operation == "remote-add": return await remotes.add_remote(data) elif operation == "remote-list": return await remotes.remote_list(data)