feat: service configuration prototype

This commit is contained in:
Inex Code 2024-07-02 23:35:28 +04:00
parent 4066be38ec
commit 9cd8d75f73
13 changed files with 257 additions and 41 deletions

View file

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

View file

@ -103,6 +103,25 @@ def service_dns_to_graphql(record: ServiceDnsRecord) -> DnsRecord:
) )
@strawberry.interface
class ConfigItem:
id: str
description: str
widget: str
type: str
@strawberry.type
class StringConfigItem(ConfigItem):
value: str
regex: Optional[str]
@strawberry.type
class BoolConfigItem(ConfigItem):
value: bool
@strawberry.type @strawberry.type
class Service: class Service:
id: str id: str
@ -132,6 +151,37 @@ class Service:
"""Get storage usage for a service""" """Get storage usage for a service"""
return get_storage_usage(self) return get_storage_usage(self)
@strawberry.field
def configuration(self) -> Optional[List[ConfigItem]]:
"""Get service configuration"""
service = get_service_by_id(self.id)
if service is None:
return None
config_items = service.get_configuration()
# If it is an empty dict, return none
if not config_items:
return None
# By the "type" field convert every dict into a ConfigItem. In the future there will be more types.
return [
StringConfigItem(
id=item["id"],
description=item["description"],
widget=item["widget"],
type=item["type"],
value=item["value"],
regex=item.get("regex"),
)
if item["type"] == "string"
else BoolConfigItem(
id=item["id"],
description=item["description"],
widget=item["widget"],
type=item["type"],
value=item["value"],
)
for item in config_items
]
# TODO: fill this # TODO: fill this
@strawberry.field @strawberry.field
def backup_snapshots(self) -> Optional[List["SnapshotInfo"]]: def backup_snapshots(self) -> Optional[List["SnapshotInfo"]]:

View file

@ -33,6 +33,16 @@ class ServiceMutationReturn(GenericMutationReturn):
service: typing.Optional[Service] = None service: typing.Optional[Service] = None
@strawberry.input
class SetServiceConfigurationInput:
"""Set service configuration input type.
The values might be of different types: str or bool.
"""
service_id: str
configuration: strawberry.scalars.JSON
@strawberry.input @strawberry.input
class MoveServiceInput: class MoveServiceInput:
"""Move service input type.""" """Move service input type."""
@ -157,6 +167,34 @@ class ServicesMutations:
service=service_to_graphql_service(service), service=service_to_graphql_service(service),
) )
@strawberry.mutation(permission_classes=[IsAuthenticated])
def set_service_configuration(
self, input: SetServiceConfigurationInput
) -> ServiceMutationReturn:
"""Set the new configuration values"""
service = get_service_by_id(input.service_id)
if service is None:
return ServiceMutationReturn(
success=False,
message=f"Service does not exist: {input.service_id}",
code=404,
)
try:
service.set_configuration(input.configuration)
return ServiceMutationReturn(
success=True,
message="Service configuration updated.",
code=200,
service=service_to_graphql_service(service),
)
except Exception as e:
return ServiceMutationReturn(
success=False,
message=pretty_error(e),
code=400,
service=service_to_graphql_service(service),
)
@strawberry.mutation(permission_classes=[IsAuthenticated]) @strawberry.mutation(permission_classes=[IsAuthenticated])
def move_service(self, input: MoveServiceInput) -> ServiceJobMutationReturn: def move_service(self, input: MoveServiceInput) -> ServiceJobMutationReturn:
"""Move service.""" """Move service."""

View file

@ -84,12 +84,12 @@ class Bitwarden(Service):
def restart(): def restart():
subprocess.run(["systemctl", "restart", "vaultwarden.service"]) subprocess.run(["systemctl", "restart", "vaultwarden.service"])
@staticmethod @classmethod
def get_configuration(): def get_configuration(cls):
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod

View file

