mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git
synced 2024-11-28 06:51:28 +00:00
Test subscription
This commit is contained in:
parent
206589d5ad
commit
52a58d94e7
|
@ -18,7 +18,8 @@ from selfprivacy_api.resources.system import api_system
|
||||||
from selfprivacy_api.resources.services import services as api_services
|
from selfprivacy_api.resources.services import services as api_services
|
||||||
from selfprivacy_api.resources.api_auth import auth as api_auth
|
from selfprivacy_api.resources.api_auth import auth as api_auth
|
||||||
|
|
||||||
from selfprivacy_api.restic_controller.tasks import huey, init_restic
|
from selfprivacy_api.utils.huey import huey
|
||||||
|
from selfprivacy_api.restic_controller.tasks import init_restic
|
||||||
|
|
||||||
from selfprivacy_api.migrations import run_migrations
|
from selfprivacy_api.migrations import run_migrations
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import strawberry
|
import strawberry
|
||||||
from selfprivacy_api.graphql import IsAuthenticated
|
from selfprivacy_api.graphql import IsAuthenticated
|
||||||
from selfprivacy_api.graphql.mutations.api_mutations import ApiMutations
|
from selfprivacy_api.graphql.mutations.api_mutations import ApiMutations
|
||||||
|
from selfprivacy_api.graphql.mutations.mutation_interface import GenericMutationReturn
|
||||||
from selfprivacy_api.graphql.mutations.ssh_mutations import SshMutations
|
from selfprivacy_api.graphql.mutations.ssh_mutations import SshMutations
|
||||||
from selfprivacy_api.graphql.mutations.storage_mutation import StorageMutations
|
from selfprivacy_api.graphql.mutations.storage_mutation import StorageMutations
|
||||||
from selfprivacy_api.graphql.mutations.system_mutations import SystemMutations
|
from selfprivacy_api.graphql.mutations.system_mutations import SystemMutations
|
||||||
|
@ -14,6 +15,7 @@ from selfprivacy_api.graphql.queries.system import System
|
||||||
|
|
||||||
from selfprivacy_api.graphql.mutations.users_mutations import UserMutations
|
from selfprivacy_api.graphql.mutations.users_mutations import UserMutations
|
||||||
from selfprivacy_api.graphql.queries.users import Users
|
from selfprivacy_api.graphql.queries.users import Users
|
||||||
|
from selfprivacy_api.jobs.test import test_job
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
|
@ -51,6 +53,16 @@ class Mutation(
|
||||||
):
|
):
|
||||||
"""Root schema for mutations"""
|
"""Root schema for mutations"""
|
||||||
|
|
||||||
|
@strawberry.mutation
|
||||||
|
def test_mutation(self) -> GenericMutationReturn:
|
||||||
|
"""Test mutation"""
|
||||||
|
test_job()
|
||||||
|
return GenericMutationReturn(
|
||||||
|
success=True,
|
||||||
|
message="Test mutation",
|
||||||
|
code=200,
|
||||||
|
)
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
0
selfprivacy_api/graphql/subscriptions/__init__.py
Normal file
0
selfprivacy_api/graphql/subscriptions/__init__.py
Normal file
26
selfprivacy_api/graphql/subscriptions/jobs.py
Normal file
26
selfprivacy_api/graphql/subscriptions/jobs.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import asyncio
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import strawberry
|
||||||
|
from selfprivacy_api.graphql import IsAuthenticated
|
||||||
|
|
||||||
|
from selfprivacy_api.jobs import Job, Jobs
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class JobSubscription:
|
||||||
|
@strawberry.subscription(permission_classes=[IsAuthenticated])
|
||||||
|
async def job_subscription(self) -> AsyncGenerator[typing.List[Job], None]:
|
||||||
|
is_updated = True
|
||||||
|
def callback(jobs: typing.List[Job]):
|
||||||
|
nonlocal is_updated
|
||||||
|
is_updated = True
|
||||||
|
Jobs().add_observer(callback)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if is_updated:
|
||||||
|
is_updated = False
|
||||||
|
yield Jobs().jobs
|
||||||
|
except GeneratorExit:
|
||||||
|
Jobs().remove_observer(callback)
|
||||||
|
return
|
|
@ -16,6 +16,7 @@ A job is a dictionary with the following keys:
|
||||||
"""
|
"""
|
||||||
import typing
|
import typing
|
||||||
import datetime
|
import datetime
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
@ -44,6 +45,8 @@ class Job:
|
||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
status: JobStatus,
|
status: JobStatus,
|
||||||
|
status_text: typing.Optional[str],
|
||||||
|
progress: typing.Optional[int],
|
||||||
created_at: datetime.datetime,
|
created_at: datetime.datetime,
|
||||||
updated_at: datetime.datetime,
|
updated_at: datetime.datetime,
|
||||||
finished_at: typing.Optional[datetime.datetime],
|
finished_at: typing.Optional[datetime.datetime],
|
||||||
|
@ -54,45 +57,25 @@ class Job:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.status = status
|
self.status = status
|
||||||
|
self.status_text = status_text or ""
|
||||||
|
self.progress = progress or 0
|
||||||
self.created_at = created_at
|
self.created_at = created_at
|
||||||
self.updated_at = updated_at
|
self.updated_at = updated_at
|
||||||
self.finished_at = finished_at
|
self.finished_at = finished_at
|
||||||
self.error = error
|
self.error = error
|
||||||
self.result = result
|
self.result = result
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
"""
|
|
||||||
Convert the job to a dictionary.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"id": self.id,
|
|
||||||
"name": self.name,
|
|
||||||
"description": self.description,
|
|
||||||
"status": self.status,
|
|
||||||
"created_at": self.created_at,
|
|
||||||
"updated_at": self.updated_at,
|
|
||||||
"finished_at": self.finished_at,
|
|
||||||
"error": self.error,
|
|
||||||
"result": self.result,
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_json(self) -> str:
|
|
||||||
"""
|
|
||||||
Convert the job to a JSON string.
|
|
||||||
"""
|
|
||||||
return json.dumps(self.to_dict())
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""
|
"""
|
||||||
Convert the job to a string.
|
Convert the job to a string.
|
||||||
"""
|
"""
|
||||||
return self.to_json()
|
return f"{self.name} - {self.status}"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""
|
"""
|
||||||
Convert the job to a string.
|
Convert the job to a string.
|
||||||
"""
|
"""
|
||||||
return self.to_json()
|
return f"{self.name} - {self.status}"
|
||||||
|
|
||||||
|
|
||||||
class Jobs:
|
class Jobs:
|
||||||
|
@ -120,9 +103,30 @@ class Jobs:
|
||||||
else:
|
else:
|
||||||
Jobs.__instance = self
|
Jobs.__instance = self
|
||||||
self.jobs = []
|
self.jobs = []
|
||||||
|
# Observers of the jobs list.
|
||||||
|
self.observers = []
|
||||||
|
|
||||||
|
def add_observer(self, observer: typing.Callable[[typing.List[Job]], None]) -> None:
|
||||||
|
"""
|
||||||
|
Add an observer to the jobs list.
|
||||||
|
"""
|
||||||
|
self.observers.append(observer)
|
||||||
|
|
||||||
|
def remove_observer(self, observer: typing.Callable[[typing.List[Job]], None]) -> None:
|
||||||
|
"""
|
||||||
|
Remove an observer from the jobs list.
|
||||||
|
"""
|
||||||
|
self.observers.remove(observer)
|
||||||
|
|
||||||
|
def _notify_observers(self) -> None:
|
||||||
|
"""
|
||||||
|
Notify the observers of the jobs list.
|
||||||
|
"""
|
||||||
|
for observer in self.observers:
|
||||||
|
observer(self.jobs)
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
self, name: str, description: str, status: JobStatus = JobStatus.CREATED
|
self, name: str, description: str, status: JobStatus = JobStatus.CREATED, status_text: str = "", progress: int = 0
|
||||||
) -> Job:
|
) -> Job:
|
||||||
"""
|
"""
|
||||||
Add a job to the jobs list.
|
Add a job to the jobs list.
|
||||||
|
@ -131,6 +135,8 @@ class Jobs:
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
status=status,
|
status=status,
|
||||||
|
status_text=status_text,
|
||||||
|
progress=progress,
|
||||||
created_at=datetime.datetime.now(),
|
created_at=datetime.datetime.now(),
|
||||||
updated_at=datetime.datetime.now(),
|
updated_at=datetime.datetime.now(),
|
||||||
finished_at=None,
|
finished_at=None,
|
||||||
|
@ -138,6 +144,9 @@ class Jobs:
|
||||||
result=None,
|
result=None,
|
||||||
)
|
)
|
||||||
self.jobs.append(job)
|
self.jobs.append(job)
|
||||||
|
# Notify the observers.
|
||||||
|
self._notify_observers()
|
||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def remove(self, job: Job) -> None:
|
def remove(self, job: Job) -> None:
|
||||||
|
@ -145,15 +154,19 @@ class Jobs:
|
||||||
Remove a job from the jobs list.
|
Remove a job from the jobs list.
|
||||||
"""
|
"""
|
||||||
self.jobs.remove(job)
|
self.jobs.remove(job)
|
||||||
|
# Notify the observers.
|
||||||
|
self._notify_observers()
|
||||||
|
|
||||||
def update(
|
def update(
|
||||||
self,
|
self,
|
||||||
job: Job,
|
job: Job,
|
||||||
name: typing.Optional[str],
|
|
||||||
description: typing.Optional[str],
|
|
||||||
status: JobStatus,
|
status: JobStatus,
|
||||||
error: typing.Optional[str],
|
status_text: typing.Optional[str] = None,
|
||||||
result: typing.Optional[str],
|
progress: typing.Optional[int] = None,
|
||||||
|
name: typing.Optional[str] = None,
|
||||||
|
description: typing.Optional[str] = None,
|
||||||
|
error: typing.Optional[str] = None,
|
||||||
|
result: typing.Optional[str] = None,
|
||||||
) -> Job:
|
) -> Job:
|
||||||
"""
|
"""
|
||||||
Update a job in the jobs list.
|
Update a job in the jobs list.
|
||||||
|
@ -162,10 +175,20 @@ class Jobs:
|
||||||
job.name = name
|
job.name = name
|
||||||
if description is not None:
|
if description is not None:
|
||||||
job.description = description
|
job.description = description
|
||||||
|
if status_text is not None:
|
||||||
|
job.status_text = status_text
|
||||||
|
if progress is not None:
|
||||||
|
job.progress = progress
|
||||||
job.status = status
|
job.status = status
|
||||||
job.updated_at = datetime.datetime.now()
|
job.updated_at = datetime.datetime.now()
|
||||||
job.error = error
|
job.error = error
|
||||||
job.result = result
|
job.result = result
|
||||||
|
if status == JobStatus.FINISHED or status == JobStatus.ERROR:
|
||||||
|
job.finished_at = datetime.datetime.now()
|
||||||
|
|
||||||
|
# Notify the observers.
|
||||||
|
self._notify_observers()
|
||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def get_job(self, id: str) -> typing.Optional[Job]:
|
def get_job(self, id: str) -> typing.Optional[Job]:
|
||||||
|
@ -177,7 +200,7 @@ class Jobs:
|
||||||
return job
|
return job
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_jobs(self) -> list:
|
def get_jobs(self) -> typing.List[Job]:
|
||||||
"""
|
"""
|
||||||
Get the jobs list.
|
Get the jobs list.
|
||||||
"""
|
"""
|
||||||
|
|
56
selfprivacy_api/jobs/test.py
Normal file
56
selfprivacy_api/jobs/test.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import time
|
||||||
|
from selfprivacy_api.utils.huey import huey
|
||||||
|
from selfprivacy_api.jobs import JobStatus, Jobs
|
||||||
|
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def test_job():
|
||||||
|
job = Jobs().add(
|
||||||
|
name="Test job",
|
||||||
|
description="This is a test job.",
|
||||||
|
status=JobStatus.CREATED,
|
||||||
|
status_text="",
|
||||||
|
progress=0,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
status_text="Performing pre-move checks...",
|
||||||
|
progress=5,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
status_text="Performing pre-move checks...",
|
||||||
|
progress=10,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
status_text="Performing pre-move checks...",
|
||||||
|
progress=15,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
status_text="Performing pre-move checks...",
|
||||||
|
progress=20,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
status_text="Performing pre-move checks...",
|
||||||
|
progress=25,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.FINISHED,
|
||||||
|
status_text="Job finished.",
|
||||||
|
progress=100,
|
||||||
|
)
|
|
@ -1,10 +1,8 @@
|
||||||
"""Tasks for the restic controller."""
|
"""Tasks for the restic controller."""
|
||||||
from huey import crontab
|
from huey import crontab
|
||||||
from huey.contrib.mini import MiniHuey
|
from selfprivacy_api.utils.huey import huey
|
||||||
from . import ResticController, ResticStates
|
from . import ResticController, ResticStates
|
||||||
|
|
||||||
huey = MiniHuey()
|
|
||||||
|
|
||||||
|
|
||||||
@huey.task()
|
@huey.task()
|
||||||
def init_restic():
|
def init_restic():
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
"""Class representing Nextcloud service."""
|
"""Class representing Nextcloud service."""
|
||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
import psutil
|
import psutil
|
||||||
from selfprivacy_api.services.service import Service, ServiceStatus
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
from selfprivacy_api.jobs import Job, JobStatus, Jobs
|
||||||
|
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
|
||||||
from selfprivacy_api.utils import ReadUserData, WriteUserData
|
from selfprivacy_api.utils import ReadUserData, WriteUserData
|
||||||
|
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||||
|
from selfprivacy_api.utils.huey import huey
|
||||||
|
|
||||||
class Nextcloud(Service):
|
class Nextcloud(Service):
|
||||||
"""Class representing Nextcloud service."""
|
"""Class representing Nextcloud service."""
|
||||||
|
@ -92,5 +98,206 @@ class Nextcloud(Service):
|
||||||
"""Return Nextcloud logs."""
|
"""Return Nextcloud logs."""
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_storage_usage(self):
|
def get_storage_usage(self) -> int:
|
||||||
return psutil.disk_usage("/var/lib/nextcloud").used
|
"""
|
||||||
|
Calculate the real storage usage of /var/lib/nextcloud and all subdirectories.
|
||||||
|
Calculate using pathlib.
|
||||||
|
Do not follow symlinks.
|
||||||
|
"""
|
||||||
|
storage_usage = 0
|
||||||
|
for path in pathlib.Path("/var/lib/nextcloud").rglob("**/*"):
|
||||||
|
if path.is_dir():
|
||||||
|
continue
|
||||||
|
storage_usage += path.stat().st_size
|
||||||
|
return storage_usage
|
||||||
|
|
||||||
|
def get_location(self) -> str:
|
||||||
|
"""Get the name of disk where Nextcloud is installed."""
|
||||||
|
with ReadUserData() as user_data:
|
||||||
|
if user_data.get("useBinds", False):
|
||||||
|
return user_data.get("nextcloud", {}).get("location", "sda1")
|
||||||
|
else:
|
||||||
|
return "sda1"
|
||||||
|
|
||||||
|
def get_dns_records(self) -> typing.List[ServiceDnsRecord]:
|
||||||
|
return super().get_dns_records()
|
||||||
|
|
||||||
|
def move_to_volume(self, volume: BlockDevice):
|
||||||
|
job = Jobs().add(
|
||||||
|
name="services.nextcloud.move",
|
||||||
|
description=f"Moving Nextcloud to volume {volume.name}",
|
||||||
|
)
|
||||||
|
move_nextcloud(self, volume, job)
|
||||||
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def move_nextcloud(nextcloud: Nextcloud, volume: BlockDevice, job: Job):
|
||||||
|
"""Move Nextcloud to another volume."""
|
||||||
|
job = Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status_text="Performing pre-move checks...",
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
)
|
||||||
|
with ReadUserData() as user_data:
|
||||||
|
if not user_data.get("useBinds", False):
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Server is not using binds.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Check if we are on the same volume
|
||||||
|
old_location = nextcloud.get_location()
|
||||||
|
if old_location == volume.name:
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Nextcloud is already on this volume.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Check if there is enough space on the new volume
|
||||||
|
if volume.fsavail < nextcloud.get_storage_usage():
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Not enough space on the new volume.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Make sure the volume is mounted
|
||||||
|
if f"/volumes/{volume.name}" not in volume.mountpoints:
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Volume is not mounted.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Make sure current actual directory exists
|
||||||
|
if not pathlib.Path(f"/volumes/{old_location}/nextcloud").exists():
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Nextcloud is not found.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stop Nextcloud
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
status_text="Stopping Nextcloud...",
|
||||||
|
progress=5,
|
||||||
|
)
|
||||||
|
nextcloud.stop()
|
||||||
|
# Wait for Nextcloud to stop, check every second
|
||||||
|
# If it does not stop in 30 seconds, abort
|
||||||
|
for _ in range(30):
|
||||||
|
if nextcloud.get_status() != ServiceStatus.RUNNING:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Nextcloud did not stop in 30 seconds.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Unmount old volume
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status_text="Unmounting old folder...",
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
progress=10,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(["umount", "/var/lib/nextcloud"], check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Unable to unmount old volume.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Move data to new volume and set correct permissions
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status_text="Moving data to new volume...",
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
progress=20,
|
||||||
|
)
|
||||||
|
shutil.move(
|
||||||
|
f"/volumes/{old_location}/nextcloud", f"/volumes/{volume.name}/nextcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status_text="Making sure Nextcloud owns its files...",
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
progress=70,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"chown",
|
||||||
|
"-R",
|
||||||
|
"nextcloud:nextcloud",
|
||||||
|
f"/volumes/{volume.name}/nextcloud",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
print(error.output)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
error="Unable to set ownership of new volume. Nextcloud may not be able to access its files. Continuing anyway.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mount new volume
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status_text="Mounting Nextcloud data...",
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
progress=90,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"mount",
|
||||||
|
"--bind",
|
||||||
|
f"/volumes/{volume.name}/nextcloud",
|
||||||
|
"/var/lib/nextcloud",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
print(error.output)
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.ERROR,
|
||||||
|
error="Unable to mount new volume.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update userdata
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status_text="Finishing move...",
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
progress=95,
|
||||||
|
)
|
||||||
|
with WriteUserData() as user_data:
|
||||||
|
if "nextcloud" not in user_data:
|
||||||
|
user_data["nextcloud"] = {}
|
||||||
|
user_data["nextcloud"]["location"] = volume.name
|
||||||
|
# Start Nextcloud
|
||||||
|
nextcloud.start()
|
||||||
|
Jobs().update(
|
||||||
|
job=job,
|
||||||
|
status=JobStatus.FINISHED,
|
||||||
|
result="Nextcloud moved successfully.",
|
||||||
|
status_text="Starting Nextcloud...",
|
||||||
|
progress=100,
|
||||||
|
)
|
||||||
|
|
|
@ -3,6 +3,8 @@ from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||||
|
|
||||||
|
|
||||||
class ServiceStatus(Enum):
|
class ServiceStatus(Enum):
|
||||||
"""Enum for service status"""
|
"""Enum for service status"""
|
||||||
|
@ -85,9 +87,17 @@ class Service(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_storage_usage(self):
|
def get_storage_usage(self) -> int:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_dns_records(self) -> typing.List[ServiceDnsRecord]:
|
def get_dns_records(self) -> typing.List[ServiceDnsRecord]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_location(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def move_to_volume(self, volume: BlockDevice):
|
||||||
|
pass
|
||||||
|
|
|
@ -16,7 +16,7 @@ def get_block_device(device_name):
|
||||||
"-J",
|
"-J",
|
||||||
"-b",
|
"-b",
|
||||||
"-o",
|
"-o",
|
||||||
"NAME,PATH,FSAVAIL,FSSIZE,FSTYPE,FSUSED,MOUNTPOINT,LABEL,UUID,SIZE, MODEL,SERIAL,TYPE",
|
"NAME,PATH,FSAVAIL,FSSIZE,FSTYPE,FSUSED,MOUNTPOINTS,LABEL,UUID,SIZE, MODEL,SERIAL,TYPE",
|
||||||
device_name,
|
device_name,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ class BlockDevice:
|
||||||
self.fssize = block_device["fssize"]
|
self.fssize = block_device["fssize"]
|
||||||
self.fstype = block_device["fstype"]
|
self.fstype = block_device["fstype"]
|
||||||
self.fsused = block_device["fsused"]
|
self.fsused = block_device["fsused"]
|
||||||
self.mountpoint = block_device["mountpoint"]
|
self.mountpoints = block_device["mountpoints"]
|
||||||
self.label = block_device["label"]
|
self.label = block_device["label"]
|
||||||
self.uuid = block_device["uuid"]
|
self.uuid = block_device["uuid"]
|
||||||
self.size = block_device["size"]
|
self.size = block_device["size"]
|
||||||
|
@ -60,7 +60,7 @@ class BlockDevice:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<BlockDevice {self.name} of size {self.size} mounted at {self.mountpoint}>"
|
return f"<BlockDevice {self.name} of size {self.size} mounted at {self.mountpoints}>"
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.name == other.name
|
return self.name == other.name
|
||||||
|
@ -77,7 +77,7 @@ class BlockDevice:
|
||||||
self.fssize = device["fssize"]
|
self.fssize = device["fssize"]
|
||||||
self.fstype = device["fstype"]
|
self.fstype = device["fstype"]
|
||||||
self.fsused = device["fsused"]
|
self.fsused = device["fsused"]
|
||||||
self.mountpoint = device["mountpoint"]
|
self.mountpoints = device["mountpoints"]
|
||||||
self.label = device["label"]
|
self.label = device["label"]
|
||||||
self.uuid = device["uuid"]
|
self.uuid = device["uuid"]
|
||||||
self.size = device["size"]
|
self.size = device["size"]
|
||||||
|
@ -92,7 +92,7 @@ class BlockDevice:
|
||||||
"fssize": self.fssize,
|
"fssize": self.fssize,
|
||||||
"fstype": self.fstype,
|
"fstype": self.fstype,
|
||||||
"fsused": self.fsused,
|
"fsused": self.fsused,
|
||||||
"mountpoint": self.mountpoint,
|
"mountpoints": self.mountpoints,
|
||||||
"label": self.label,
|
"label": self.label,
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
"size": self.size,
|
"size": self.size,
|
||||||
|
@ -219,6 +219,6 @@ class BlockDevices:
|
||||||
"""
|
"""
|
||||||
block_devices = []
|
block_devices = []
|
||||||
for block_device in self.block_devices:
|
for block_device in self.block_devices:
|
||||||
if block_device.mountpoint == mountpoint:
|
if mountpoint in block_device.mountpoints:
|
||||||
block_devices.append(block_device)
|
block_devices.append(block_device)
|
||||||
return block_devices
|
return block_devices
|
||||||
|
|
4
selfprivacy_api/utils/huey.py
Normal file
4
selfprivacy_api/utils/huey.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""MiniHuey singleton."""
|
||||||
|
from huey.contrib.mini import MiniHuey
|
||||||
|
|
||||||
|
huey = MiniHuey()
|
103
selfprivacy_api/utils/migrate_to_binds.py
Normal file
103
selfprivacy_api/utils/migrate_to_binds.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
"""Function to perform migration of app data to binds."""
|
||||||
|
import subprocess
|
||||||
|
import psutil
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
from selfprivacy_api.services.nextcloud import Nextcloud
|
||||||
|
from selfprivacy_api.utils import WriteUserData
|
||||||
|
from selfprivacy_api.utils.block_devices import BlockDevices
|
||||||
|
|
||||||
|
class BindMigrationConfig:
|
||||||
|
"""Config for bind migration.
|
||||||
|
For each service provide block device name.
|
||||||
|
"""
|
||||||
|
email_block_device: str
|
||||||
|
bitwarden_block_device: str
|
||||||
|
gitea_block_device: str
|
||||||
|
nextcloud_block_device: str
|
||||||
|
pleroma_block_device: str
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_to_binds(config: BindMigrationConfig):
|
||||||
|
"""Migrate app data to binds."""
|
||||||
|
|
||||||
|
# Get block devices.
|
||||||
|
block_devices = BlockDevices().get_block_devices()
|
||||||
|
block_device_names = [ device.name for device in block_devices ]
|
||||||
|
|
||||||
|
# Get all unique required block devices
|
||||||
|
required_block_devices = []
|
||||||
|
for block_device_name in config.__dict__.values():
|
||||||
|
if block_device_name not in required_block_devices:
|
||||||
|
required_block_devices.append(block_device_name)
|
||||||
|
|
||||||
|
# Check if all block devices from config are present.
|
||||||
|
for block_device_name in required_block_devices:
|
||||||
|
if block_device_name not in block_device_names:
|
||||||
|
raise Exception(f"Block device {block_device_name} is not present.")
|
||||||
|
|
||||||
|
# Make sure all required block devices are mounted.
|
||||||
|
# sda1 is the root partition and is always mounted.
|
||||||
|
for block_device_name in required_block_devices:
|
||||||
|
if block_device_name == "sda1":
|
||||||
|
continue
|
||||||
|
block_device = BlockDevices().get_block_device(block_device_name)
|
||||||
|
if block_device is None:
|
||||||
|
raise Exception(f"Block device {block_device_name} is not present.")
|
||||||
|
if f"/volumes/{block_device_name}" not in block_device.mountpoints:
|
||||||
|
raise Exception(f"Block device {block_device_name} is not mounted.")
|
||||||
|
|
||||||
|
# Activate binds in userdata
|
||||||
|
with WriteUserData() as user_data:
|
||||||
|
if "email" not in user_data:
|
||||||
|
user_data["email"] = {}
|
||||||
|
user_data["email"]["block_device"] = config.email_block_device
|
||||||
|
if "bitwarden" not in user_data:
|
||||||
|
user_data["bitwarden"] = {}
|
||||||
|
user_data["bitwarden"]["block_device"] = config.bitwarden_block_device
|
||||||
|
if "gitea" not in user_data:
|
||||||
|
user_data["gitea"] = {}
|
||||||
|
user_data["gitea"]["block_device"] = config.gitea_block_device
|
||||||
|
if "nextcloud" not in user_data:
|
||||||
|
user_data["nextcloud"] = {}
|
||||||
|
user_data["nextcloud"]["block_device"] = config.nextcloud_block_device
|
||||||
|
if "pleroma" not in user_data:
|
||||||
|
user_data["pleroma"] = {}
|
||||||
|
user_data["pleroma"]["block_device"] = config.pleroma_block_device
|
||||||
|
|
||||||
|
user_data["useBinds"] = True
|
||||||
|
|
||||||
|
# Make sure /volumes/sda1 exists.
|
||||||
|
pathlib.Path("/volumes/sda1").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Perform migration of Nextcloud.
|
||||||
|
# Data is moved from /var/lib/nextcloud to /volumes/<block_device_name>/nextcloud.
|
||||||
|
# /var/lib/nextcloud is removed and /volumes/<block_device_name>/nextcloud is mounted as bind mount.
|
||||||
|
|
||||||
|
# Turn off Nextcloud
|
||||||
|
Nextcloud().stop()
|
||||||
|
|
||||||
|
# Move data from /var/lib/nextcloud to /volumes/<block_device_name>/nextcloud.
|
||||||
|
# /var/lib/nextcloud is removed and /volumes/<block_device_name>/nextcloud is mounted as bind mount.
|
||||||
|
nextcloud_data_path = pathlib.Path("/var/lib/nextcloud")
|
||||||
|
nextcloud_bind_path = pathlib.Path(f"/volumes/{config.nextcloud_block_device}/nextcloud")
|
||||||
|
if nextcloud_data_path.exists():
|
||||||
|
shutil.move(str(nextcloud_data_path), str(nextcloud_bind_path))
|
||||||
|
else:
|
||||||
|
raise Exception("Nextcloud data path does not exist.")
|
||||||
|
|
||||||
|
# Make sure folder /var/lib/nextcloud exists.
|
||||||
|
nextcloud_data_path.mkdir(mode=0o750, parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Make sure this folder is owned by user nextcloud and group nextcloud.
|
||||||
|
shutil.chown(nextcloud_bind_path, user="nextcloud", group="nextcloud")
|
||||||
|
shutil.chown(nextcloud_data_path, user="nextcloud", group="nextcloud")
|
||||||
|
|
||||||
|
# Mount nextcloud bind mount.
|
||||||
|
subprocess.run(["mount","--bind", str(nextcloud_bind_path), str(nextcloud_data_path)], check=True)
|
||||||
|
|
||||||
|
# Recursively chown all files in nextcloud bind mount.
|
||||||
|
subprocess.run(["chown", "-R", "nextcloud:nextcloud", str(nextcloud_data_path)], check=True)
|
||||||
|
|
||||||
|
# Start Nextcloud
|
||||||
|
Nextcloud().start()
|
Loading…
Reference in a new issue