From e839e814d912017806e4dbf4205de098aa0dcd43 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 5 Feb 2025 16:54:59 +0100 Subject: [PATCH 1/4] Base for init command - it's using all commands available in server --- server/mergin/app.py | 9 -- server/mergin/commands.py | 198 +++++++++++++++++++++++++++----------- 2 files changed, 143 insertions(+), 64 deletions(-) diff --git a/server/mergin/app.py b/server/mergin/app.py index 24b1ebc9..0f23e2ac 100644 --- a/server/mergin/app.py +++ b/server/mergin/app.py @@ -139,15 +139,6 @@ def create_simple_app() -> Flask: if Configuration.GEVENT_WORKER: flask_app.wsgi_app = GeventTimeoutMiddleware(flask_app.wsgi_app) - @flask_app.cli.command() - def init_db(): - """Re-creates application database""" - print("Database initialization ...") - db.drop_all(bind=None) - db.create_all(bind=None) - db.session.commit() - print("Done. Tables created.") - add_commands(flask_app) return flask_app diff --git a/server/mergin/commands.py b/server/mergin/commands.py index e1b80875..6380927b 100644 --- a/server/mergin/commands.py +++ b/server/mergin/commands.py @@ -1,95 +1,183 @@ import click from flask import Flask -from sqlalchemy import or_, func +import random +import string +from datetime import datetime, timezone -from .config import Configuration +def _echo_title(title): + click.echo("") + click.echo(f"# {title}") + click.echo() + + +def _echo_error(msg): + click.secho("Error: ", fg="red", nl=False, bold=True) + click.secho(msg, fg="bright_red") def add_commands(app: Flask): from .app import db + from mergin.auth.models import UserProfile - @app.cli.group() - def server(): - """Server management commands.""" - pass + def _check_celery(): + from celery import current_app - @server.command() - @click.option("--email", required=True) - def send_check_email(email: str): # pylint: disable=W0612 + ping_celery = current_app.control.inspect().ping() + if not ping_celery: + _echo_error( + "Celery process not running properly. Configure celery worker and celery beat. This breaks also email sending from the system.", + ) + return + click.secho("Celery is running properly", fg="green") + return True + + def _send_statistics(): + from .stats.tasks import send_statistics + + if not app.config.get("COLLECT_STATISTICS"): + return + + _echo_title("Sending statistics.") + if not _check_celery(): + return + send_statistics.delay() + click.secho("Statistics sent.", fg="green") + + def _send_email(email: str): """Send check email to specified email address.""" from .celery import send_email_async + _echo_title(f"Sending check email to specified email address {email}.") if app.config["MAIL_SUPPRESS_SEND"]: - click.echo( - click.style( - "Sending emails is disabled. Please set MAIL_SUPPRESS_SEND=False to enable sending emails.", - fg="red", - ) + _echo_error( + "Sending emails is disabled. Please set MAIL_SUPPRESS_SEND=False to enable sending emails." ) return - if not app.config["MAIL_DEFAULT_SENDER"]: - click.echo( - click.style( - "No default sender set. Please set MAIL_DEFAULT_SENDER environment variable", - fg="red", - ) + default_sender = app.config.get("MAIL_DEFAULT_SENDER") + if not default_sender: + _echo_error( + "No default sender set. Please set MAIL_DEFAULT_SENDER environment variable", ) return email_data = { "subject": "Mergin Maps server check", "html": "Awesome, your email configuration of Mergin Maps server is working.", "recipients": [email], - "sender": app.config["MAIL_DEFAULT_SENDER"], + "sender": default_sender, } - click.echo( - f"Sending email to specified email address {email}. Check your inbox." - ) try: + is_celery_running = _check_celery() + if not is_celery_running: + return send_email_async.delay(**email_data) except Exception as e: - click.echo( - click.style( - f"Error sending email: {e}", - fg="red", - ) + _echo_error( + f"Error sending email: {e}", ) - @server.command() - def check(): # pylint: disable=W0612 - """Check server configuration. Define email to send testing email.""" - from celery import current_app + def _check_server(): # pylint: disable=W0612 + """Check server configuration.""" - click.echo(f"Mergin Maps server version: {app.config['VERSION']}") + _echo_title("Server health check") + click.echo(f"Mergin Maps version: {app.config['VERSION']}") base_url = app.config["MERGIN_BASE_URL"] if not base_url: - click.echo( - click.style( - "No base URL set. Please set MERGIN_BASE_URL environment variable", - fg="red", - ), + _echo_error( + "No base URL set. Please set MERGIN_BASE_URL environment variable", ) else: - click.echo(f"Base URL of server is {base_url}") + click.secho(f"Base URL of server is {base_url}", fg="green") tables = db.engine.table_names() if not tables: - click.echo( - click.style( - "Database not initialized. Run flask init-db command", fg="red" - ) - ) + _echo_error("Database not initialized. Run flask init-db command") else: - click.echo("Database initialized properly") + click.secho("Database initialized properly", fg="green") - ping_celery = current_app.control.inspect().ping() - if not ping_celery: - click.echo( - click.style( - "Celery not running. Configure celery worker and celery beat", - fg="red", - ) + _check_celery() + + def _init_db(): + """Create database tables.""" + from .stats.models import MerginInfo + + _echo_title("Database initialization") + with click.progressbar( + label="Creating database", length=4, show_eta=False + ) as progress_bar: + progress_bar.update(0) + db.drop_all(bind=None) + progress_bar.update(1) + db.create_all(bind=None) + progress_bar.update(2) + db.session.commit() + progress_bar.update(3) + info = MerginInfo.query.first() + if not info and app.config.get("COLLECT_STATISTICS"): + # create new info with random service id + service_id = app.config.get("SERVICE_ID", None) + info = MerginInfo(service_id) + db.session.add(info) + db.session.commit() + progress_bar.update(4) + + click.secho("Tables created.", fg="green") + + @app.cli.command() + def init_db(): + """Re-create database tables.""" + _init_db() + + @app.cli.command() + @click.option("--email", "-e", required=True) + @click.option( + "--recreate", + "-r", + help="Recreate database and admin user.", + is_flag=True, + required=False, + ) + def init(email: str, recreate: bool): + """Initialize database if does not exist or -r is provided. Perform check of server configuration. Send statistics, respecting your setup.""" + + from .auth.models import User + + tables = db.engine.table_names() + if not tables or recreate: + _init_db() + + _echo_title("Creating admin user. Copy generated password.") + username = "admin" + password_chars = string.ascii_letters + string.digits + password = "".join(random.choice(password_chars) for i in range(12)) + user = User(username=username, passwd=password, email=email, is_admin=True) + user.profile = UserProfile() + user.active = True + db.session.add(user) + db.session.commit() + click.secho( + "Admin user created. Please save generated password.", fg="green" ) - else: - click.echo("Celery running properly") + click.secho(f"Username: {username}") + click.secho(f"Password: {password}") + click.secho(f"Email: {email}") + _check_server() + _send_email(email) + _send_statistics() + + @app.cli.group() + def server(): + """Server management commands.""" + pass + + @server.command() + @click.option("--email", required=True) + def send_check_email(email: str): # pylint: disable=W0612 + """Send check email to specified email address.""" + _send_email(email) + + @server.command() + def check(): + """Check server configuration.""" + _check_server() From 009f1ffbe81abbdb1ab1101088479b687d984c40 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 7 Feb 2025 08:59:02 +0100 Subject: [PATCH 2/4] Disable sedning stats on application init --- server/application.py | 5 ----- server/mergin/commands.py | 21 +++++++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/server/application.py b/server/application.py index db46e12e..8397030f 100644 --- a/server/application.py +++ b/server/application.py @@ -76,8 +76,3 @@ def setup_periodic_tasks(sender, **kwargs): send_statistics, name="send usage statistics", ) - - -# send report after start -if Configuration.COLLECT_STATISTICS: - send_statistics.delay() diff --git a/server/mergin/commands.py b/server/mergin/commands.py index 6380927b..b85833f0 100644 --- a/server/mergin/commands.py +++ b/server/mergin/commands.py @@ -33,12 +33,18 @@ def _check_celery(): return True def _send_statistics(): - from .stats.tasks import send_statistics + from .stats.tasks import send_statistics, save_statistics + + _echo_title("Sending statistics.") + # save rows to MerginStatistics table + save_statistics.delay() if not app.config.get("COLLECT_STATISTICS"): + click.secho( + "Statistics sending is disabled.", + ) return - _echo_title("Sending statistics.") if not _check_celery(): return send_statistics.delay() @@ -80,6 +86,13 @@ def _check_server(): # pylint: disable=W0612 """Check server configuration.""" _echo_title("Server health check") + edition_map = { + "ce": "Community Edition", + "ee": "Enterprise Edition", + } + edition = edition_map.get(app.config["SERVER_TYPE"]) + if edition: + click.echo(f"Mergin Maps edition: {edition}") click.echo(f"Mergin Maps version: {app.config['VERSION']}") base_url = app.config["MERGIN_BASE_URL"] @@ -148,7 +161,7 @@ def init(email: str, recreate: bool): _init_db() _echo_title("Creating admin user. Copy generated password.") - username = "admin" + username = User.generate_username(email) password_chars = string.ascii_letters + string.digits password = "".join(random.choice(password_chars) for i in range(12)) user = User(username=username, passwd=password, email=email, is_admin=True) @@ -159,9 +172,9 @@ def init(email: str, recreate: bool): click.secho( "Admin user created. Please save generated password.", fg="green" ) + click.secho(f"Email: {email}") click.secho(f"Username: {username}") click.secho(f"Password: {password}") - click.secho(f"Email: {email}") _check_server() _send_email(email) _send_statistics() From ac51ee510ae3175a9fa2a223e6934d03f479f168 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 10 Feb 2025 10:45:28 +0100 Subject: [PATCH 3/4] Abort if recreate prompt is "no" - use default value CONTACT_EMAIL for init --- server/mergin/commands.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/server/mergin/commands.py b/server/mergin/commands.py index b85833f0..556c0e42 100644 --- a/server/mergin/commands.py +++ b/server/mergin/commands.py @@ -103,6 +103,14 @@ def _check_server(): # pylint: disable=W0612 else: click.secho(f"Base URL of server is {base_url}", fg="green") + contact_email = app.config["CONTACT_EMAIL"] + if not contact_email: + _echo_error( + "No contact email set. Please set CONTACT_EMAIL environment variable", + ) + else: + click.secho(f"Base URL of server is {base_url}", fg="green") + tables = db.engine.table_names() if not tables: _echo_error("Database not initialized. Run flask init-db command") @@ -143,7 +151,7 @@ def init_db(): _init_db() @app.cli.command() - @click.option("--email", "-e", required=True) + @click.option("--email", "-e", required=True, envvar="CONTACT_EMAIL") @click.option( "--recreate", "-r", @@ -156,6 +164,13 @@ def init(email: str, recreate: bool): from .auth.models import User + if recreate: + click.confirm( + "Are you sure you want to recreate database and admin user? This will remove all data!", + default=False, + abort=True, + ) + tables = db.engine.table_names() if not tables or recreate: _init_db() From eda20ee6883dd43f4100f19f734c58c5efcaad79 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 10 Feb 2025 13:00:41 +0100 Subject: [PATCH 4/4] Enhance confirm comand --- server/mergin/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/mergin/commands.py b/server/mergin/commands.py index 556c0e42..47c0600f 100644 --- a/server/mergin/commands.py +++ b/server/mergin/commands.py @@ -164,14 +164,14 @@ def init(email: str, recreate: bool): from .auth.models import User - if recreate: + tables = db.engine.table_names() + if recreate and tables: click.confirm( "Are you sure you want to recreate database and admin user? This will remove all data!", default=False, abort=True, ) - tables = db.engine.table_names() if not tables or recreate: _init_db()