from __future__ import annotations import logging import subprocess import pathlib from os.path import exists from pydantic import BaseModel from selfprivacy_api.utils.block_devices import BlockDevice, BlockDevices logger = logging.getLogger(__name__) # tests override it to a tmpdir VOLUMES_PATH = "/volumes" class BindError(Exception): pass class OwnedPath(BaseModel): """ A convenient interface for explicitly defining ownership of service folders. One overrides Service.get_owned_paths() for this. Why this exists?: One could use Bind to define ownership but then one would need to handle drive which is unnecessary and produces code duplication. It is also somewhat semantically wrong to include Owned Path into Bind instead of user and group. Because owner and group in Bind are applied to the original folder on the drive, not to the binding path. But maybe it is ok since they are technically both owned. Idk yet. """ path: str owner: str group: str class Bind: """ A directory that resides on some volume but we mount it into fs where we need it. Used for storing service data. """ def __init__(self, binding_path: str, owner: str, group: str, drive: BlockDevice): self.binding_path = binding_path self.owner = owner self.group = group self.drive = drive # TODO: delete owned path interface from Service @staticmethod def from_owned_path(path: OwnedPath, drive_name: str) -> Bind: drive = BlockDevices().get_block_device(drive_name) if drive is None: raise BindError(f"No such drive: {drive_name}") return Bind( binding_path=path.path, owner=path.owner, group=path.group, drive=drive ) def bind_foldername(self) -> str: return self.binding_path.split("/")[-1] def location_at_volume(self) -> str: return f"{VOLUMES_PATH}/{self.drive.name}/{self.bind_foldername()}" def validate(self) -> None: path = pathlib.Path(self.location_at_volume()) if not path.exists(): raise BindError(f"directory {path} is not found.") if not path.is_dir(): raise BindError(f"{path} is not a directory.") if path.owner() != self.owner: raise BindError(f"{path} is not owned by {self.owner}.") def bind(self) -> None: if not exists(self.binding_path): raise BindError(f"cannot bind to a non-existing path: {self.binding_path}") source = self.location_at_volume() target = self.binding_path try: subprocess.run( ["mount", "--bind", source, target], stderr=subprocess.PIPE, check=True, ) except subprocess.CalledProcessError as error: logging.error(error.stderr) raise BindError(f"Unable to bind {source} to {target} :{error.stderr}") def unbind(self) -> None: if not exists(self.binding_path): raise BindError(f"cannot unbind a non-existing path: {self.binding_path}") try: subprocess.run( # umount -l ? ["umount", self.binding_path], check=True, ) except subprocess.CalledProcessError: raise BindError(f"Unable to unmount folder {self.binding_path}.") pass def ensure_ownership(self) -> None: true_location = self.location_at_volume() try: subprocess.run( [ "chown", "-R", f"{self.owner}:{self.group}", # Could we just chown the binded location instead? true_location, ], check=True, stderr=subprocess.PIPE, ) except subprocess.CalledProcessError as error: logging.error(error.stderr) error_message = ( f"Unable to set ownership of {true_location} :{error.stderr}" ) raise BindError(error_message)