Compare commits

...

3 Commits

Author SHA1 Message Date
Ferdinand Thiessen 348adefb7c feat(scheduler): Scheduler is now a plugin
Scheduler allows to schedule tasks, like cron does, but requires special configuration.
2021-12-06 23:48:05 +01:00
Ferdinand Thiessen dca52b764c fix(plugins): Setting a plugin setting to None removes that setting 2021-12-06 23:44:41 +01:00
Ferdinand Thiessen f6c229d2ef feat(core): Selected authentification plugin is always enabled 2021-12-06 23:44:07 +01:00
8 changed files with 126 additions and 38 deletions

View File

@ -36,18 +36,21 @@ class CustomJSONEncoder(JSONEncoder):
def __load_plugins(app):
logger.info("Search for plugins")
logger.debug("Search for plugins")
app.config["FG_PLUGINS"] = {}
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugins"):
logger.debug("Found plugin: >{}<".format(entry_point.name))
plugin = None
if entry_point.name in config and config[entry_point.name].get("enabled", False):
logger.debug(f"Found plugin: >{entry_point.name}<")
if entry_point.name == config["FLASCHENGEIST"]["auth"] or (
entry_point.name in config and config[entry_point.name].get("enabled", False)
):
logger.debug(f"Load plugin {entry_point.name}")
try:
logger.info(f"Load plugin {entry_point.name}")
plugin = entry_point.load()
if not hasattr(plugin, "name"):
setattr(plugin, "name", entry_point.name)
plugin = plugin(config[entry_point.name])
plugin = plugin(config.get(entry_point.name, {}))
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
app.register_blueprint(plugin.blueprint)
except:
@ -55,17 +58,18 @@ def __load_plugins(app):
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.",
exc_info=True,
)
del plugin
continue
if isinstance(plugin, AuthPlugin):
logger.debug(f"Found authentication plugin: {entry_point.name}")
if entry_point.name == config["FLASCHENGEIST"]["auth"]:
app.config["FG_AUTH_BACKEND"] = plugin
if isinstance(plugin, AuthPlugin):
if entry_point.name != config["FLASCHENGEIST"]["auth"]:
logger.debug(f"Unload not configured AuthPlugin {entry_point.name}")
del plugin
continue
else:
logger.info(f"Using authentication plugin: {entry_point.name}")
app.config["FG_AUTH_BACKEND"] = plugin
else:
del plugin
continue
if plugin:
app.config["FG_PLUGINS"][entry_point.name] = plugin
logger.info(f"Using plugin: {entry_point.name}")
app.config["FG_PLUGINS"][entry_point.name] = plugin
if "FG_AUTH_BACKEND" not in app.config:
logger.error("No authentication plugin configured or authentication plugin not found")
raise RuntimeError("No authentication plugin configured or authentication plugin not found")

View File

@ -77,6 +77,7 @@ def configure_app(app, test_config=None):
"auth": {"enabled": True},
"roles": {"enabled": True},
"users": {"enabled": True},
"scheduler": {"enabled": True},
},
)

View File

@ -11,7 +11,11 @@ root = "/api"
# Set secret key
secret_key = "V3ryS3cr3t"
# Domain used by frontend
#domain = "flaschengeist.local"
[scheduler]
# Possible values are: "passive_web" (default), "active_web" and "system"
# See documentation
# cron = "passive_web"
[LOGGING]
# You can override all settings from the logging.toml here
@ -44,12 +48,8 @@ allowed_mimetypes = [
"image/webp"
]
[auth_plain]
enabled = true
[auth_ldap]
# Full documentation https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/plugins_auth_ldap
enabled = false
# host = "localhost"
# port = 389
# base_dn = "dc=example,dc=com"

View File

@ -96,7 +96,10 @@ class Plugin:
.one_or_none()
)
if setting is not None:
setting.value = value
if value is None:
db.session.delete(setting)
else:
setting.value = value
else:
db.session.add(_PluginSetting(plugin=self.name, name=name, value=value))
db.session.commit()

View File

@ -0,0 +1,83 @@
import pkg_resources
from datetime import datetime, timedelta
from flask import Blueprint
from flaschengeist import logger
from flaschengeist.utils.HTTP import no_content
from . import Plugin
class __Task:
def __init__(self, function, **kwags):
self.function = function
self.interval = timedelta(**kwags)
_scheduled_tasks: dict[__Task] = dict()
def scheduled(id: str, replace=False, **kwargs):
"""
kwargs: days, hours, minutes
"""
def real_decorator(function):
if id not in _scheduled_tasks or replace:
logger.info(f"Registered task: {id}")
_scheduled_tasks[id] = __Task(function, **kwargs)
else:
logger.debug(f"Skipping already registered task: {id}")
return function
return real_decorator
class SchedulerPlugin(Plugin):
id = "dev.flaschengeist.scheduler"
name = "scheduler"
blueprint = Blueprint(name, __name__)
def __init__(self, config=None):
"""Constructor called by create_app
Args:
config: Dict configuration containing the plugin section
"""
def __view_func():
self.run_tasks()
return no_content()
def __passiv_func(v):
try:
self.run_tasks()
except:
logger.error("Error while executing scheduled tasks!", exc_info=True)
self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version
cron = None if config is None else config.get("cron", "passive_web").lower()
if cron is None or cron == "passive_web":
self.blueprint.teardown_app_request(__passiv_func)
elif cron == "active_web":
self.blueprint.add_url_rule("/cron", view_func=__view_func)
def run_tasks(self):
changed = False
now = datetime.now()
status = self.get_setting("status", default=dict())
for id, task in _scheduled_tasks.items():
last_run = status.setdefault(id, now)
if last_run + task.interval <= now:
logger.debug(
f"Run task {id}, was scheduled for {last_run + task.interval}, next iteration: {now + task.interval}"
)
task.function()
changed = True
if changed:
# Remove not registered tasks
for id in status.keys():
if id not in _scheduled_tasks.keys():
del status[id]
self.set_setting("status", status)

View File

@ -1,17 +0,0 @@
from flask import current_app
from flaschengeist.utils.HTTP import no_content
_scheduled = set()
def scheduled(func):
_scheduled.add(func)
return func
@current_app.route("/cron")
def __run_scheduled():
for function in _scheduled:
function()
return no_content()

View File

@ -39,6 +39,19 @@ Configuration is done within the a `flaschengeist.toml`file, you can copy the on
Uncomment and change at least all the database parameters!
#### CRON
Some functionality used by some plugins rely on regular updates,
but as flaschengeists works as an WSGI app it can not controll when it gets called.
So you have to configure one of the following options to call flaschengeists CRON tasks:
1. Passive Web-CRON: Every time an users calls flaschengeist a task is scheduled (**NOT RECOMMENDED**)
- Pros: No external configuration needed
- Cons: Slower user experience, no guaranteed execution time of tasks
2. Active Web-CRON: You configure a webworker to call `<flaschengeist>/cron`
- Pros: Guaranteed execution interval, no impact on user experience (at least if you do not limit wsgi worker threads)
- Cons: Uses one of the webserver threads while executing
### Database installation
The user needs to have full permissions to the database.
If not you need to create user and database manually do (or similar on Windows):

View File

@ -68,6 +68,7 @@ setup(
"balance = flaschengeist.plugins.balance:BalancePlugin",
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
"pricelist = flaschengeist.plugins.pricelist:PriceListPlugin",
"scheduler = flaschengeist.plugins.scheduler:SchedulerPlugin",
],
},
cmdclass={