Remote add.

master
Marko Semet 2020-04-16 16:02:31 +02:00
parent d19d8c0198
commit 00d79c8adf
6 changed files with 178 additions and 30 deletions

View File

@ -1,7 +1,7 @@
import argparse
import asyncio
import os
from . import user_service, sys_service
from . import client, sys_service, user_service
from .sys_service import rpc
from .user_service import rpc
del rpc
@ -11,7 +11,11 @@ def main(args):
# Parser
parser = argparse.ArgumentParser("home-backup", description="Manage backup tools")
sub_parser = parser.add_subparsers(dest="action")
parser_remote_add = sub_parser.add_parser("remote-add", help="Add remote for backups.")
parser_remote_add.add_argument("--type", nargs=1, type=str, default=["borgbackup"], help="The remote type.\nDefault: borgbackup")
parser_remote_add.add_argument("--target", nargs=1, type=str, default=None, help="The target of the remote (required by borgbackup).")
parser_remote_add.add_argument("name", nargs=1, type=str, help="The name of the remote.")
parser_remote_list = sub_parser.add_parser("remote-list", help="List remotes.")
parser_remote_delete = sub_parser.add_parser("remote-delete", help="Delete remote.")
@ -38,6 +42,23 @@ def main(args):
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":
# Check remote type
remote_type = result.type[0]
if remote_type not in ("borgbackup",):
raise ValueError("Unknown backup type %s." % repr(remote_type))
# Parse type
info = {}
if remote_type == "borgbackup":
if result.target is None:
raise ValueError("Target isn't set.")
info["target"] = result.target[0]
else:
raise NotImplementedError("Type %s isn't supported well." % remote_type)
# Add remote
client.run_command(client.remote_add_gen(name=result.name[0], rtype=remote_type, info=info))
# Not found action
elif result.action is None:

View File

@ -0,0 +1,31 @@
import asyncio
from . import defaults, utils
#
# Base connector
#
def run_command(async_func, user_path:str=None):
async def runner():
# Connect to user socket
nonlocal user_path
if user_path is None:
user_path = defaults.USER_PATH
if user_path is None:
raise RuntimeError("User service socket path isn't set.")
sock = utils.Connection()
await sock.init(user_path)
# Run async function
await async_func(sock)
asyncio.run(runner())
#
# Remotes
#
def remote_add_gen(name:str, rtype:str, info):
async def remote_add(con:utils.Connection):
result = await con.call({"operation": "remote-add", "name": name, "type": rtype, "info": info})
if result["status"] != "success":
raise RuntimeError("Wasn't able to add remote.") # TODO: Show error
return remote_add

View File

@ -4,6 +4,7 @@ import configparser
import io
import os
import time
from .remotes import Remote
from .. import utils
@ -23,20 +24,14 @@ class Backup():
def __init__(self, name:str, periode:int=None, blocked:list=[]):
# Check args
if not isinstance(name, str):
raise TypeError("Name has to be a string.")
for i in filter(lambda x: not("a" <= x <= "z" or "A" <= x <= "Z" or "0" <= x <= "9" or x in ("_", "-")), i):
raise ValueError("%s isn't a valid char in a backup name." % i)
utils.valid_name_check(name)
if periode is not None and not isinstance(periode, int):
raise TypeError("Periode have to be an integer or null.")
if periode is not None and periode < 0:
raise ValueError("periode can't be negetive.")
blocked = set(blocked)
for i in blocked:
if not isinstance(i, str):
raise TypeError("Blocked have to be a list of strings.")
for j in filter(lambda x: not("a" <= x <= "z" or "A" <= x <= "Z" or "0" <= x <= "9" or x in ("_", "-")), i):
raise ValueError("%s isn't a valid char in a block name." % i)
utils.valid_name_check(i)
# Set values
self.name = name
@ -52,31 +47,41 @@ class Backup():
backups = []
backups_lock = asyncio.Lock()
remotes = {}
config_lock = asyncio.Lock()
if os.path.exists(CONFIG_FILE):
def _parse_config():
# Load config
config = configparser.ConfigParser()
config.read(CONFIG_FILE)
# Create backups
backups = []
for iID, i in filter(lambda x: x[0].startswith("BACKUP|"), config.items()):
iID = iID[len("BACKUP|"):]
periode = None
if "periode" in i:
periode = int(i["periode"])
blocked = []
if "blocked" in i:
blocked = i["blocked"].split(",")
backups.append(Backup(iID, periode=periode, blocked=blocked))
return backups
backups = _parse_config()
remotes = {}
for iID, i in filter(lambda x: x[0] != "DEFAULT", config.items()):
if iID.startswith("BACKUP|"): # Parse backup config
iID = iID[len("BACKUP|"):]
periode = None
if "periode" in i:
periode = int(i["periode"])
blocked = []
if "blocked" in i:
blocked = i["blocked"].split(",")
backups.append(Backup(iID, periode=periode, blocked=blocked))
elif 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))
return backups, remotes
backups, remotes = _parse_config()
async def save_config():
# Write config
# Create config
config = configparser.ConfigParser()
# Add backups
for i in backups:
if not isinstance(i, Backup):
raise ValueError("backups contains a non backup config entry.")
@ -86,12 +91,19 @@ async def save_config():
if i.periode is not None:
backup_data["periode"] = str(i.periode)
config["BACKUP|%s" % i.name] = backup_data
# Add remotes
for i in remotes.values():
if not isinstance(i, Remote):
raise ValueError("remotes contains a non remote config entry.")
config["REMOTE|%s" % i.name] = i.dump_config()
# Write data
tmp = io.StringIO()
config.write(tmp)
to_write = tmp.getvalue()
async with aiofile.AIOFile(CONFIG_FILE, "w") as f:
await f.write(tmp.read())
await f.write(to_write)
await f.fsync()
@ -120,7 +132,7 @@ class Timer():
next_time = int(time.time())
candiates = {}
candidate_name = set()
async with backups_lock:
async with config_lock:
for i in backups:
if self.__latest < i.get_next_scedule(self.__latest, self.__zero) <= next_time:
candiates[i.name] = i

