Compare commits

...

2 Commits

Author SHA1 Message Date
Ferdinand Thiessen e9fbce0da7 [cli] Added install command to install the database and all plugins
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-07-31 16:33:55 +02:00
Ferdinand Thiessen 47b838804b [plugins][cli] Fix initial migration file + Make sure plugin permissions are installed
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
2022-07-31 16:33:09 +02:00
13 changed files with 100 additions and 36 deletions

View File

@ -50,10 +50,16 @@ If not you need to create user and database manually do (or similar on Windows):
) | sudo mysql ) | sudo mysql
Then you can install the database tables, this will update all tables from core + all enabled plugins. Then you can install the database tables, this will update all tables from core + all enabled plugins.
*Hint:* The same command can be later used to upgrade the database after plugins or core are updated. And also install all enabled plugins:
$ flaschengeist install
*Hint:* To only install the database tables, or upgrade the database after plugins or core are updated later
you can use this command:
$ flaschengeist db upgrade heads $ flaschengeist db upgrade heads
## Plugins ## Plugins
To only upgrade one plugin (for example the `events` plugin): To only upgrade one plugin (for example the `events` plugin):
@ -88,6 +94,7 @@ with the difference of the main logger will be forced to output to `stderr` and
of the CLI will override the logging level you have configured for the main logger. of the CLI will override the logging level you have configured for the main logger.
$ flaschengeist run $ flaschengeist run
or with debug messages: or with debug messages:
$ flaschengeist run --debug $ flaschengeist run --debug

View File

@ -0,0 +1,4 @@
from pathlib import Path
alembic_migrations = str(Path(__file__).resolve().parent / "migrations")

View File

@ -18,14 +18,21 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### op.create_table(
"plugin_setting",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("plugin", sa.String(length=127), nullable=True),
sa.Column("name", sa.String(length=127), nullable=False),
sa.Column("value", sa.PickleType(protocol=4), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin_setting")),
)
op.create_table( op.create_table(
"image", "image",
sa.Column("id", flaschengeist.models.Serial(), nullable=False), sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("filename_", sa.String(length=127), nullable=False), sa.Column("filename", sa.String(length=255), nullable=False),
sa.Column("mimetype_", sa.String(length=30), nullable=False), sa.Column("mimetype", sa.String(length=127), nullable=False),
sa.Column("thumbnail_", sa.String(length=127), nullable=True), sa.Column("thumbnail", sa.String(length=255), nullable=True),
sa.Column("path_", sa.String(length=127), nullable=True), sa.Column("path", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_image")), sa.PrimaryKeyConstraint("id", name=op.f("pk_image")),
) )
op.create_table( op.create_table(

View File

@ -88,12 +88,14 @@ def main(*args, **kwargs):
from .export_cmd import export from .export_cmd import export
from .docs_cmd import docs from .docs_cmd import docs
from .run_cmd import run from .run_cmd import run
from .install_cmd import install
# Override logging level # Override logging level
environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX)) environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX))
cli.add_command(export) cli.add_command(export)
cli.add_command(docs) cli.add_command(docs)
cli.add_command(install)
cli.add_command(plugin) cli.add_command(plugin)
cli.add_command(run) cli.add_command(run)
cli(*args, **kwargs) cli(*args, **kwargs)

View File

@ -0,0 +1,20 @@
import click
from click.decorators import pass_context
from flask.cli import with_appcontext
from flask_migrate import upgrade
from flaschengeist.alembic import alembic_migrations
from flaschengeist.cli.plugin_cmd import install_plugin_command
from flaschengeist.utils.hook import Hook
@click.command()
@with_appcontext
@pass_context
@Hook("plugins.installed")
def install(ctx):
# Install database
upgrade(alembic_migrations, revision="heads")
# Install plugins
install_plugin_command(ctx, [], True)

View File

