diff options
| -rw-r--r-- | CHANGELOG.rst | 5 | ||||
| -rw-r--r-- | doc/administration.rst | 1 | ||||
| -rw-r--r-- | doc/administration/maintenance-mode.rst | 70 | ||||
| -rw-r--r-- | fietsboek/__init__.py | 31 | ||||
| -rw-r--r-- | fietsboek/data.py | 14 | ||||
| -rw-r--r-- | fietsboek/scripts/fietscron.py | 9 | ||||
| -rw-r--r-- | fietsboek/scripts/fietsctl.py | 38 | 
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  | 