@ -0,0 +1,73 @@
from abc import ABC, abstractmethod
import re
from typing import Optional
class ConfigItem(ABC):
id: str
description: str
widget: str
type: str
@abstractmethod
def get_value(self, service_options):
pass
@abstractmethod
def set_value(self, value, service_options):
pass
def as_dict(self, service_options):
return {
"id": self.id,
"type": self.type,
"description": self.description,
"widget": self.widget,
"value": self.get_value(service_options),
}
class StringConfigItem(ConfigItem):
def __init__(
self,
id: str,
default_value: str,
description: str,
regex: Optional[str] = None,
widget: Optional[str] = None,
):
self.id = id
self.type = "string"
self.default_value = default_value
self.description = description
self.regex = re.compile(regex) if regex else None
self.widget = widget if widget else "text"
def get_value(self, service_options):
return service_options.get(self.id, self.default_value)
def set_value(self, value, service_options):
if self.regex and not self.regex.match(value):
raise ValueError(f"Value {value} does not match regex {self.regex}")
service_options[self.id] = value
class BoolConfigItem(ConfigItem):
def __init__(
self,
id: str,
default_value: bool,
description: str,
widget: Optional[str] = None,
):
self.id = id
self.type = "bool"
self.default_value = default_value
self.description = description
self.widget = widget if widget else "switch"
def get_value(self, service_options):
return service_options.get(self.id, self.default_value)
def set_value(self, value, service_options):
service_options[self.id] = value

View file

@ -3,11 +3,16 @@ import base64
import subprocess import subprocess
from typing import Optional, List from typing import Optional, List
from selfprivacy_api.utils import get_domain from selfprivacy_api.utils import get_domain, ReadUserData, WriteUserData
from selfprivacy_api.utils.systemd import get_service_status from selfprivacy_api.utils.systemd import get_service_status
from selfprivacy_api.services.service import Service, ServiceStatus from selfprivacy_api.services.service import Service, ServiceStatus
from selfprivacy_api.services.forgejo.icon import FORGEJO_ICON from selfprivacy_api.services.forgejo.icon import FORGEJO_ICON
from selfprivacy_api.services.config_item import (
StringConfigItem,
BoolConfigItem,
ConfigItem,
)
class Forgejo(Service): class Forgejo(Service):
@ -16,6 +21,41 @@ class Forgejo(Service):
Previously was Gitea, so some IDs are still called gitea for compatibility. Previously was Gitea, so some IDs are still called gitea for compatibility.
""" """
config_items: dict[str, ConfigItem] = {
"subdomain": StringConfigItem(
id="subdomain",
default_value="git",
description="Subdomain",
regex=r"[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]",
widget="subdomain",
),
"appName": StringConfigItem(
id="appName",
default_value="SelfPrivacy git Service",
description="The name displayed in the web interface",
),
"enableLfs": BoolConfigItem(
id="enableLfs",
default_value=True,
description="Enable Git LFS",
),
"forcePrivate": BoolConfigItem(
id="forcePrivate",
default_value=False,
description="Force all new repositories to be private",
),
"disableRegistration": BoolConfigItem(
id="disableRegistration",
default_value=False,
description="Disable registration of new users",
),
"requireSigninView": BoolConfigItem(
id="requireSigninView",
default_value=False,
description="Require signin to view any page",
),
}
@staticmethod @staticmethod
def get_id() -> str: def get_id() -> str:
"""Return service id. For compatibility keep in gitea.""" """Return service id. For compatibility keep in gitea."""
@ -82,13 +122,28 @@ class Forgejo(Service):
def restart(): def restart():
subprocess.run(["systemctl", "restart", "forgejo.service"]) subprocess.run(["systemctl", "restart", "forgejo.service"])
@staticmethod @classmethod
def get_configuration(): def get_configuration(cls):
return {} with ReadUserData() as user_data:
return {
key: cls.config_items[key].as_dict(
user_data.get("modules", {}).get(cls.get_id(), {})
)
for key in cls.config_items
}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) with WriteUserData() as user_data:
if "modules" not in user_data:
user_data["modules"] = {}
if cls.get_id() not in user_data["modules"]:
user_data["modules"][cls.get_id()] = {}
for key, value in config_items.items():
cls.config_items[key].set_value(
value,
user_data["modules"][cls.get_id()],
)
@staticmethod @staticmethod
def get_logs(): def get_logs():

