core (main, config): Component auto-configuration:
New `jid`, `secret` and `autoconnect` field are used for component settings. When used, backend will automatically create the profile and set its relevant parameters, or check that existing profile is in sync (and update it if necessary). This make component installation easier to Libervia, as it is not necessary anymore to use the CLI frontend to install a component. rel 476
This commit is contained in:
parent
51776603e0
commit
a9fa2eeed4
3 changed files with 159 additions and 16 deletions
|
|
@ -31,6 +31,7 @@ from libervia.backend.core import patches
|
|||
from libervia.backend.models.tor import TorData
|
||||
from libervia.backend.tools.config import (
|
||||
BridgeSettings,
|
||||
ComponentSettings,
|
||||
DBusBridgeSettings,
|
||||
PbBridgeSettings,
|
||||
)
|
||||
|
|
@ -296,6 +297,8 @@ class LiberviaBackend(service.Service):
|
|||
self.stopService()
|
||||
sys.exit(C.EXIT_ERROR)
|
||||
|
||||
await self._sync_components_profiles()
|
||||
|
||||
self.initialised.callback(None)
|
||||
log.info(_("Backend is ready"))
|
||||
|
||||
|
|
@ -535,6 +538,109 @@ class LiberviaBackend(service.Service):
|
|||
defers_list.append(utils.as_deferred(unload))
|
||||
return defers_list
|
||||
|
||||
async def _sync_components_profiles(self) -> None:
|
||||
"""Synchronize components profiles data with settings.
|
||||
|
||||
``jid`` and ``secret`` are used in settings as helpers to easily create
|
||||
autoconnecting components. This method syncs those settings with actual profiles.
|
||||
"""
|
||||
log.debug("Synchronizing component settings.")
|
||||
for component_field_name in G.config.component.__class__.model_fields:
|
||||
component_settings = cast(
|
||||
ComponentSettings, getattr(G.config.component, component_field_name)
|
||||
)
|
||||
|
||||
if component_settings.jid is not None:
|
||||
# Automatic setting fields are used
|
||||
assert component_settings.secret is not None
|
||||
entry_point = component_settings._import_name
|
||||
assert entry_point is not None
|
||||
profile = entry_point
|
||||
if not G.storage.has_profile(entry_point):
|
||||
log.info(f"Creating profile {profile!r}")
|
||||
await G.host.memory.create_profile(profile, "", entry_point)
|
||||
# FIXME: why do we need to start session to set parameters? To check
|
||||
# after parameters refactoring.
|
||||
await G.host.memory.start_session("", profile)
|
||||
await G.host.memory.param_set(
|
||||
"JabberID",
|
||||
str(component_settings.jid),
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
)
|
||||
await G.host.memory.param_set(
|
||||
"Password",
|
||||
component_settings.secret,
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
)
|
||||
await G.host.memory.param_set(
|
||||
"autoconnect_backend",
|
||||
component_settings.autoconnect,
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
)
|
||||
log.info(f"Component profile {profile!r} set for auto-connection.")
|
||||
else:
|
||||
# Profile exists, check if values correspond to settings
|
||||
try:
|
||||
# FIXME: As for new profile condition above, we need to check if
|
||||
# we can skip session starting to set profile parameters.
|
||||
await G.host.memory.start_session("", profile)
|
||||
except exceptions.PasswordError:
|
||||
# Component passwords should be empty to allow for autoconnection.
|
||||
log.warning(
|
||||
f"Can't set automatically component profile for {profile}: "
|
||||
"the password is not empty as expected."
|
||||
)
|
||||
continue
|
||||
current_jid = await G.host.memory.param_get_a_async(
|
||||
"JabberID", "Connection", profile_key=profile
|
||||
)
|
||||
if current_jid != str(component_settings.jid):
|
||||
log.warning(
|
||||
f"Component profile {profile!r} JID mismatch: "
|
||||
f"current={current_jid}, settings={component_settings.jid}, "
|
||||
f"updating from settings"
|
||||
)
|
||||
await G.host.memory.param_set(
|
||||
"JabberID",
|
||||
str(component_settings.jid),
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
)
|
||||
|
||||
current_password = await G.host.memory.param_get_a_async(
|
||||
"Password", "Connection", profile_key=profile
|
||||
)
|
||||
if current_password != component_settings.secret:
|
||||
log.warning(
|
||||
f"Component profile {profile!r} secret mismatch, "
|
||||
f"updating from settings"
|
||||
)
|
||||
await G.host.memory.param_set(
|
||||
"Password",
|
||||
component_settings.secret,
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
)
|
||||
|
||||
current_autoconnect = await G.host.memory.param_get_a_async(
|
||||
"autoconnect_backend", "Connection", profile_key=profile
|
||||
)
|
||||
expected_autoconnect = component_settings.autoconnect
|
||||
if current_autoconnect != expected_autoconnect:
|
||||
log.warning(
|
||||
f"Component profile {profile!r} autoconnect setting"
|
||||
f" mismatch, updating from settings"
|
||||
)
|
||||
await G.host.memory.param_set(
|
||||
"autoconnect_backend",
|
||||
expected_autoconnect,
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
)
|
||||
|
||||
def _connect(self, profile_key, password="", options=None):
|
||||
profile = self.memory.get_profile_name(profile_key)
|
||||
return defer.ensureDeferred(self.connect(profile, password, options))
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ from uuid import uuid4
|
|||
import shortuuid
|
||||
from twisted.internet import defer, error, reactor
|
||||
from twisted.internet.base import DelayedCall
|
||||
from twisted.python import failure
|
||||
from twisted.words.protocols.jabber import jid
|
||||
|
||||
from libervia.backend.core import exceptions
|
||||
|
|
@ -44,7 +43,6 @@ from libervia.backend.memory.disco import Discovery
|
|||
from libervia.backend.memory.params import Params
|
||||
from libervia.backend.memory.persistent import PersistentDict
|
||||
from libervia.backend.memory.sqla import (
|
||||
Notification,
|
||||
NotificationPriority,
|
||||
NotificationStatus,
|
||||
NotificationType,
|
||||
|
|
@ -518,15 +516,19 @@ class Memory:
|
|||
|
||||
self.memory_data["Profile_default"] = profile
|
||||
|
||||
def create_profile(self, name, password, component=None):
|
||||
def create_profile(
|
||||
self, name: str, password: str, component: str | None = None
|
||||
) -> defer.Deferred[None]:
|
||||
"""Create a new profile
|
||||
|
||||
@param name(unicode): profile name
|
||||
@param password(unicode): profile password
|
||||
Can be empty to disable password
|
||||
@param component(None, unicode): set to entry point if this is a component
|
||||
@return: Deferred
|
||||
@raise exceptions.NotFound: component is not a known plugin import name
|
||||
@param name: profile name
|
||||
@param password: profile password (can be empty to disable password)
|
||||
@param component: entry point if this is a component
|
||||
@return: Deferred that fires when the profile is created
|
||||
@raise ValueError: if profile name is empty, starts with '@', or contains line
|
||||
feed
|
||||
@raise exceptions.ConflictError: if a session for this profile exists
|
||||
@raise exceptions.NotFound: if component is not a known plugin import name
|
||||
"""
|
||||
if not name:
|
||||
raise ValueError("Empty profile name")
|
||||
|
|
@ -547,7 +549,8 @@ class Memory:
|
|||
)
|
||||
)
|
||||
)
|
||||
# FIXME: PLUGIN_INFO is not currently accessible after import, but type shoul be tested here
|
||||
# FIXME: PLUGIN_INFO is not currently accessible after import, but type should
|
||||
# be tested here
|
||||
# if self.host.plugins[component].PLUGIN_INFO[u"type"] != C.PLUG_TYPE_ENTRY_POINT:
|
||||
# raise ValueError(_(u"Plugin {component} is not an entry point !".format(
|
||||
# component = component)))
|
||||
|
|
@ -612,12 +615,12 @@ class Memory:
|
|||
"""
|
||||
return self.storage.profile_is_component(profile_name)
|
||||
|
||||
def get_entry_point(self, profile_name):
|
||||
"""Get a component entry point
|
||||
def get_entry_point(self, profile_name: str) -> str:
|
||||
"""Get a component entry point for a given profile.
|
||||
|
||||
@param profile_name(unicode): name of the profile
|
||||
@return (bool): True if profile is a component
|
||||
@raise exceptions.NotFound: profile doesn't exist
|
||||
@param profile_name: name of the profile
|
||||
@return: the component entry point (import name) for the profile
|
||||
@raise exceptions.NotFound: if the profile doesn't exist or is not a component
|
||||
"""
|
||||
return self.storage.get_entry_point(profile_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from socket import getfqdn
|
||||
from typing import Annotated, Any, Literal, TypeVar, cast
|
||||
from typing import Annotated, Any, Literal, Self, TypeVar, cast
|
||||
|
||||
from pydantic import (
|
||||
AfterValidator,
|
||||
|
|
@ -32,6 +32,7 @@ from pydantic import (
|
|||
ValidationError,
|
||||
create_model,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic.types import PathType
|
||||
from pydantic_settings import (
|
||||
|
|
@ -45,6 +46,7 @@ from libervia.backend.core import exceptions
|
|||
from libervia.backend.core.constants import Const as C, xdg_data_home
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.models.types import JIDType
|
||||
from libervia.backend.tools.common.files_utils import get_downloads_dir
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
|
@ -103,6 +105,7 @@ class DynamicSettingsBase(ConfigOptions):
|
|||
|
||||
_key: str = ""
|
||||
_sub_key: str | None = None
|
||||
_import_name: str | None = None
|
||||
|
||||
@classmethod
|
||||
def _init_subclass(cls, /, key: str, plugin_info: dict | None = None) -> None:
|
||||
|
|
@ -127,6 +130,7 @@ class DynamicSettingsBase(ConfigOptions):
|
|||
f"There is already a plugin registered for {key!r}.{sub_key!r}."
|
||||
)
|
||||
register[sub_key] = cls
|
||||
cls._import_name = plugin_info.get(C.PI_IMPORT_NAME)
|
||||
else:
|
||||
cls._sub_key = None
|
||||
DynamicSettings[key] = cls
|
||||
|
|
@ -143,10 +147,40 @@ class PluginSettings(DynamicSettingsBase):
|
|||
class ComponentSettings(DynamicSettingsBase):
|
||||
"""Configuration options for components."""
|
||||
|
||||
jid: JIDType | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"JID of the component. If used, the component profile will be created"
|
||||
" automatically (with the component entry point used as name), and"
|
||||
" auto-connect on backend startup."
|
||||
),
|
||||
)
|
||||
secret: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Shared secret between the component and the XMPP server. To be used with"
|
||||
" ``jid`` field for automatic configuration and connetion."
|
||||
),
|
||||
)
|
||||
autoconnect: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Whether to automatically connect this component on backend startup. Used"
|
||||
" when ``jid`` and ``secret`` are set."
|
||||
),
|
||||
)
|
||||
|
||||
def __init_subclass__(cls, /, plugin_info: dict, **kwargs) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
super()._init_subclass(key="component", plugin_info=plugin_info)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def jid_and_secret_together(self) -> Self:
|
||||
"""Check that "jid" and "secret" are (un)set at the same time"""
|
||||
if (self.jid is None) ^ (self.secret is None):
|
||||
raise ValueError('You must set "jid" and "secret" together.')
|
||||
return self
|
||||
|
||||
|
||||
class FrontendSettings(DynamicSettingsBase):
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue