Merge branch 'restic-rewrite' of https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api into restic-rewrite
continuous-integration/drone/push Build is failing Details

pull/35/head
Houkime 2023-07-19 10:35:30 +00:00
commit a303e5ce37
11 changed files with 123 additions and 90 deletions

View File

@ -7,6 +7,9 @@ steps:
commands:
- kill $(ps aux | grep 'redis-server 127.0.0.1:6389' | awk '{print $2}') || true
- redis-server --bind 127.0.0.1 --port 6389 >/dev/null &
# We do not care about persistance on CI
- sleep 10
- redis-cli -h 127.0.0.1 -p 6389 config set stop-writes-on-bgsave-error no
- coverage run -m pytest -q
- coverage xml
- sonar-scanner -Dsonar.projectKey=SelfPrivacy-REST-API -Dsonar.sources=. -Dsonar.host.url=http://analyzer.lan:9000 -Dsonar.login="$SONARQUBE_TOKEN"

1
.gitignore vendored
View File

@ -147,3 +147,4 @@ cython_debug/
# End of https://www.toptal.com/developers/gitignore/api/flask
*.db
*.rdb

64
api.nix
View File

@ -1,64 +0,0 @@
{ lib, python39Packages }:
with python39Packages;
buildPythonApplication {
pname = "selfprivacy-api";
version = "2.0.0";
propagatedBuildInputs = [
setuptools
portalocker
pytz
pytest
pytest-mock
pytest-datadir
huey
gevent
mnemonic
pydantic
typing-extensions
psutil
fastapi
uvicorn
(buildPythonPackage rec {
pname = "strawberry-graphql";
version = "0.123.0";
format = "pyproject";
patches = [
./strawberry-graphql.patch
];
propagatedBuildInputs = [
typing-extensions
python-multipart
python-dateutil
# flask
pydantic
pygments
poetry
# flask-cors
(buildPythonPackage rec {
pname = "graphql-core";
version = "3.2.0";
format = "setuptools";
src = fetchPypi {
inherit pname version;
sha256 = "sha256-huKgvgCL/eGe94OI3opyWh2UKpGQykMcJKYIN5c4A84=";
};
checkInputs = [
pytest-asyncio
pytest-benchmark
pytestCheckHook
];
pythonImportsCheck = [
"graphql"
];
})
];
src = fetchPypi {
inherit pname version;
sha256 = "KsmZ5Xv8tUg6yBxieAEtvoKoRG60VS+iVGV0X6oCExo=";
};
})
];
src = ./.;
}

View File

@ -1,2 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.callPackage ./api.nix {}

View File

