Base sys service.

master
Marko Semet 2020-04-16 00:00:37 +02:00
parent ba0adb8847
commit c328171527
10 changed files with 236 additions and 1 deletions

2
.gitignore vendored
View File

@ -128,4 +128,4 @@ dmypy.json
# Pyre type checker
.pyre/
/venv

View File

@ -1,2 +1,8 @@
# home-backup
# Todos
* Protocol documentation
* JSON base protocol
* Callbacks and result data structure
* Change system service socket path

View File

@ -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:])

View File

@ -0,0 +1 @@
DEFAULT_PATH = "/run/home_backup.socket"

View File

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

View File

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

View File

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

View File

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

17
setup.py 100644
View File

@ -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"]
)

14
venv.sh 100755
View File

@ -0,0 +1,14 @@
#! /usr/bin/bash
cd "$(dirname "$0")"
python3 -m venv venv &&
source venv/bin/activate &&
pip install --upgrade pip &&
pip install . &&
if [ "$#" -gt 1 ]
then
exec -- "$@"
else
echo Using shell "$SHELL"
exec "$SHELL"
fi