Add some services endpoints

pull/13/head
Inex Code 2022-08-13 01:29:18 +04:00
parent e7df559787
commit 00badfbbf8
23 changed files with 314 additions and 138 deletions

View File

@ -52,5 +52,6 @@ async def startup():
run_migrations()
init_restic()
if __name__ == "__main__":
uvicorn.run("selfprivacy_api.app:app", host="0.0.0.0", port=5050, log_level="info")

View File

@ -0,0 +1,13 @@
import typing
import strawberry
@strawberry.type
class DnsRecord:
"""DNS record"""
record_type: str
name: str
content: str
ttl: int
priority: typing.Optional[int]

View File

@ -0,0 +1,24 @@
import typing
import strawberry
@strawberry.type
class StorageVolume:
"""Stats and basic info about a volume or a system disk."""
total_space: str
free_space: str
used_space: str
root: bool
name: str
model: typing.Optional[str]
serial: typing.Optional[str]
type: str
usages: list["StorageUsageInterface"]
@strawberry.interface
class StorageUsageInterface:
used_space: str
volume: typing.Optional[StorageVolume]
title: str

View File

@ -7,6 +7,7 @@ from strawberry.types import Info
from selfprivacy_api.actions.api_tokens import get_api_tokens_with_caller_flag
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.utils import parse_date
from selfprivacy_api.dependencies import get_api_version as get_api_version_dependency
from selfprivacy_api.utils.auth import (
get_recovery_token_status,
@ -17,7 +18,7 @@ from selfprivacy_api.utils.auth import (
def get_api_version() -> str:
"""Get API version"""
return "1.2.7"
return get_api_version_dependency()
@strawberry.type

View File

@ -0,0 +1,45 @@
"""Jobs status"""
# pylint: disable=too-few-public-methods
import typing
import strawberry
import datetime
from selfprivacy_api.jobs import Jobs
@strawberry.type
class ApiJob:
name: str
description: str
status: str
status_text: typing.Optional[str]
progress: typing.Optional[int]
created_at: datetime.datetime
updated_at: datetime.datetime
finished_at: typing.Optional[datetime.datetime]
error: typing.Optional[str]
result: typing.Optional[str]
@strawberry.type
class Job:
@strawberry.field
def get_jobs(self) -> typing.List[ApiJob]:
Jobs.get_instance().get_jobs()
return [
ApiJob(
name=job.name,
description=job.description,
status=job.status.name,
status_text=job.status_text,
progress=job.progress,
created_at=job.created_at,
updated_at=job.updated_at,
finished_at=job.finished_at,
error=job.error,
result=job.result,
)
for job in Jobs.get_instance().get_jobs()
]

View File

@ -0,0 +1,117 @@
"""Services status"""
# pylint: disable=too-few-public-methods
from enum import Enum
import typing
import strawberry
import datetime
from selfprivacy_api.graphql.common_types.dns import DnsRecord
from selfprivacy_api.graphql.common_types.storage_usage import (
StorageUsageInterface,
StorageVolume,
)
from selfprivacy_api.services import get_all_services, get_service_by_id
from selfprivacy_api.services import Service as ServiceInterface
from selfprivacy_api.utils.block_devices import BlockDevices
@strawberry.enum
class ServiceStatusEnum(Enum):
RUNNING = "RUNNING"
DEGRADED = "DEGRADED"
ERROR = "ERROR"
STOPPED = "STOPPED"
OFF = "OFF"
@strawberry.type
class ServiceStorageUsage(StorageUsageInterface):
"""Storage usage for a service"""
service: typing.Optional["Service"]
def get_storage_usage(root: "Service") -> ServiceStorageUsage:
"""Get storage usage for a service"""
service = get_service_by_id(root.id)
if service is None:
return ServiceStorageUsage(
service=service,
title="Not found",
used_space="0",
volume=get_volume_by_id("sda1"),
)
return ServiceStorageUsage(
service=service_to_graphql_service(service),
title=service.get_display_name(),
used_space=str(service.get_storage_usage()),
volume=get_volume_by_id(service.get_location()),
)
@strawberry.type
class Service:
storage_usage: ServiceStorageUsage = strawberry.field(resolver=get_storage_usage)
id: str
display_name: str
description: str
svg_icon: str
is_movable: bool
is_required: bool
is_enabled: bool
status: ServiceStatusEnum
url: typing.Optional[str]
dns_records: typing.Optional[typing.List[DnsRecord]]
def service_to_graphql_service(service: ServiceInterface) -> Service:
"""Convert service to graphql service"""
return Service(
id=service.get_id(),
display_name=service.get_display_name(),
description=service.get_description(),
svg_icon=service.get_svg_icon(),
is_movable=service.is_movable(),
is_required=service.is_required(),
is_enabled=service.is_enabled(),
status=ServiceStatusEnum(service.get_status().value),
url=service.get_url(),
dns_records=[
DnsRecord(
record_type=record.type,
name=record.name,
content=record.content,
ttl=record.ttl,
priority=record.priority,
)
for record in service.get_dns_records()
],
)
def get_volume_by_id(volume_id: str) -> typing.Optional[StorageVolume]:
"""Get volume by id"""
volume = BlockDevices().get_block_device(volume_id)
if volume is None:
return None
return StorageVolume(
total_space=str(volume.fssize)
if volume.fssize is not None
else str(volume.size),
free_space=str(volume.fsavail),
used_space=str(volume.fsused),
root=volume.name == "sda1",
name=volume.name,
model=volume.model,
serial=volume.serial,
type=volume.type,
usages=[],
)
@strawberry.type
class Services:
@strawberry.field
def all_services(self, info) -> typing.List[Service]:
services = get_all_services()
return [service_to_graphql_service(service) for service in services]

View File

@ -2,23 +2,10 @@
# pylint: disable=too-few-public-methods
import typing
import strawberry
from selfprivacy_api.graphql.common_types.storage_usage import StorageVolume
from selfprivacy_api.utils.block_devices import BlockDevices
@strawberry.type
class StorageVolume:
"""Stats and basic info about a volume or a system disk."""
total_space: str
free_space: str
used_space: str
root: bool
name: str
model: typing.Optional[str]
serial: typing.Optional[str]
type: str
@strawberry.type
class Storage:
"""GraphQL queries to get storage information."""
@ -38,6 +25,7 @@ class Storage:
model=volume.model,
serial=volume.serial,
type=volume.type,
usages=[],
)
for volume in BlockDevices().get_block_devices()
]

