2024-02-26 15:01:07 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
|
|
import pathlib
|
2023-04-17 14:54:42 +00:00
|
|
|
from pydantic import BaseModel
|
2024-02-29 00:54:39 +00:00
|
|
|
from os.path import exists
|
2023-04-17 14:54:42 +00:00
|
|
|
|
2024-02-26 15:01:07 +00:00
|
|
|
from selfprivacy_api.utils.block_devices import BlockDevice, BlockDevices
|
2023-04-17 14:54:42 +00:00
|
|
|
|
2024-02-29 00:54:39 +00:00
|
|
|
# tests override it to a tmpdir
|
|
|
|
VOLUMES_PATH = "/volumes"
|
|
|
|
|
2024-02-26 15:01:07 +00:00
|
|
|
|
|
|
|
class BindError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-04-17 14:54:42 +00:00
|
|
|
class OwnedPath(BaseModel):
|
2024-03-04 17:16:08 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2023-04-17 14:54:42 +00:00
|
|
|
path: str
|
|
|
|
owner: str
|
|
|
|
group: str
|
2024-02-26 15:01:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Bind:
|
|
|
|
"""
|
2024-02-29 00:54:39 +00:00
|
|
|
A directory that resides on some volume but we mount it into fs where we need it.
|
|
|
|
Used for storing service data.
|
2024-02-26 15:01:07 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-02-29 00:54:39 +00:00
|
|
|
# TODO: delete owned path interface from Service
|
2024-02-26 15:01:07 +00:00
|
|
|
@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:
|
2024-02-29 00:54:39 +00:00
|
|
|
return f"{VOLUMES_PATH}/{self.drive.name}/{self.bind_foldername()}"
|
2024-02-26 15:01:07 +00:00
|
|
|
|
2024-02-29 00:54:39 +00:00
|
|
|
def validate(self) -> None:
|
2024-02-26 15:01:07 +00:00
|
|
|
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:
|
2024-02-29 00:54:39 +00:00
|
|
|
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
|
|
|
|
|
2024-02-26 15:01:07 +00:00
|
|
|
try:
|
|
|
|
subprocess.run(
|
2024-02-29 00:54:39 +00:00
|
|
|
["mount", "--bind", source, target],
|
|
|
|
stderr=subprocess.PIPE,
|
2024-02-26 15:01:07 +00:00
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError as error:
|
2024-02-29 00:54:39 +00:00
|
|
|
print(error.stderr)
|
|
|
|
raise BindError(f"Unable to bind {source} to {target} :{error.stderr}")
|
2024-02-26 15:01:07 +00:00
|
|
|
|
|
|
|
def unbind(self) -> None:
|
2024-02-29 00:54:39 +00:00
|
|
|
if not exists(self.binding_path):
|
|
|
|
raise BindError(f"cannot unbind a non-existing path: {self.binding_path}")
|
|
|
|
|
2024-02-26 15:01:07 +00:00
|
|
|
try:
|
|
|
|
subprocess.run(
|
2024-02-29 00:54:39 +00:00
|
|
|
# umount -l ?
|
2024-02-26 15:01:07 +00:00
|
|
|
["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,
|
2024-02-29 00:54:39 +00:00
|
|
|
stderr=subprocess.PIPE,
|
2024-02-26 15:01:07 +00:00
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError as error:
|
2024-02-29 00:54:39 +00:00
|
|
|
print(error.stderr)
|
2024-02-26 15:01:07 +00:00
|
|
|
error_message = (
|
2024-02-29 00:54:39 +00:00
|
|
|
f"Unable to set ownership of {true_location} :{error.stderr}"
|
2024-02-26 15:01:07 +00:00
|
|
|
)
|
|
|
|
raise BindError(error_message)
|