diff --git a/selfprivacy_api/actions/services.py b/selfprivacy_api/actions/services.py index ebb0917..f9486d1 100644 --- a/selfprivacy_api/actions/services.py +++ b/selfprivacy_api/actions/services.py @@ -27,7 +27,7 @@ def move_service(service_id: str, volume_name: str) -> Job: job = Jobs.add( type_id=f"services.{service.get_id()}.move", name=f"Move {service.get_display_name()}", - description=f"Moving {service.get_display_name()} data to {volume.name}", + description=f"Moving {service.get_display_name()} data to {volume.get_display_name().lower()}", ) move_service_task(service, volume, job) diff --git a/selfprivacy_api/dependencies.py b/selfprivacy_api/dependencies.py index b9d0904..69ce319 100644 --- a/selfprivacy_api/dependencies.py +++ b/selfprivacy_api/dependencies.py @@ -27,4 +27,4 @@ async def get_token_header( def get_api_version() -> str: """Get API version""" - return "3.2.1" + return "3.2.2" diff --git a/selfprivacy_api/graphql/__init__.py b/selfprivacy_api/graphql/__init__.py index 6124a1a..edd8a78 100644 --- a/selfprivacy_api/graphql/__init__.py +++ b/selfprivacy_api/graphql/__init__.py @@ -16,6 +16,10 @@ class IsAuthenticated(BasePermission): token = info.context["request"].headers.get("Authorization") if token is None: token = info.context["request"].query_params.get("token") + if token is None: + connection_params = info.context.get("connection_params") + if connection_params is not None: + token = connection_params.get("Authorization") if token is None: return False return is_token_valid(token.replace("Bearer ", "")) diff --git a/selfprivacy_api/graphql/queries/providers.py b/selfprivacy_api/graphql/queries/providers.py index b9ca7ef..2995fe8 100644 --- a/selfprivacy_api/graphql/queries/providers.py +++ b/selfprivacy_api/graphql/queries/providers.py @@ -14,6 +14,7 @@ class DnsProvider(Enum): class ServerProvider(Enum): HETZNER = "HETZNER" DIGITALOCEAN = "DIGITALOCEAN" + OTHER = "OTHER" @strawberry.enum diff --git a/selfprivacy_api/jobs/migrate_to_binds.py b/selfprivacy_api/jobs/migrate_to_binds.py index 3250c9a..782b361 100644 --- a/selfprivacy_api/jobs/migrate_to_binds.py +++ b/selfprivacy_api/jobs/migrate_to_binds.py @@ -6,7 +6,7 @@ import shutil from pydantic import BaseModel from selfprivacy_api.jobs import Job, JobStatus, Jobs from selfprivacy_api.services.bitwarden import Bitwarden -from selfprivacy_api.services.gitea import Gitea +from selfprivacy_api.services.forgejo import Forgejo from selfprivacy_api.services.mailserver import MailServer from selfprivacy_api.services.nextcloud import Nextcloud from selfprivacy_api.services.pleroma import Pleroma @@ -230,7 +230,7 @@ def migrate_to_binds(config: BindMigrationConfig, job: Job): status_text="Migrating Gitea.", ) - Gitea().stop() + Forgejo().stop() if not pathlib.Path("/volumes/sda1/gitea").exists(): if not pathlib.Path("/volumes/sdb/gitea").exists(): @@ -241,7 +241,7 @@ def migrate_to_binds(config: BindMigrationConfig, job: Job): group="gitea", ) - Gitea().start() + Forgejo().start() # Perform migration of Mail server diff --git a/selfprivacy_api/services/__init__.py b/selfprivacy_api/services/__init__.py index f9dfac2..da02eba 100644 --- a/selfprivacy_api/services/__init__.py +++ b/selfprivacy_api/services/__init__.py @@ -2,7 +2,7 @@ import typing from selfprivacy_api.services.bitwarden import Bitwarden -from selfprivacy_api.services.gitea import Gitea +from selfprivacy_api.services.forgejo import Forgejo from selfprivacy_api.services.jitsimeet import JitsiMeet from selfprivacy_api.services.mailserver import MailServer from selfprivacy_api.services.nextcloud import Nextcloud @@ -13,7 +13,7 @@ import selfprivacy_api.utils.network as network_utils services: list[Service] = [ Bitwarden(), - Gitea(), + Forgejo(), MailServer(), Nextcloud(), Pleroma(), diff --git a/selfprivacy_api/services/flake_service_manager.py b/selfprivacy_api/services/flake_service_manager.py new file mode 100644 index 0000000..63c2279 --- /dev/null +++ b/selfprivacy_api/services/flake_service_manager.py @@ -0,0 +1,53 @@ +import re +from typing import Tuple, Optional + +FLAKE_CONFIG_PATH = "/etc/nixos/sp-modules/flake.nix" + + +class FlakeServiceManager: + def __enter__(self) -> "FlakeServiceManager": + self.services = {} + + with open(FLAKE_CONFIG_PATH, "r") as file: + for line in file: + service_name, url = self._extract_services(input_string=line) + if service_name and url: + self.services[service_name] = url + + return self + + def _extract_services( + self, input_string: str + ) -> Tuple[Optional[str], Optional[str]]: + pattern = r"inputs\.([\w-]+)\.url\s*=\s*([\S]+);" + match = re.search(pattern, input_string) + + if match: + variable_name = match.group(1) + url = match.group(2) + return variable_name, url + else: + return None, None + + def __exit__(self, exc_type, exc_value, traceback) -> None: + with open(FLAKE_CONFIG_PATH, "w") as file: + file.write( + """ +{ + description = "SelfPrivacy NixOS PoC modules/extensions/bundles/packages/etc";\n +""" + ) + + for key, value in self.services.items(): + file.write( + f""" + inputs.{key}.url = {value}; +""" + ) + + file.write( + """ + outputs = _: { }; +} +""" + ) diff --git a/selfprivacy_api/services/gitea/__init__.py b/selfprivacy_api/services/forgejo/__init__.py similarity index 72% rename from selfprivacy_api/services/gitea/__init__.py rename to selfprivacy_api/services/forgejo/__init__.py index 311d59e..d035736 100644 --- a/selfprivacy_api/services/gitea/__init__.py +++ b/selfprivacy_api/services/forgejo/__init__.py @@ -7,31 +7,34 @@ from selfprivacy_api.utils import get_domain from selfprivacy_api.utils.systemd import get_service_status from selfprivacy_api.services.service import Service, ServiceStatus -from selfprivacy_api.services.gitea.icon import GITEA_ICON +from selfprivacy_api.services.forgejo.icon import FORGEJO_ICON -class Gitea(Service): - """Class representing Gitea service""" +class Forgejo(Service): + """Class representing Forgejo service. + + Previously was Gitea, so some IDs are still called gitea for compatibility. + """ @staticmethod def get_id() -> str: - """Return service id.""" + """Return service id. For compatibility keep in gitea.""" return "gitea" @staticmethod def get_display_name() -> str: """Return service display name.""" - return "Gitea" + return "Forgejo" @staticmethod def get_description() -> str: """Return service description.""" - return "Gitea is a Git forge." + return "Forgejo is a Git forge." @staticmethod def get_svg_icon() -> str: """Read SVG icon from file and return it as base64 encoded string.""" - return base64.b64encode(GITEA_ICON.encode("utf-8")).decode("utf-8") + return base64.b64encode(FORGEJO_ICON.encode("utf-8")).decode("utf-8") @staticmethod def get_url() -> Optional[str]: @@ -65,19 +68,19 @@ class Gitea(Service): Return code 3 means service is stopped. Return code 4 means service is off. """ - return get_service_status("gitea.service") + return get_service_status("forgejo.service") @staticmethod def stop(): - subprocess.run(["systemctl", "stop", "gitea.service"]) + subprocess.run(["systemctl", "stop", "forgejo.service"]) @staticmethod def start(): - subprocess.run(["systemctl", "start", "gitea.service"]) + subprocess.run(["systemctl", "start", "forgejo.service"]) @staticmethod def restart(): - subprocess.run(["systemctl", "restart", "gitea.service"]) + subprocess.run(["systemctl", "restart", "forgejo.service"]) @staticmethod def get_configuration(): @@ -93,4 +96,5 @@ class Gitea(Service): @staticmethod def get_folders() -> List[str]: + """The data folder is still called gitea for compatibility.""" return ["/var/lib/gitea"] diff --git a/selfprivacy_api/services/gitea/gitea.svg b/selfprivacy_api/services/forgejo/gitea.svg similarity index 100% rename from selfprivacy_api/services/gitea/gitea.svg rename to selfprivacy_api/services/forgejo/gitea.svg diff --git a/selfprivacy_api/services/gitea/icon.py b/selfprivacy_api/services/forgejo/icon.py similarity index 98% rename from selfprivacy_api/services/gitea/icon.py rename to selfprivacy_api/services/forgejo/icon.py index 569f96a..5e600cf 100644 --- a/selfprivacy_api/services/gitea/icon.py +++ b/selfprivacy_api/services/forgejo/icon.py @@ -1,4 +1,4 @@ -GITEA_ICON = """ +FORGEJO_ICON = """ diff --git a/selfprivacy_api/utils/block_devices.py b/selfprivacy_api/utils/block_devices.py index 4de5b75..0db8fe0 100644 --- a/selfprivacy_api/utils/block_devices.py +++ b/selfprivacy_api/utils/block_devices.py @@ -90,6 +90,14 @@ class BlockDevice: def __hash__(self): return hash(self.name) + def get_display_name(self) -> str: + if self.is_root(): + return "System disk" + elif self.model == "Volume": + return "Expandable volume" + else: + return self.name + def is_root(self) -> bool: """ Return True if the block device is the root device. diff --git a/selfprivacy_api/utils/redis_pool.py b/selfprivacy_api/utils/redis_pool.py index 39c536f..ea827d1 100644 --- a/selfprivacy_api/utils/redis_pool.py +++ b/selfprivacy_api/utils/redis_pool.py @@ -29,8 +29,6 @@ class RedisPool: url, decode_responses=True, ) - # TODO: inefficient, this is probably done each time we connect - self.get_connection().config_set("notify-keyspace-events", "KEA") @staticmethod def connection_url(dbnumber: int) -> str: diff --git a/setup.py b/setup.py index 473ece8..23c544e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="selfprivacy_api", - version="3.2.1", + version="3.2.2", packages=find_packages(), scripts=[ "selfprivacy_api/app.py", diff --git a/tests/test_flake_services_manager.py b/tests/test_flake_services_manager.py new file mode 100644 index 0000000..4650b6d --- /dev/null +++ b/tests/test_flake_services_manager.py @@ -0,0 +1,127 @@ +import pytest + +from selfprivacy_api.services.flake_service_manager import FlakeServiceManager + +all_services_file = """ +{ + description = "SelfPrivacy NixOS PoC modules/extensions/bundles/packages/etc"; + + + inputs.bitwarden.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/bitwarden; + + inputs.gitea.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/gitea; + + inputs.jitsi-meet.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/jitsi-meet; + + inputs.nextcloud.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/nextcloud; + + inputs.ocserv.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/ocserv; + + inputs.pleroma.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/pleroma; + + inputs.simple-nixos-mailserver.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/simple-nixos-mailserver; + + outputs = _: { }; +} +""" + + +some_services_file = """ +{ + description = "SelfPrivacy NixOS PoC modules/extensions/bundles/packages/etc"; + + + inputs.bitwarden.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/bitwarden; + + inputs.gitea.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/gitea; + + inputs.jitsi-meet.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/jitsi-meet; + + outputs = _: { }; +} +""" + + +@pytest.fixture +def some_services_flake_mock(mocker, datadir): + flake_config_path = datadir / "some_services.nix" + mocker.patch( + "selfprivacy_api.services.flake_service_manager.FLAKE_CONFIG_PATH", + new=flake_config_path, + ) + return flake_config_path + + +@pytest.fixture +def no_services_flake_mock(mocker, datadir): + flake_config_path = datadir / "no_services.nix" + mocker.patch( + "selfprivacy_api.services.flake_service_manager.FLAKE_CONFIG_PATH", + new=flake_config_path, + ) + return flake_config_path + + +# --- + + +def test_read_services_list(some_services_flake_mock): + with FlakeServiceManager() as manager: + services = { + "bitwarden": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/bitwarden", + "gitea": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/gitea", + "jitsi-meet": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/jitsi-meet", + } + assert manager.services == services + + +def test_change_services_list(some_services_flake_mock): + services = { + "bitwarden": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/bitwarden", + "gitea": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/gitea", + "jitsi-meet": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/jitsi-meet", + "nextcloud": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/nextcloud", + "ocserv": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/ocserv", + "pleroma": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/pleroma", + "simple-nixos-mailserver": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/simple-nixos-mailserver", + } + + with FlakeServiceManager() as manager: + manager.services = services + + with FlakeServiceManager() as manager: + assert manager.services == services + + with open(some_services_flake_mock, "r", encoding="utf-8") as file: + file_content = file.read().strip() + + assert all_services_file.strip() == file_content + + +def test_read_empty_services_list(no_services_flake_mock): + with FlakeServiceManager() as manager: + services = {} + assert manager.services == services + + +def test_change_empty_services_list(no_services_flake_mock): + services = { + "bitwarden": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/bitwarden", + "gitea": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/gitea", + "jitsi-meet": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/jitsi-meet", + "nextcloud": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/nextcloud", + "ocserv": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/ocserv", + "pleroma": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/pleroma", + "simple-nixos-mailserver": "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/simple-nixos-mailserver", + } + + with FlakeServiceManager() as manager: + manager.services = services + + with FlakeServiceManager() as manager: + assert manager.services == services + + with open(no_services_flake_mock, "r", encoding="utf-8") as file: + file_content = file.read().strip() + + assert all_services_file.strip() == file_content diff --git a/tests/test_flake_services_manager/no_services.nix b/tests/test_flake_services_manager/no_services.nix new file mode 100644 index 0000000..5967016 --- /dev/null +++ b/tests/test_flake_services_manager/no_services.nix @@ -0,0 +1,4 @@ +{ + description = "SelfPrivacy NixOS PoC modules/extensions/bundles/packages/etc"; + outputs = _: { }; +} diff --git a/tests/test_flake_services_manager/some_services.nix b/tests/test_flake_services_manager/some_services.nix new file mode 100644 index 0000000..4bbb919 --- /dev/null +++ b/tests/test_flake_services_manager/some_services.nix @@ -0,0 +1,12 @@ +{ + description = "SelfPrivacy NixOS PoC modules/extensions/bundles/packages/etc"; + + + inputs.bitwarden.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/bitwarden; + + inputs.gitea.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/gitea; + + inputs.jitsi-meet.url = git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/jitsi-meet; + + outputs = _: { }; +} diff --git a/tests/test_services_systemctl.py b/tests/test_services_systemctl.py index 8b247e0..43805e8 100644 --- a/tests/test_services_systemctl.py +++ b/tests/test_services_systemctl.py @@ -2,7 +2,7 @@ import pytest from selfprivacy_api.services.service import ServiceStatus from selfprivacy_api.services.bitwarden import Bitwarden -from selfprivacy_api.services.gitea import Gitea +from selfprivacy_api.services.forgejo import Forgejo from selfprivacy_api.services.mailserver import MailServer from selfprivacy_api.services.nextcloud import Nextcloud from selfprivacy_api.services.ocserv import Ocserv @@ -22,7 +22,7 @@ def call_args_asserts(mocked_object): "dovecot2.service", "postfix.service", "vaultwarden.service", - "gitea.service", + "forgejo.service", "phpfpm-nextcloud.service", "ocserv.service", "pleroma.service", @@ -77,7 +77,7 @@ def mock_popen_systemctl_service_not_ok(mocker): def test_systemctl_ok(mock_popen_systemctl_service_ok): assert MailServer.get_status() == ServiceStatus.ACTIVE assert Bitwarden.get_status() == ServiceStatus.ACTIVE - assert Gitea.get_status() == ServiceStatus.ACTIVE + assert Forgejo.get_status() == ServiceStatus.ACTIVE assert Nextcloud.get_status() == ServiceStatus.ACTIVE assert Ocserv.get_status() == ServiceStatus.ACTIVE assert Pleroma.get_status() == ServiceStatus.ACTIVE @@ -87,7 +87,7 @@ def test_systemctl_ok(mock_popen_systemctl_service_ok): def test_systemctl_failed_service(mock_popen_systemctl_service_not_ok): assert MailServer.get_status() == ServiceStatus.FAILED assert Bitwarden.get_status() == ServiceStatus.FAILED - assert Gitea.get_status() == ServiceStatus.FAILED + assert Forgejo.get_status() == ServiceStatus.FAILED assert Nextcloud.get_status() == ServiceStatus.FAILED assert Ocserv.get_status() == ServiceStatus.FAILED assert Pleroma.get_status() == ServiceStatus.FAILED diff --git a/tests/test_websocket_uvicorn_standalone.py b/tests/test_websocket_uvicorn_standalone.py new file mode 100644 index 0000000..43a53ef --- /dev/null +++ b/tests/test_websocket_uvicorn_standalone.py @@ -0,0 +1,39 @@ +import pytest +from fastapi import FastAPI, WebSocket +import uvicorn + +# import subprocess +from multiprocessing import Process +import asyncio +from time import sleep +from websockets import client + +app = FastAPI() + + +@app.websocket("/") +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"You sent: {data}") + + +def run_uvicorn(): + uvicorn.run(app, port=5000) + return True + + +@pytest.mark.asyncio +async def test_uvcorn_ws_works_in_prod(): + proc = Process(target=run_uvicorn) + proc.start() + sleep(2) + + ws = await client.connect("ws://127.0.0.1:5000") + + await ws.send("hohoho") + message = await ws.read_message() + assert message == "You sent: hohoho" + await ws.close() + proc.kill()