@ -4,7 +4,9 @@ from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from importlib_metadata import EntryPoint, entry_points from importlib_metadata import EntryPoint, entry_points
from flaschengeist.database import db
from flaschengeist.config import config from flaschengeist.config import config
from flaschengeist.models.user import Permission
@click.group() @click.group()
@ -12,6 +14,35 @@ def plugin():
pass pass
def install_plugin_command(ctx, plugin, all):
"""Install one or more plugins"""
if not all and len(plugin) == 0:
ctx.fail("At least one plugin must be specified, or use `--all` flag.")
if all:
plugins = current_app.config["FG_PLUGINS"]
else:
try:
plugins = {plugin_name: current_app.config["FG_PLUGINS"][plugin_name] for plugin_name in plugin}
except KeyError as e:
ctx.fail(f"Invalid plugin name, could not find >{e.args[0]}<")
for name, plugin in plugins.items():
click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False)
# Install permissions
if plugin.permissions:
cur_perm = set(x.name for x in Permission.query.filter(Permission.name.in_(plugin.permissions)).all())
all_perm = set(plugin.permissions)
add = all_perm - cur_perm
if add:
db.session.bulk_save_objects([Permission(name=x) for x in all_perm])
db.session.commit()
# Custom installation steps
plugin.install()
click.secho(" ok", fg="green")
@plugin.command() @plugin.command()
@click.argument("plugin", nargs=-1, type=str) @click.argument("plugin", nargs=-1, type=str)
@click.option("--all", help="Install all enabled plugins", is_flag=True) @click.option("--all", help="Install all enabled plugins", is_flag=True)
@ -19,20 +50,7 @@ def plugin():
@pass_context @pass_context
def install(ctx, plugin, all): def install(ctx, plugin, all):
"""Install one or more plugins""" """Install one or more plugins"""
if not all and len(plugin) == 0: return install_plugin_command(ctx, plugin, all)
ctx.fail("At least one plugin must be specified, or use `--all` flag.")
if all:
plugins = current_app.config["FG_PLUGINS"].values()
else:
try:
plugins = [current_app.config["FG_PLUGINS"][p] for p in plugin]
except KeyError as e:
ctx.fail(f"Invalid plugin ID, could not find >{e.args[0]}<")
for p in plugins:
name = p.id.split(".")[-1]
click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False)
p.install()
click.secho(" ok", fg="green")
@plugin.command() @plugin.command()
@ -44,14 +62,16 @@ def uninstall(ctx: click.Context, plugin):
if len(plugin) == 0: if len(plugin) == 0:
ctx.fail("At least one plugin must be specified") ctx.fail("At least one plugin must be specified")
try: try:
plugins = [current_app.config["FG_PLUGINS"][p] for p in plugin] plugins = {plugin_name: current_app.config["FG_PLUGINS"][plugin_name] for plugin_name in plugin}
except KeyError as e: except KeyError as e:
ctx.fail(f"Invalid plugin ID, could not find >{e.args[0]}<") ctx.fail(f"Invalid plugin ID, could not find >{e.args[0]}<")
if ( if (
click.prompt( click.prompt(
"You are going to uninstall:\n\n" "You are going to uninstall:\n\n"
f"\t{', '.join([p.id.split('.')[-1] for p in plugins])}\n\n" f"\t{', '.join([plugin_name for plugin_name in plugins.keys()])}\n\n"
"Are you sure?", "Are you sure?",
default="n", default="n",
show_choices=True, show_choices=True,
@ -60,10 +80,9 @@ def uninstall(ctx: click.Context, plugin):
!= "y" != "y"
): ):
ctx.exit() ctx.exit()
for p in plugins: for name, plugin in plugins.items():
name = p.id.split(".")[-1]
click.echo(f"Uninstalling {name}{'.'*(20-len(name))}", nl=False) click.echo(f"Uninstalling {name}{'.'*(20-len(name))}", nl=False)
p.uninstall() plugin.uninstall()
click.secho(" ok", fg="green") click.secho(" ok", fg="green")
@ -78,7 +97,7 @@ def ls(enabled, no_header):
return p.version return p.version
plugins = entry_points(group="flaschengeist.plugins") plugins = entry_points(group="flaschengeist.plugins")
enabled_plugins = [key for key, value in config.items() if "enabled" in value] + [config["FLASCHENGEIST"]["auth"]] enabled_plugins = [key for key, value in config.items() if "enabled" in value and value["enabled"]] + [config["FLASCHENGEIST"]["auth"]]
loaded_plugins = current_app.config["FG_PLUGINS"].keys() loaded_plugins = current_app.config["FG_PLUGINS"].keys()
if not no_header: if not no_header:

View File

@ -9,11 +9,11 @@ from ..database import db
class Image(db.Model, ModelSerializeMixin): class Image(db.Model, ModelSerializeMixin):
__tablename__ = "image" __tablename__ = "image"
id: int = db.Column("id", Serial, primary_key=True) id: int = db.Column(Serial, primary_key=True)
filename_: str = db.Column(db.String(127), nullable=False) filename_: str = db.Column("filename", db.String(255), nullable=False)
mimetype_: str = db.Column(db.String(30), nullable=False) mimetype_: str = db.Column("mimetype", db.String(127), nullable=False)
thumbnail_: str = db.Column(db.String(127)) thumbnail_: str = db.Column("thumbnail", db.String(255))
path_: str = db.Column(db.String(127)) path_: str = db.Column("path", db.String(255))
def open(self): def open(self):
return open(self.path_, "rb") return open(self.path_, "rb")

View File

@ -8,6 +8,6 @@ from ..database import db
class _PluginSetting(db.Model): class _PluginSetting(db.Model):
__tablename__ = "plugin_setting" __tablename__ = "plugin_setting"
id = db.Column("id", Serial, primary_key=True) id = db.Column("id", Serial, primary_key=True)
plugin: str = db.Column(db.String(30)) plugin: str = db.Column(db.String(127))
name: str = db.Column(db.String(30), nullable=False) name: str = db.Column(db.String(127), nullable=False)
value: Any = db.Column(db.PickleType(protocol=4)) value: Any = db.Column(db.PickleType(protocol=4))

View File

@ -126,7 +126,8 @@ class Plugin:
def install(self): def install(self):
"""Installation routine """Installation routine
Is always called with Flask application context Is always called with Flask application context,
it is called after the plugin permissions are installed.
""" """
pass pass

View File

@ -19,7 +19,7 @@ class AuthPlain(AuthPlugin):
def install(self): def install(self):
plugins_installed(self.post_install) plugins_installed(self.post_install)
def post_install(self, **kwargs): def post_install(self, *args, **kwargs):
if User.query.filter(User.deleted == False).count() == 0: if User.query.filter(User.deleted == False).count() == 0:
logger.info("Installing admin user") logger.info("Installing admin user")
role = Role.query.filter(Role.name == "Superuser").first() role = Role.query.filter(Role.name == "Superuser").first()

View File

@ -16,6 +16,7 @@ from . import permissions
class RolesPlugin(Plugin): class RolesPlugin(Plugin):
id = "roles"
blueprint = Blueprint("roles", __name__) blueprint = Blueprint("roles", __name__)
permissions = permissions.permissions permissions = permissions.permissions

View File

@ -39,6 +39,8 @@ def scheduled(id: str, replace=False, **kwargs):
class SchedulerPlugin(Plugin): class SchedulerPlugin(Plugin):
id = "scheduler"
def __init__(self, entry_point, config=None): def __init__(self, entry_point, config=None):
super().__init__(entry_point, config) super().__init__(entry_point, config)
self.blueprint = Blueprint(self.name, __name__) self.blueprint = Blueprint(self.name, __name__)

View File

@ -18,6 +18,7 @@ from flaschengeist.utils.datetime import from_iso_format
class UsersPlugin(Plugin): class UsersPlugin(Plugin):
id = "users"
blueprint = Blueprint("users", __name__) blueprint = Blueprint("users", __name__)
permissions = permissions.permissions permissions = permissions.permissions
@ -104,7 +105,7 @@ def frontend(userid, current_session):
raise Forbidden raise Forbidden
if request.method == "POST": if request.method == "POST":
if request.content_length > 1024**2: if request.content_length > 1024 ** 2:
raise BadRequest raise BadRequest
current_session.user_.set_attribute("frontend", request.get_json()) current_session.user_.set_attribute("frontend", request.get_json())
return no_content() return no_content()