aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst5
-rw-r--r--doc/administration.rst1
-rw-r--r--doc/administration/maintenance-mode.rst70
-rw-r--r--fietsboek/__init__.py31
-rw-r--r--fietsboek/data.py14
-rw-r--r--fietsboek/scripts/fietscron.py9
-rw-r--r--fietsboek/scripts/fietsctl.py38
7 files changed, 165 insertions, 3 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dd4bfab..8b08927 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,6 +4,11 @@ Changelog
Unreleased
----------
+Added
+^^^^^
+
+- The maintenance mode.
+
0.5.0 - 2023-01-12
------------------
diff --git a/doc/administration.rst b/doc/administration.rst
index 706a4cb..6cf0f52 100644
--- a/doc/administration.rst
+++ b/doc/administration.rst
@@ -11,6 +11,7 @@ Administration Guide
administration/backup
administration/cronjobs
administration/custom-pages
+ administration/maintenance-mode
This guide contains information pertaining to the administration of a Fietsboek
instance. This includes the installation, updating, configuration, and backing
diff --git a/doc/administration/maintenance-mode.rst b/doc/administration/maintenance-mode.rst
new file mode 100644
index 0000000..d02e27a
--- /dev/null
+++ b/doc/administration/maintenance-mode.rst
@@ -0,0 +1,70 @@
+Maintenance Mode
+================
+
+Sometimes, it can be useful to deactivate your fietsboek instance temporarily,
+for example when you want to apply updates, or you need to otherwise fiddle
+with the data. Deactivating your instance avoids any "race issues" or accesses
+to the database and such, which might be in inconsistent states.
+
+To help with this situation, fietsboek provides a *maintenance mode*. This
+allows you to deactivate fietsboek without having to fiddle with your webserver
+settings.
+
+In the maintenance mode, fietsboek will reply to any HTTP request with a ``503
+Service Unavailable`` response, and won't process the requests further.
+
+.. note::
+
+ The maintenance mode is implemented early in fietsboek, however if you have
+ custom HTTP proxies or other WSGI middleware between fietsboek and the
+ outside world, the maintenance mode will not affect those.
+
+ In those cases, it might be better to deactivate the whole pipeline,
+ depending on the setup and the middleware used — at least if those also
+ access the database.
+
+In addition to not replying to HTTP requests, ``fietscron`` will also respect
+the maintenance mode and do nothing. This is to avoid accidental interference
+with long tasks like updating, such that a cronjob doesn't access data in an
+invalid state.
+
+Unlike ``fietscron`` and the HTTP requests, ``fietsctl`` and ``fietsupdate``
+will **continue to work**.
+
+Controlling the Maintenance Mode
+--------------------------------
+
+You can enable and disable the maintenance mode using the ``fietsctl`` script.
+To enable the mode, simply pass a reason (that will be shown to users) to
+``fietsctl maintenance-mode``::
+
+ fietsctl -c production.ini maintenance-mode "Updating the instance"
+
+Similarly, you can disable the maintenance mode by passing the ``--disable``
+flag::
+
+ fietsctl -c production.ini maintenance-mode --disable
+
+To check the current status of the maintenance mode, call the subcommand
+without extra arguments::
+
+ fietsctl -c production.ini maintenance-mode
+
+Manually Intervening with Maintenance
+-------------------------------------
+
+If you cannot (or don't want to) use the ``fietsctl maintenance-mode`` command,
+you can also manually control the maintenance mode: The :file:`MAINTENANCE`
+file in the data directory controls whether the mode is enabled or disabled.
+
+To enable the maintenance mode, it is enough to ensure that the file exists::
+
+ touch data-dir/MAINTENANCE
+
+The reason for the maintenance mode is saved in the file content::
+
+ echo "Updating the instance" > data-dir/MAINTENANCE
+
+To disable the maintenance mode, remove the file again::
+
+ rm data-dir/MAINTENANCE
diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py
index 423e901..10ba164 100644
--- a/fietsboek/__init__.py
+++ b/fietsboek/__init__.py
@@ -14,14 +14,17 @@ Content
-------
"""
from pathlib import Path
-from typing import Optional
+from typing import Callable, Optional
import importlib_metadata
import redis
from pyramid.config import Configurator
from pyramid.csrf import CookieCSRFStoragePolicy
+from pyramid.httpexceptions import HTTPServiceUnavailable
from pyramid.i18n import default_locale_negotiator
+from pyramid.registry import Registry
from pyramid.request import Request
+from pyramid.response import Response
from pyramid.session import SignedCookieSessionFactory
from . import config as mod_config
@@ -59,6 +62,31 @@ def locale_negotiator(request: Request) -> Optional[str]:
return negotiated
+def maintenance_mode(
+ handler: Callable[[Request], Response], _registry: Registry
+) -> Callable[[Request], Response]:
+ """A Pyramid Tween that handles the maintenance mode.
+
+ Note that we do this as a tween to ensure we check for the maintenance mode
+ as early as possible. This avoids hitting the DB, which might be in an
+ inconsistent state.
+
+ :param handler: The next handler in the tween/view chain.
+ :param registry: The application registry.
+ """
+
+ def tween(request: Request) -> Response:
+ maintenance = request.data_manager.maintenance_mode()
+ if maintenance is None:
+ return handler(request)
+
+ return HTTPServiceUnavailable(
+ f"This fietsboek is currently in maintenance mode: {maintenance}",
+ )
+
+ return tween
+
+
def main(_global_config, **settings):
"""This function returns a Pyramid WSGI application."""
parsed_config = mod_config.parse(settings)
@@ -90,6 +118,7 @@ def main(_global_config, **settings):
config.include("pyramid_jinja2")
config.include(".routes")
config.include(".models")
+ config.add_tween(".maintenance_mode")
config.scan()
config.add_translation_dirs("fietsboek:locale/")
for pack in parsed_config.language_packs:
diff --git a/fietsboek/data.py b/fietsboek/data.py
index f32e24f..96acaec 100644
--- a/fietsboek/data.py
+++ b/fietsboek/data.py
@@ -54,6 +54,20 @@ class DataManager:
def _track_data_dir(self, track_id):
return self.data_dir / "tracks" / str(track_id)
+ def maintenance_mode(self) -> Optional[str]:
+ """Checks whether the maintenance mode is enabled.
+
+ If maintenance mode is enabled, returns the reason given.
+
+ If maintenance mode is disabled, returns ``None``.
+
+ :return: The maintenance mode state.
+ """
+ try:
+ return (self.data_dir / "MAINTENANCE").read_text()
+ except FileNotFoundError:
+ return None
+
def initialize(self, track_id: int) -> "TrackDataDir":
"""Creates the data directory for a track.
diff --git a/fietsboek/scripts/fietscron.py b/fietsboek/scripts/fietscron.py
index 3b22ac2..a142f39 100644
--- a/fietsboek/scripts/fietscron.py
+++ b/fietsboek/scripts/fietscron.py
@@ -37,9 +37,14 @@ def cli(config):
settings = pyramid.paster.get_appsettings(config)
config = mod_config.parse(settings)
- engine = create_engine(config.sqlalchemy_url)
-
+ # Do this early to reduce the chances of "accidentally" hitting the
+ # database when maintenance mode is turned on.
data_manager = DataManager(config.data_dir)
+ if data_manager.maintenance_mode() is not None:
+ LOGGER.info("Skipping cronjob tasks due to maintenance mode")
+ return
+
+ engine = create_engine(config.sqlalchemy_url)
LOGGER.debug("Starting maintenance tasks")
remove_old_uploads(engine)
diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py
index 400fa9b..a1f14f7 100644
--- a/fietsboek/scripts/fietsctl.py
+++ b/fietsboek/scripts/fietsctl.py
@@ -137,6 +137,26 @@ def cmd_passwd(env, args):
return EXIT_OKAY
+def cmd_maintenance_mode(env, args):
+ """Check the status of the maintenance mode, or activate or deactive it.
+
+ With neither `--disable` nor `reason` given, just checks the state of the
+ maintenance mode.
+ """
+ data_manager = env["request"].data_manager
+ if args.reason is None and not args.disable:
+ maintenance = data_manager.maintenance_mode()
+ if maintenance is None:
+ print("Maintenance mode is disabled")
+ else:
+ print(f"Maintenance mode is enabled: {maintenance}")
+ elif args.disable:
+ (data_manager.data_dir / "MAINTENANCE").unlink()
+ else:
+ (data_manager.data_dir / "MAINTENANCE").write_text(args.reason)
+ return EXIT_OKAY
+
+
def cmd_version():
"""Show the installed fietsboek version."""
name = __name__.split(".", 1)[0]
@@ -247,6 +267,24 @@ def parse_args(argv):
)
p_passwd.set_defaults(func=cmd_passwd)
+ p_maintenance = subparsers.add_parser(
+ "maintenance-mode",
+ help="enable or disable the maintenance mode",
+ description=cmd_maintenance_mode.__doc__,
+ )
+ group = p_maintenance.add_mutually_exclusive_group(required=False)
+ group.add_argument(
+ "--disable",
+ action="store_true",
+ help="disable the maintenance mode",
+ )
+ group.add_argument(
+ "reason",
+ nargs="?",
+ help="reason for the maintenance",
+ )
+ p_maintenance.set_defaults(func=cmd_maintenance_mode)
+
return parser.parse_args(argv[1:]), parser