View File

@ -2,6 +2,7 @@
# pylint: disable=too-few-public-methods
import typing
import strawberry
from selfprivacy_api.graphql.common_types.dns import DnsRecord
from selfprivacy_api.graphql.queries.common import Alert, Severity
from selfprivacy_api.graphql.queries.providers import DnsProvider, ServerProvider
@ -10,17 +11,6 @@ import selfprivacy_api.actions.system as system_actions
import selfprivacy_api.actions.ssh as ssh_actions
@strawberry.type
class DnsRecord:
"""DNS record"""
recordType: str
name: str
content: str
ttl: int
priority: typing.Optional[int]
@strawberry.type
class SystemDomainInfo:
"""Information about the system domain"""

View File

@ -10,12 +10,13 @@ from selfprivacy_api.graphql.mutations.storage_mutation import StorageMutations
from selfprivacy_api.graphql.mutations.system_mutations import SystemMutations
from selfprivacy_api.graphql.queries.api_queries import Api
from selfprivacy_api.graphql.queries.jobs import Job
from selfprivacy_api.graphql.queries.services import Services
from selfprivacy_api.graphql.queries.storage import Storage
from selfprivacy_api.graphql.queries.system import System
from selfprivacy_api.graphql.mutations.users_mutations import UserMutations
from selfprivacy_api.graphql.queries.users import Users
from selfprivacy_api.graphql.subscriptions.jobs import JobSubscription
from selfprivacy_api.jobs.test import test_job
@ -43,6 +44,16 @@ class Query:
"""Storage queries"""
return Storage()
@strawberry.field(permission_classes=[IsAuthenticated])
def jobs(self) -> Job:
"""Jobs queries"""
return Job()
@strawberry.field(permission_classes=[IsAuthenticated])
def services(self) -> Services:
"""Services queries"""
return Services()
@strawberry.type
class Mutation(
@ -67,4 +78,4 @@ class Mutation(
pass
schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=JobSubscription)
schema = strawberry.Schema(query=Query, mutation=Mutation)

View File

@ -1,76 +0,0 @@
import asyncio
import datetime
from typing import AsyncGenerator
import typing
import strawberry
from selfprivacy_api.graphql import IsAuthenticated
from selfprivacy_api.jobs import Job, Jobs
@strawberry.type
class ApiJob:
name: str
description: str
status: str
status_text: typing.Optional[str]
progress: typing.Optional[int]
created_at: datetime.datetime
updated_at: datetime.datetime
finished_at: typing.Optional[datetime.datetime]
error: typing.Optional[str]
result: typing.Optional[str]
@strawberry.type
class JobSubscription:
@strawberry.subscription
async def count(self, target: int = 100) -> AsyncGenerator[int, None]:
for i in range(target):
yield i
await asyncio.sleep(0.5)
@strawberry.subscription()
async def job_subscription(self) -> AsyncGenerator[typing.List[ApiJob], None]:
is_updated = True
def callback(jobs: typing.List[Job]):
nonlocal is_updated
is_updated = True
print("Subscribing to job updates...")
Jobs.get_instance().add_observer(callback)
yield [
ApiJob(
name=job.name,
description=job.description,
status=job.status.name,
status_text=job.status_text,
progress=job.progress,
created_at=job.created_at,
updated_at=job.updated_at,
finished_at=job.finished_at,
error=job.error,
result=job.result,
)
for job in Jobs.get_instance().get_jobs()
]
while True:
if is_updated:
is_updated = False
yield [
ApiJob(
name=job.name,
description=job.description,
status=job.status.name,
status_text=job.status_text,
progress=job.progress,
created_at=job.created_at,
updated_at=job.updated_at,
finished_at=job.finished_at,
error=job.error,
result=job.result,
)
for job in Jobs.get_instance().get_jobs()
]

View File

@ -1,9 +1,7 @@
import time
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.jobs import JobStatus, Jobs
huey = Huey()
@huey.task()
def test_job():

View File

@ -1,10 +1,8 @@
"""Tasks for the restic controller."""
from huey import crontab
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.huey import huey
from . import ResticController, ResticStates
huey = Huey()
@huey.task()
def init_restic():

View File

@ -0,0 +1,39 @@
"""Services module."""
import typing
from selfprivacy_api.services.bitwarden import Bitwarden
from selfprivacy_api.services.gitea import Gitea
from selfprivacy_api.services.mailserver import MailServer
from selfprivacy_api.services.nextcloud import Nextcloud
from selfprivacy_api.services.pleroma import Pleroma
from selfprivacy_api.services.ocserv import Ocserv
from selfprivacy_api.services.service import Service
services = [
Bitwarden(),
Gitea(),
MailServer(),
Nextcloud(),
Pleroma(),
Ocserv(),
]
def get_all_services() -> typing.List[Service]:
return services
def get_service_by_id(service_id: str) -> typing.Optional[Service]:
for service in services:
if service.get_id() == service_id:
return service
return None
def get_enabled_services() -> typing.List[Service]:
return [service for service in services if service.is_enabled()]
def get_disabled_services() -> typing.List[Service]:
return [service for service in services if not service.is_enabled()]

View File

@ -8,13 +8,11 @@ from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.network import get_ip4
huey = Huey()
class Bitwarden(Service):
"""Class representing Bitwarden service."""
@ -40,6 +38,12 @@ class Bitwarden(Service):
with open("selfprivacy_api/services/bitwarden/bitwarden.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def get_url() -> typing.Optional[str]:
"""Return service url."""
domain = get_domain()
return f"https://password.{domain}"
@staticmethod
def is_movable() -> bool:
return True

View File

@ -7,12 +7,10 @@ import shutil
from pydantic import BaseModel
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
huey = Huey()
from selfprivacy_api.services.service import Service, ServiceStatus
class FolderMoveNames(BaseModel):

View File

@ -3,18 +3,16 @@ import base64
import subprocess
import typing
from selfprivacy_api.jobs import Job, JobStatus, Jobs
from selfprivacy_api.jobs import Jobs
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.network import get_ip4
huey = Huey()
class Gitea(Service):
"""Class representing Gitea service"""
@ -40,6 +38,12 @@ class Gitea(Service):
with open("selfprivacy_api/services/gitea/gitea.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def get_url() -> typing.Optional[str]:
"""Return service url."""
domain = get_domain()
return f"https://git.{domain}"
@staticmethod
def is_movable() -> bool:
return True

View File

@ -12,11 +12,9 @@ from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceS
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_dkim_key, get_domain
from selfprivacy_api.utils import huey
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.huey import Huey
from selfprivacy_api.utils.huey import huey
from selfprivacy_api.utils.network import get_ip4
huey = Huey()
class MailServer(Service):
"""Class representing mail service"""
@ -38,6 +36,11 @@ class MailServer(Service):
with open("selfprivacy_api/services/mailserver/mailserver.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def get_url() -> typing.Optional[str]:
"""Return service url."""
return None
@staticmethod
def is_movable() -> bool:
return True

View File

@ -7,7 +7,7 @@ from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.network import get_ip4
@ -16,7 +16,7 @@ class Nextcloud(Service):
"""Class representing Nextcloud service."""
@staticmethod
def get_id(self) -> str:
def get_id() -> str:
"""Return service id."""
return "nextcloud"
@ -36,6 +36,12 @@ class Nextcloud(Service):
with open("selfprivacy_api/services/nextcloud/nextcloud.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def get_url() -> typing.Optional[str]:
"""Return service url."""
domain = get_domain()
return f"https://cloud.{domain}"
@staticmethod
def is_movable() -> bool:
return True

View File

@ -32,6 +32,11 @@ class Ocserv(Service):
with open("selfprivacy_api/services/ocserv/ocserv.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def get_url() -> typing.Optional[str]:
"""Return service url."""
return None
@staticmethod
def is_movable() -> bool:
return False
@ -79,6 +84,10 @@ class Ocserv(Service):
def get_configuration():
return {}
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
@staticmethod
def get_logs():
return ""

View File

@ -7,7 +7,7 @@ from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move
from selfprivacy_api.services.generic_size_counter import get_storage_usage
from selfprivacy_api.services.generic_status_getter import get_service_status
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
from selfprivacy_api.utils import ReadUserData, WriteUserData
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.utils.network import get_ip4
@ -32,6 +32,12 @@ class Pleroma(Service):
with open("selfprivacy_api/services/pleroma/pleroma.svg", "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def get_url() -> typing.Optional[str]:
"""Return service url."""
domain = get_domain()
return f"https://social.{domain}"
@staticmethod
def is_movable() -> bool:
return True

View File

@ -52,6 +52,11 @@ class Service(ABC):
def get_svg_icon() -> str:
pass
@staticmethod
@abstractmethod
def get_url() -> typing.Optional[str]:
pass
@staticmethod
@abstractmethod
def is_movable() -> bool:

View File

@ -4,13 +4,5 @@ from huey import SqliteHuey
HUEY_DATABASE = "/etc/nixos/userdata/tasks.db"
# Singleton instance containing the huey database.
class Huey:
"""Huey singleton."""
__instance = None
def __new__(cls):
"""Create a new instance of the huey singleton."""
if Huey.__instance is None:
Huey.__instance = SqliteHuey(HUEY_DATABASE)
return Huey.__instance
huey = SqliteHuey(HUEY_DATABASE)