@ -1,12 +1,16 @@
from datetime import datetime, timedelta
from operator import add
from os import statvfs, path, walk
from os import statvfs
from typing import List, Optional
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.services import get_service_by_id
from selfprivacy_api.services.service import Service, ServiceStatus, StoppedService
from selfprivacy_api.services.service import (
Service,
ServiceStatus,
StoppedService,
)
from selfprivacy_api.jobs import Jobs, JobStatus, Job
@ -41,16 +45,17 @@ class NotDeadError(AssertionError):
def __str__(self):
return f"""
Service {self.service_name} should be either stopped or dead from an error before we back up.
Normally, this error is unreachable because we do try ensure this.
Apparently, not this time.
"""
Service {self.service_name} should be either stopped or dead from
an error before we back up.
Normally, this error is unreachable because we do try ensure this.
Apparently, not this time.
"""
class Backups:
"""A stateless controller class for backups"""
### Providers
# Providers
@staticmethod
def provider():
@ -172,7 +177,7 @@ class Backups:
user_data["backup"] = DEFAULT_JSON_PROVIDER
### Init
# Init
@staticmethod
def init_repo():
@ -191,7 +196,7 @@ class Backups:
return False
### Backup
# Backup
@staticmethod
def back_up(service: Service):
@ -221,7 +226,8 @@ class Backups:
Jobs.update(job, status=JobStatus.FINISHED)
return snapshot
### Restoring
# Restoring
@staticmethod
def _ensure_queued_restore_job(service, snapshot) -> Job:
job = get_restore_job(service)
@ -237,12 +243,17 @@ class Backups:
Jobs.update(job, status=JobStatus.RUNNING)
try:
Backups._restore_service_from_snapshot(service, snapshot.id, verify=False)
Backups._restore_service_from_snapshot(
service,
snapshot.id,
verify=False,
)
except Exception as e:
Backups._restore_service_from_snapshot(
service, failsafe_snapshot.id, verify=False
)
raise e
# TODO: Do we really have to forget this snapshot? — Inex
Backups.forget_snapshot(failsafe_snapshot)
@staticmethod
@ -295,8 +306,9 @@ class Backups:
else:
raise NotImplementedError(
"""
We do not know if there is enough space for restoration because there is some novel restore strategy used!
This is a developer's fault, open a issue please
We do not know if there is enough space for restoration because
there is some novel restore strategy used!
This is a developer's fault, open an issue please
"""
)
available_space = Backups.space_usable_for_service(service)
@ -307,15 +319,20 @@ class Backups:
)
@staticmethod
def _restore_service_from_snapshot(service: Service, snapshot_id: str, verify=True):
def _restore_service_from_snapshot(
service: Service,
snapshot_id: str,
verify=True,
):
folders = service.get_folders()
Backups.provider().backupper.restore_from_backup(
snapshot_id,
folders,
verify=verify,
)
### Snapshots
# Snapshots
@staticmethod
def get_snapshots(service: Service) -> List[Snapshot]:
@ -377,7 +394,7 @@ class Backups:
# expiring cache entry
Storage.cache_snapshot(snapshot)
### Autobackup
# Autobackup
@staticmethod
def is_autobackup_enabled(service: Service) -> bool:
@ -472,7 +489,7 @@ class Backups:
)
]
### Helpers
# Helpers
@staticmethod
def space_usable_for_service(service: Service) -> int:
@ -500,6 +517,9 @@ class Backups:
def assert_dead(service: Service):
# if we backup the service that is failing to restore it to the
# previous snapshot, its status can be FAILED
# And obviously restoring a failed service is the moun route
if service.get_status() not in [ServiceStatus.INACTIVE, ServiceStatus.FAILED]:
# And obviously restoring a failed service is the main route
if service.get_status() not in [
ServiceStatus.INACTIVE,
ServiceStatus.FAILED,
]:
raise NotDeadError(service)

View File

@ -30,7 +30,12 @@ class AbstractBackupper(ABC):
raise NotImplementedError
@abstractmethod
def restore_from_backup(self, snapshot_id: str, folders: List[str], verify=True):
def restore_from_backup(
self,
snapshot_id: str,
folders: List[str],
verify=True,
):
"""Restore a target folder using a snapshot"""
raise NotImplementedError

View File

@ -81,7 +81,7 @@ class ResticBackupper(AbstractBackupper):
mount_command.insert(0, "nohup")
handle = subprocess.Popen(mount_command, stdout=subprocess.DEVNULL, shell=False)
sleep(2)
if not "ids" in listdir(dir):
if "ids" not in listdir(dir):
raise IOError("failed to mount dir ", dir)
return handle
@ -211,7 +211,12 @@ class ResticBackupper(AbstractBackupper):
except ValueError as e:
raise ValueError("cannot restore a snapshot: " + output) from e
def restore_from_backup(self, snapshot_id, folders: List[str], verify=True):
def restore_from_backup(
self,
snapshot_id,
folders: List[str],
verify=True,
):
"""
Restore from backup with restic
"""
@ -236,6 +241,9 @@ class ResticBackupper(AbstractBackupper):
dst = folder
sync(src, dst)
if not verify:
self.unmount_repo(dir)
def do_restore(self, snapshot_id, target="/", verify=False):
"""barebones restic restore"""
restore_command = self.restic_command(

View File

@ -27,4 +27,4 @@ async def get_token_header(
def get_api_version() -> str:
"""Get API version"""
return "2.1.2"
return "2.1.3"

View File

@ -22,6 +22,9 @@ from selfprivacy_api.migrations.providers import CreateProviderFields
from selfprivacy_api.migrations.prepare_for_nixos_2211 import (
MigrateToSelfprivacyChannelFrom2205,
)
from selfprivacy_api.migrations.prepare_for_nixos_2305 import (
MigrateToSelfprivacyChannelFrom2211,
)
migrations = [
FixNixosConfigBranch(),
@ -31,6 +34,7 @@ migrations = [
CheckForFailedBindsMigration(),
CreateProviderFields(),
MigrateToSelfprivacyChannelFrom2205(),
MigrateToSelfprivacyChannelFrom2211(),
]

View File

@ -0,0 +1,58 @@
import os
import subprocess
from selfprivacy_api.migrations.migration import Migration
class MigrateToSelfprivacyChannelFrom2211(Migration):
"""Migrate to selfprivacy Nix channel.
For some reason NixOS 22.11 servers initialized with the nixos channel instead of selfprivacy.
This stops us from upgrading to NixOS 23.05
"""
def get_migration_name(self):
return "migrate_to_selfprivacy_channel_from_2211"
def get_migration_description(self):
return "Migrate to selfprivacy Nix channel from NixOS 22.11."
def is_migration_needed(self):
try:
output = subprocess.check_output(
["nix-channel", "--list"], start_new_session=True
)
output = output.decode("utf-8")
first_line = output.split("\n", maxsplit=1)[0]
return first_line.startswith("nixos") and (
first_line.endswith("nixos-22.11")
)
except subprocess.CalledProcessError:
return False
def migrate(self):
# Change the channel and update them.
# Also, go to /etc/nixos directory and make a git pull
current_working_directory = os.getcwd()
try:
print("Changing channel")
os.chdir("/etc/nixos")
subprocess.check_output(
[
"nix-channel",
"--add",
"https://channel.selfprivacy.org/nixos-selfpricacy",
"nixos",
]
)
subprocess.check_output(["nix-channel", "--update"])
nixos_config_branch = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"], start_new_session=True
)
if nixos_config_branch.decode("utf-8").strip() == "api-redis":
print("Also changing nixos-config branch from api-redis to master")
subprocess.check_output(["git", "checkout", "master"])
subprocess.check_output(["git", "pull"])
os.chdir(current_working_directory)
except subprocess.CalledProcessError:
os.chdir(current_working_directory)
print("Error")

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="selfprivacy_api",
version="2.1.2",
version="2.1.3",
packages=find_packages(),
scripts=[
"selfprivacy_api/app.py",