View file

@ -88,12 +88,12 @@ class JitsiMeet(Service):
) )
subprocess.run(["systemctl", "restart", "jicofo.service"], check=False) subprocess.run(["systemctl", "restart", "jicofo.service"], check=False)
@staticmethod @classmethod
def get_configuration(): def get_configuration(cls):
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod

View file

@ -89,12 +89,12 @@ class MailServer(Service):
subprocess.run(["systemctl", "restart", "dovecot2.service"], check=False) subprocess.run(["systemctl", "restart", "dovecot2.service"], check=False)
subprocess.run(["systemctl", "restart", "postfix.service"], check=False) subprocess.run(["systemctl", "restart", "postfix.service"], check=False)
@staticmethod @classmethod
def get_configuration(): def get_configuration(cls):
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod

View file

@ -85,13 +85,13 @@ class Nextcloud(Service):
"""Restart Nextcloud service.""" """Restart Nextcloud service."""
subprocess.Popen(["systemctl", "restart", "phpfpm-nextcloud.service"]) subprocess.Popen(["systemctl", "restart", "phpfpm-nextcloud.service"])
@staticmethod @classmethod
def get_configuration() -> dict: def get_configuration(cls) -> dict:
"""Return Nextcloud configuration.""" """Return Nextcloud configuration."""
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod

View file

@ -69,12 +69,12 @@ class Ocserv(Service):
def restart(): def restart():
subprocess.run(["systemctl", "restart", "ocserv.service"], check=False) subprocess.run(["systemctl", "restart", "ocserv.service"], check=False)
@staticmethod @classmethod
def get_configuration(): def get_configuration(cls):
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod

View file

@ -72,12 +72,12 @@ class Pleroma(Service):
subprocess.run(["systemctl", "restart", "pleroma.service"]) subprocess.run(["systemctl", "restart", "pleroma.service"])
subprocess.run(["systemctl", "restart", "postgresql.service"]) subprocess.run(["systemctl", "restart", "postgresql.service"])
@staticmethod @classmethod
def get_configuration(config_items): def get_configuration(cls):
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod

View file

@ -1,6 +1,6 @@
"""Abstract class for a service running on a server""" """Abstract class for a service running on a server"""
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Optional from typing import Any, List, Optional
from selfprivacy_api import utils from selfprivacy_api import utils
from selfprivacy_api.utils import ReadUserData, WriteUserData from selfprivacy_api.utils import ReadUserData, WriteUserData
@ -179,14 +179,14 @@ class Service(ABC):
"""Restart the service. Usually this means restarting systemd unit.""" """Restart the service. Usually this means restarting systemd unit."""
pass pass
@staticmethod @classmethod
@abstractmethod @abstractmethod
def get_configuration(): def get_configuration(cls) -> dict:
pass pass
@staticmethod @classmethod
@abstractmethod @abstractmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
pass pass
@staticmethod @staticmethod

View file

@ -163,12 +163,12 @@ class DummyService(Service):
cls.set_status(ServiceStatus.RELOADING) # is a correct one? cls.set_status(ServiceStatus.RELOADING) # is a correct one?
cls.change_status_with_async_delay(ServiceStatus.ACTIVE, cls.startstop_delay) cls.change_status_with_async_delay(ServiceStatus.ACTIVE, cls.startstop_delay)
@staticmethod @classmethod
def get_configuration(): def get_configuration(cls):
return {} return {}
@staticmethod @classmethod
def set_configuration(config_items): def set_configuration(cls, config_items):
return super().set_configuration(config_items) return super().set_configuration(config_items)
@staticmethod @staticmethod