Base sys service.
parent
ba0adb8847
commit
c328171527
|
@ -128,4 +128,4 @@ dmypy.json
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
/venv
|
|
@ -1,2 +1,8 @@
|
||||||
# home-backup
|
# home-backup
|
||||||
|
|
||||||
|
# Todos
|
||||||
|
|
||||||
|
* Protocol documentation
|
||||||
|
* JSON base protocol
|
||||||
|
* Callbacks and result data structure
|
||||||
|
* Change system service socket path
|
|
@ -0,0 +1,31 @@
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from .sys_service import rpc
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
# Parser
|
||||||
|
parser = argparse.ArgumentParser("home-backup", description="Manage backup tools")
|
||||||
|
parser.add_argument("--sys-server", dest="sys_server", action="store_const", const=True, default=False, help="Run system service (root required).")
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
if not args:
|
||||||
|
args = ["--help"]
|
||||||
|
result = parser.parse_args(args)
|
||||||
|
|
||||||
|
# Deamon mode
|
||||||
|
if not isinstance(result.sys_server, bool):
|
||||||
|
raise RuntimeError("Arg parser has the wrong type.")
|
||||||
|
if result.sys_server:
|
||||||
|
# Check if root
|
||||||
|
if os.getuid() != 0:
|
||||||
|
raise RuntimeError("System service has to run as root.")
|
||||||
|
|
||||||
|
# Run deamon
|
||||||
|
asyncio.run(rpc.run_deamon()) # TODO: Change default path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
main(sys.argv[1:])
|
|
@ -0,0 +1 @@
|
||||||
|
DEFAULT_PATH = "/run/home_backup.socket"
|
|
@ -0,0 +1,53 @@
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from . import mounts
|
||||||
|
|
||||||
|
|
||||||
|
async def list_subvolumes_of(path:str):
|
||||||
|
# Run process
|
||||||
|
if isinstance(path, str):
|
||||||
|
raise ValueError("path isn't a string.")
|
||||||
|
proc = await asyncio.create_subprocess_exec([b"btrfs", b"subvolume", b"list", b"-o", path.encode()], stdout=asyncio.subprocess.PIPE)
|
||||||
|
proc_data = await proc.communicate()
|
||||||
|
if not isinstance(proc_data, tuple):
|
||||||
|
raise RuntimeError("Type doesn't match.")
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError("btrfs coudn't list subevolumes.")
|
||||||
|
|
||||||
|
# Generate list
|
||||||
|
result = []
|
||||||
|
for i in proc_data[0].splitlines():
|
||||||
|
if i: # Remove empty line
|
||||||
|
tmp = i.split(" ")[7:]
|
||||||
|
if tmp[0] != "path":
|
||||||
|
raise RuntimeError("Btrfs progs doesn't output as expected.")
|
||||||
|
result.append(" ".join(tmp[1:]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def list_path_subvolumes(mount:mounts.Mountpoint, path:str):
|
||||||
|
# Check types
|
||||||
|
if not isinstance(mount, mounts.Mountpoint):
|
||||||
|
raise TypeError("mount has to be a Mountpoint.")
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise TypeError("path has to be a string.")
|
||||||
|
if mount.target == "btrfs":
|
||||||
|
raise ValueError("mount type isn't a btrfs file system.")
|
||||||
|
if not isinstance(mount.subvolume, str):
|
||||||
|
raise ValueError("mount doesn't have a subvolume.")
|
||||||
|
|
||||||
|
# Generate list
|
||||||
|
raw_list = await list_subvolumes_of(path)
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
if not path.startswith(mount.target):
|
||||||
|
raise ValueError("path isn't inside of the mounting point.")
|
||||||
|
own_path = "/".join(filter(bool, ("%s/%s" % (mount.subvolume, path[len(mount.target):])).split("/")))
|
||||||
|
result = []
|
||||||
|
for i in map(lambda x: "/".join(filter(bool, x.split("/"))), raw_list):
|
||||||
|
if not i.startswith(own_path):
|
||||||
|
raise ValueError("%s isn't a subpath of a the main path %s." % (repr(i), repr(own_path)))
|
||||||
|
tmp = i[len(own_path):]
|
||||||
|
if tmp and tmp[0] == "/":
|
||||||
|
tmp = tmp[1:]
|
||||||
|
result.append(tmp)
|
||||||
|
return result
|
|
@ -0,0 +1,46 @@
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class Mountpoint():
|
||||||
|
fstype:str
|
||||||
|
target:str
|
||||||
|
subvolume:str
|
||||||
|
|
||||||
|
def __init__(self, fstype:str, target:str, subvolume:str):
|
||||||
|
# Check args
|
||||||
|
if not isinstance(fstype, str):
|
||||||
|
raise ValueError("fstype isn't a strng.")
|
||||||
|
if not isinstance(target, str):
|
||||||
|
raise ValueError("target isn't a strng.")
|
||||||
|
if subvolume is not None and not isinstance(subvolume, str):
|
||||||
|
raise ValueError("subvolume isn't a strng.")
|
||||||
|
|
||||||
|
# Set values
|
||||||
|
self.fstype = fstype
|
||||||
|
self.target = target
|
||||||
|
self.subvolume = subvolume
|
||||||
|
|
||||||
|
|
||||||
|
async def list_mounts():
|
||||||
|
# List mounts
|
||||||
|
proc_call = await asyncio.create_subprocess_exec(b"findmnt", b"-lJ", stdout=asyncio.subprocess.PIPE)
|
||||||
|
proc_data = await proc_call.communicate()
|
||||||
|
if not isinstance(proc_data, tuple):
|
||||||
|
raise RuntimeError("Type doesn't match.")
|
||||||
|
if proc_call.returncode != 0:
|
||||||
|
raise RuntimeError("Can't find mounts.")
|
||||||
|
data = json.loads(proc_data[0])
|
||||||
|
|
||||||
|
# Parse mounts
|
||||||
|
result = []
|
||||||
|
for mount in data["filesystem"]:
|
||||||
|
subvolume = None
|
||||||
|
if mount["fstype"] == "btrfs":
|
||||||
|
search_term = "subvol="
|
||||||
|
tmp = list(filter(lambda x: x.startswith(search_term), mount["options"].split(",")))
|
||||||
|
if len(tmp) != 1:
|
||||||
|
raise ValueError("Can't find subvolume of mount %s." % repr(mount["target"]))
|
||||||
|
subvolume = tmp[0][len(search_term):]
|
||||||
|
result.append(Mountpoint(mount["target"], mount["fstype"], subvolume))
|
||||||
|
return result
|
|
@ -0,0 +1,19 @@
|
||||||
|
from . import btrfs, mounts, utils
|
||||||
|
from .. import defaults
|
||||||
|
|
||||||
|
|
||||||
|
@utils.rpc_callback
|
||||||
|
async def callback_func(data, uid):
|
||||||
|
# Get operation
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("data have to be a object.")
|
||||||
|
if "operation" not in data:
|
||||||
|
raise ValueError("'operation' isn't set.")
|
||||||
|
operation = data["operation"]
|
||||||
|
|
||||||
|
# Run operation
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_deamon(path:str=defaults.DEFAULT_PATH):
|
||||||
|
await utils.run_access_socket(path, callback_func)
|
|
@ -0,0 +1,48 @@
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_home(name:str):
|
||||||
|
# Check args
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise TypeError("name have to be a string.")
|
||||||
|
|
||||||
|
# Find home
|
||||||
|
proc = await asyncio.create_subprocess_exec(b"getent", b"passwd", name.encode(), stdout=asyncio.subprocess.PIPE)
|
||||||
|
proc_data = proc.communicate()
|
||||||
|
if not isinstance(proc_data, tuple):
|
||||||
|
raise RuntimeError("Internal value isn't the expected type.")
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError("getent didn't work.")
|
||||||
|
return proc_data[0].split(":")[6]
|
||||||
|
|
||||||
|
|
||||||
|
async def run_access_socket(path:str, async_callback):
|
||||||
|
async def run_func(read, write):
|
||||||
|
# Get user id
|
||||||
|
socket = write.get_extra_info("socket")
|
||||||
|
tmp = socket.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i'))
|
||||||
|
tmp = struct.unpack('3i', tmp)[1]
|
||||||
|
uid = str(tmp)
|
||||||
|
|
||||||
|
# Run callback
|
||||||
|
await async_callback(read, write, uid)
|
||||||
|
await (await asyncio.start_unix_server(run_func, path=path)).serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
_format_length = struct.Struct(">I")
|
||||||
|
def rpc_callback(async_func):
|
||||||
|
@functools.wraps(async_func)
|
||||||
|
async def wrap_func(read, write, uid):
|
||||||
|
while not read.at_eof():
|
||||||
|
# Read data
|
||||||
|
size = _format_length.unpack(await read.readexactly(_format_length.size))
|
||||||
|
data = json.loads((await read.readexactly(size)).decode("UTF-8"))
|
||||||
|
|
||||||
|
# Callback and return result
|
||||||
|
result = json.dumps(await async_func(data, uid)).encode("UTF-8")
|
||||||
|
write.write(_format_length.pack(len(result)))
|
||||||
|
write.write(result)
|
||||||
|
return wrap_func
|
|
@ -0,0 +1,17 @@
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
# Config
|
||||||
|
VERSION = "0.1"
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
setup(name="home-backup",
|
||||||
|
version=VERSION,
|
||||||
|
license="AGPLv3+",
|
||||||
|
description="Automation to backup home filsystem (using btrfs features).",
|
||||||
|
author="Marko Semet",
|
||||||
|
author_email="marko@marko10-000.de",
|
||||||
|
#url="", TODO: Make a project site
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=["systemd==0.16.1"]
|
||||||
|
)
|
Loading…
Reference in New Issue