mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git
synced 2024-11-25 13:31:27 +00:00
feat: service configuration prototype
This commit is contained in:
parent
4066be38ec
commit
9cd8d75f73
|
@ -27,4 +27,4 @@ async def get_token_header(
|
|||
|
||||
def get_api_version() -> str:
|
||||
"""Get API version"""
|
||||
return "3.2.2"
|
||||
return "3.2.2+configs"
|
||||
|
|
|
@ -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
|
||||
class Service:
|
||||
id: str
|
||||
|
@ -132,6 +151,37 @@ class Service:
|
|||
"""Get storage usage for a service"""
|
||||
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
|
||||
@strawberry.field
|
||||
def backup_snapshots(self) -> Optional[List["SnapshotInfo"]]:
|
||||
|
|
|
@ -33,6 +33,16 @@ class ServiceMutationReturn(GenericMutationReturn):
|
|||
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
|
||||
class MoveServiceInput:
|
||||
"""Move service input type."""
|
||||
|
@ -157,6 +167,34 @@ class ServicesMutations:
|
|||
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])
|
||||
def move_service(self, input: MoveServiceInput) -> ServiceJobMutationReturn:
|
||||
"""Move service."""
|
||||
|
|
|
@ -84,12 +84,12 @@ class Bitwarden(Service):
|
|||
def restart():
|
||||
subprocess.run(["systemctl", "restart", "vaultwarden.service"])
|
||||
|
||||
@staticmethod
|
||||
def get_configuration():
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
73
selfprivacy_api/services/config_item.py
Normal file
73
selfprivacy_api/services/config_item.py
Normal 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
|
|
@ -3,11 +3,16 @@ import base64
|
|||
import subprocess
|
||||
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.services.service import Service, ServiceStatus
|
||||
from selfprivacy_api.services.forgejo.icon import FORGEJO_ICON
|
||||
from selfprivacy_api.services.config_item import (
|
||||
StringConfigItem,
|
||||
BoolConfigItem,
|
||||
ConfigItem,
|
||||
)
|
||||
|
||||
|
||||
class Forgejo(Service):
|
||||
|
@ -16,6 +21,41 @@ class Forgejo(Service):
|
|||
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
|
||||
def get_id() -> str:
|
||||
"""Return service id. For compatibility keep in gitea."""
|
||||
|
@ -82,13 +122,28 @@ class Forgejo(Service):
|
|||
def restart():
|
||||
subprocess.run(["systemctl", "restart", "forgejo.service"])
|
||||
|
||||
@staticmethod
|
||||
def get_configuration():
|
||||
return {}
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
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
|
||||
def set_configuration(config_items):
|
||||
return super().set_configuration(config_items)
|
||||
@classmethod
|
||||
def set_configuration(cls, 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
|
||||
def get_logs():
|
||||
|
|
|
@ -88,12 +88,12 @@ class JitsiMeet(Service):
|
|||
)
|
||||
subprocess.run(["systemctl", "restart", "jicofo.service"], check=False)
|
||||
|
||||
@staticmethod
|
||||
def get_configuration():
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -89,12 +89,12 @@ class MailServer(Service):
|
|||
subprocess.run(["systemctl", "restart", "dovecot2.service"], check=False)
|
||||
subprocess.run(["systemctl", "restart", "postfix.service"], check=False)
|
||||
|
||||
@staticmethod
|
||||
def get_configuration():
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -85,13 +85,13 @@ class Nextcloud(Service):
|
|||
"""Restart Nextcloud service."""
|
||||
subprocess.Popen(["systemctl", "restart", "phpfpm-nextcloud.service"])
|
||||
|
||||
@staticmethod
|
||||
def get_configuration() -> dict:
|
||||
@classmethod
|
||||
def get_configuration(cls) -> dict:
|
||||
"""Return Nextcloud configuration."""
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -69,12 +69,12 @@ class Ocserv(Service):
|
|||
def restart():
|
||||
subprocess.run(["systemctl", "restart", "ocserv.service"], check=False)
|
||||
|
||||
@staticmethod
|
||||
def get_configuration():
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -72,12 +72,12 @@ class Pleroma(Service):
|
|||
subprocess.run(["systemctl", "restart", "pleroma.service"])
|
||||
subprocess.run(["systemctl", "restart", "postgresql.service"])
|
||||
|
||||
@staticmethod
|
||||
def get_configuration(config_items):
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Abstract class for a service running on a server"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from selfprivacy_api import utils
|
||||
from selfprivacy_api.utils import ReadUserData, WriteUserData
|
||||
|
@ -179,14 +179,14 @@ class Service(ABC):
|
|||
"""Restart the service. Usually this means restarting systemd unit."""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_configuration():
|
||||
def get_configuration(cls) -> dict:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def set_configuration(config_items):
|
||||
def set_configuration(cls, config_items):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -163,12 +163,12 @@ class DummyService(Service):
|
|||
cls.set_status(ServiceStatus.RELOADING) # is a correct one?
|
||||
cls.change_status_with_async_delay(ServiceStatus.ACTIVE, cls.startstop_delay)
|
||||
|
||||
@staticmethod
|
||||
def get_configuration():
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def set_configuration(config_items):
|
||||
@classmethod
|
||||
def set_configuration(cls, config_items):
|
||||
return super().set_configuration(config_items)
|
||||
|
||||
@staticmethod
|
||||
|
|
Loading…
Reference in a new issue