Base sys service.
parent
ba0adb8847
commit
c328171527
|
@ -128,4 +128,4 @@ dmypy.json
|
|||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
/venv
|
|
@ -1,2 +1,8 @@
|
|||
# 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