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/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..47c0600f 100644 --- a/server/mergin/commands.py +++ b/server/mergin/commands.py @@ -1,95 +1,211 @@ 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, 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 + + 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") + 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"] 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.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.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, envvar="CONTACT_EMAIL") + @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 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, ) - else: - click.echo("Celery running properly") + + if not tables or recreate: + _init_db() + + _echo_title("Creating admin user. Copy generated password.") + 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) + user.profile = UserProfile() + user.active = True + db.session.add(user) + db.session.commit() + 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}") + _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()