View File

@ -0,0 +1,73 @@
from .. import utils
class Remote():
rtype:str
name:str
target:str
def __init__(self, rtype:str, name:str, target:str=None):
# Check args
if rtype not in ("borgbackup",):
raise ValueError("rtype have to be borgbackup.")
utils.valid_name_check(name)
self.rtype = rtype
self.name = name
# Check borg config
if rtype == "borgbackup":
if not isinstance(target, str):
raise TypeError("target have to be a string.")
self.target = target
def dump_config(self):
if self.rtype == "borgbackup":
return {"type": "borgbackup", "target":self.target}
else:
raise NotImplementedError("Unknown backup type %s to dump." % self.rtype)
@staticmethod
def load_remote(name:str, config):
config = dict(config.items())
if config["type"] == "borgbackup":
# Borg backup
return Remote(rtype="borgbackup", name=name, target=config["target"])
else:
raise ValueError("Unknown backup type %s." % repr(config["type"]))
# RPC implementations
async def add_remote(data:dict):
# Load config
from . import config
# Load data
name = data["name"]
del data["name"]
rtype = data["type"]
del data["type"]
info = data["info"]
if not isinstance(info, dict):
raise TypeError("info isn't a object.")
info["type"] = rtype
del data["info"]
for i in data.keys():
raise ValueError("%s is an unknown option." % repr(i))
rem = Remote.load_remote(name, info)
# Set data
async with config.config_lock:
# Check
if name in config.remotes:
raise ValueError("'%s' remote already exists." % name)
# Set remote
config.remotes[name] = rem
await config.save_config()
# Return success
return {"status": "success"}

View File

@ -1,6 +1,6 @@
import asyncio
import os
from . import config
from . import config, remotes
from .. import defaults, utils
@ -33,9 +33,13 @@ def gen_callback_func(master:BackupManager):
if "operation" not in data:
raise ValueError("'operation' isn't set.")
operation = data["operation"]
del data["operation"]
# Run operation
raise NotImplementedError()
if operation == "remote-add":
return await remotes.add_remote(data)
else:
raise NotImplementedError("%s isn't a supported operation." % repr(operation))
return callback_func

View File

@ -87,5 +87,12 @@ class Connection():
await self.__write.drain()
# Recive date and return
size = _format_length.unpack(await self.__read.readexactly(_format_length.size))
return json.loads((await self.__read.readexactly(size)).decode("UTF-8"))
(size,) = _format_length.unpack(await self.__read.readexactly(_format_length.size))
return json.loads((await self.__read.readexactly(size)).decode("UTF-8"))
def valid_name_check(name:str):
if not isinstance(name, str):
raise TypeError("name has to be a string.")
for i in filter(lambda x: not("a" <= x <= "z" or "A" <= x <= "Z" or "0" <= x <= "9" or x in ("_", "-")), name):
raise ValueError("names can't contain %s." % repr(i))