From 6e839704e5827054da56e4c3a9d2d31462893bf8 Mon Sep 17 00:00:00 2001 From: Quentin Date: Mon, 11 Apr 2016 17:34:35 +0200 Subject: [PATCH 1/8] Handle maintenance mode through a cache key Useful for global maintenance over a cluster of front servers (sharing the same cache backend) --- maintenancemode/conf.py | 4 ++ .../management/commands/maintenance.py | 20 ++++++- maintenancemode/middleware.py | 33 ++++++++-- maintenancemode/utils.py | 60 ++++++++++++++++--- maintenancemode/views.py | 1 + 5 files changed, 103 insertions(+), 15 deletions(-) diff --git a/maintenancemode/conf.py b/maintenancemode/conf.py index c59b0f7..910a243 100644 --- a/maintenancemode/conf.py +++ b/maintenancemode/conf.py @@ -11,6 +11,10 @@ class MaintenanceSettings(AppConf): LOCKFILE_PATH = os.path.join( os.path.abspath(os.path.dirname(__file__)), 'maintenance.lock') MODE = False + LOCKING_METHOD = 'file' + CACHE_KEY = "DJANGO_MAINTENANCE_MODE_ON" + CACHE_TTL = 60 * 60 * 24 + CACHE_BACKEND = "default" class Meta: prefix = 'maintenance' diff --git a/maintenancemode/management/commands/maintenance.py b/maintenancemode/management/commands/maintenance.py index 67117c1..1adaaf3 100644 --- a/maintenancemode/management/commands/maintenance.py +++ b/maintenancemode/management/commands/maintenance.py @@ -16,17 +16,33 @@ def handle(self, *args, **options): verbosity = int(options.get('verbosity')) if command is not None: - if command.lower() in ('on', 'activate'): + try: + maintenance_duration = int(command) + except ValueError: + maintenance_duration = None + if maintenance_duration: + maintenance.activate(maintenance_duration) + if verbosity > 0: + self.stdout.write( + "Maintenance mode was activated " + "succesfully for %s seconds" % maintenance_duration + ) + return + elif command.lower() in ('on', 'activate'): maintenance.activate() if verbosity > 0: self.stdout.write( "Maintenance mode was activated succesfully") + return elif command.lower() in ('off', 'deactivate'): maintenance.deactivate() if verbosity > 0: self.stdout.write( "Maintenance mode was deactivated succesfully") + return if command not in self.opts: raise CommandError( - "Allowed commands are: %s" % '|'.join(self.opts)) + "Allowed commands are: %s or maintenance duration in seconds" + % '|'.join(self.opts) + ) diff --git a/maintenancemode/middleware.py b/maintenancemode/middleware.py index dd5234a..03f1dc7 100644 --- a/maintenancemode/middleware.py +++ b/maintenancemode/middleware.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- import re -import django +from datetime import datetime +from time import mktime +from wsgiref.handlers import format_date_time +import django from django.conf import urls from django.core import urlresolvers -from .conf import settings from . import utils as maintenance +from .conf import settings urls.handler503 = 'maintenancemode.views.temporary_unavailable' urls.__all__.append('handler503') @@ -19,7 +22,20 @@ class MaintenanceModeMiddleware(object): def process_request(self, request): # Allow access if middleware is not activated - if not (settings.MAINTENANCE_MODE or maintenance.status()): + value = settings.MAINTENANCE_MODE or maintenance.status() + if not value: + return None + + if isinstance(value, datetime): + retry_after = value + else: + retry_after = None + + # used by template + request.retry_after = retry_after + + if retry_after and datetime.now() > retry_after: + # maintenance ended return None INTERNAL_IPS = maintenance.IPList(settings.INTERNAL_IPS) @@ -52,4 +68,13 @@ def process_request(self, request): else: callback, param_dict = resolver.resolve_error_handler('503') - return callback(request, **param_dict) + print callback + + response = callback(request, **param_dict) + + if retry_after: + response["Retry-After"] = format_date_time( + mktime(retry_after.timetuple()) + ) + + return response diff --git a/maintenancemode/utils.py b/maintenancemode/utils.py index d4b7253..4a58a70 100644 --- a/maintenancemode/utils.py +++ b/maintenancemode/utils.py @@ -2,8 +2,15 @@ import os +from dateutil.relativedelta import relativedelta +from datetime import datetime +from django.core.cache import get_cache +from django.core.exceptions import ImproperlyConfigured + from .conf import settings +LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD + class IPList(list): """Stolen from https://djangosnippets.org/snippets/1362/""" @@ -26,18 +33,53 @@ def __contains__(self, ip): return False -def activate(): - try: - open(settings.MAINTENANCE_LOCKFILE_PATH, 'ab', 0).close() - except OSError: - pass # shit happens +def activate(maintenance_duration=None): + if maintenance_duration: + cache_value = ( + datetime.now() + relativedelta(seconds=maintenance_duration) + ) + else: + cache_value = True + if LOCKING_METHOD == "file": + try: + open(settings.MAINTENANCE_LOCKFILE_PATH, 'ab', 0).close() + except OSError: + pass # shit happens + elif LOCKING_METHOD == "cache": + cache = get_cache(settings.MAINTENANCE_CACHE_BACKEND) + cache.set( + settings.MAINTENANCE_CACHE_KEY, + cache_value, + settings.MAINTENANCE_CACHE_TTL + ) + else: + raise ImproperlyConfigured( + "Unknown locking method %s" % LOCKING_METHOD) def deactivate(): - if os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH): - os.remove(settings.MAINTENANCE_LOCKFILE_PATH) + LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD + if LOCKING_METHOD == "file": + if os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH): + os.remove(settings.MAINTENANCE_LOCKFILE_PATH) + elif LOCKING_METHOD == "cache": + cache = get_cache(settings.MAINTENANCE_CACHE_BACKEND) + cache.delete( + settings.MAINTENANCE_CACHE_KEY + ) + else: + raise ImproperlyConfigured( + "Unknown locking method %s" % LOCKING_METHOD) def status(): - return settings.MAINTENANCE_MODE or os.path.isfile( - settings.MAINTENANCE_LOCKFILE_PATH) + LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD + if LOCKING_METHOD == "file": + return settings.MAINTENANCE_MODE or os.path.isfile( + settings.MAINTENANCE_LOCKFILE_PATH) + elif LOCKING_METHOD == "cache": + cache = get_cache(settings.MAINTENANCE_CACHE_BACKEND) + return cache.get(settings.MAINTENANCE_CACHE_KEY) + else: + raise ImproperlyConfigured( + "Unknown locking method %s" % LOCKING_METHOD) diff --git a/maintenancemode/views.py b/maintenancemode/views.py index a45e873..ef4baa0 100644 --- a/maintenancemode/views.py +++ b/maintenancemode/views.py @@ -30,6 +30,7 @@ def temporary_unavailable(request, template_name='503.html'): """ context = { 'request_path': request.path, + 'request': request } return http.HttpResponseTemporaryUnavailable( render_to_string(template_name, context)) From e237128f3f7b2ec6f0406e688bd54674c857fed1 Mon Sep 17 00:00:00 2001 From: Quentin Date: Tue, 12 Apr 2016 11:42:50 +0200 Subject: [PATCH 2/8] Implement the possibility to wait for the end of the maintenance if waiting time < MAINTENANCE_MAX_WAIT_FOR_END --- maintenancemode/conf.py | 1 + maintenancemode/middleware.py | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/maintenancemode/conf.py b/maintenancemode/conf.py index 910a243..679208d 100644 --- a/maintenancemode/conf.py +++ b/maintenancemode/conf.py @@ -15,6 +15,7 @@ class MaintenanceSettings(AppConf): CACHE_KEY = "DJANGO_MAINTENANCE_MODE_ON" CACHE_TTL = 60 * 60 * 24 CACHE_BACKEND = "default" + MAX_WAIT_FOR_END = 0 class Meta: prefix = 'maintenance' diff --git a/maintenancemode/middleware.py b/maintenancemode/middleware.py index 03f1dc7..574d631 100644 --- a/maintenancemode/middleware.py +++ b/maintenancemode/middleware.py @@ -4,6 +4,8 @@ from datetime import datetime from time import mktime from wsgiref.handlers import format_date_time +from time import sleep +import logging import django from django.conf import urls @@ -17,9 +19,28 @@ IGNORE_URLS = tuple([re.compile(u) for u in settings.MAINTENANCE_IGNORE_URLS]) +MAX_WAIT_FOR_END = settings.MAINTENANCE_MAX_WAIT_FOR_END + +logger = logging.getLogger(__name__) class MaintenanceModeMiddleware(object): + def cond_wait_for_end_of_maintenance(self, request, retry_after): + """ + Wait for remaining maintenance time if waiting time is + less than MAX_WAIT_FOR_END + """ + ends_in = (retry_after - datetime.now()).total_seconds() + max_wait = MAX_WAIT_FOR_END + if ends_in > 0 and ends_in < max_wait: + logger.info( + u"[%s] waiting for %ss" % ( + request.path, ends_in + ) + ) + sleep(ends_in) + return + def process_request(self, request): # Allow access if middleware is not activated value = settings.MAINTENANCE_MODE or maintenance.status() @@ -34,6 +55,9 @@ def process_request(self, request): # used by template request.retry_after = retry_after + if retry_after: + self.cond_wait_for_end_of_maintenance(request, retry_after) + if retry_after and datetime.now() > retry_after: # maintenance ended return None @@ -68,8 +92,6 @@ def process_request(self, request): else: callback, param_dict = resolver.resolve_error_handler('503') - print callback - response = callback(request, **param_dict) if retry_after: From 34b4493bec6de5b04f6d45904e7fa6342fe56aa1 Mon Sep 17 00:00:00 2001 From: Maxime MENAGER Date: Wed, 22 Jun 2016 14:58:09 +0200 Subject: [PATCH 3/8] Remove django dependency Theses dependencies were forcing django to its last version --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index fc5c2a0..ed77907 100644 --- a/setup.py +++ b/setup.py @@ -36,12 +36,10 @@ def find_version(*parts): license='BSD License', install_requires=[ - 'django>=1.4.2', 'django-appconf', 'ipy', ], requires=[ - 'Django (>=1.4.2)', ], description="django-maintenancemode allows you to temporary shutdown your site for maintenance work", From 6dfc6619fcfc9bc113f45c625dcd0a6dde36b296 Mon Sep 17 00:00:00 2001 From: Hugo Defrance Date: Sat, 7 Jan 2017 23:50:44 +0100 Subject: [PATCH 4/8] Added default setting --- maintenancemode/middleware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maintenancemode/middleware.py b/maintenancemode/middleware.py index 574d631..2192cfb 100644 --- a/maintenancemode/middleware.py +++ b/maintenancemode/middleware.py @@ -17,9 +17,9 @@ urls.handler503 = 'maintenancemode.views.temporary_unavailable' urls.__all__.append('handler503') -IGNORE_URLS = tuple([re.compile(u) for u in settings.MAINTENANCE_IGNORE_URLS]) +IGNORE_URLS = tuple([re.compile(u) for u in getattr(settings, 'MAINTENANCE_IGNORE_URLS', [])]) -MAX_WAIT_FOR_END = settings.MAINTENANCE_MAX_WAIT_FOR_END +MAX_WAIT_FOR_END = getattr(settings, 'MAINTENANCE_MAX_WAIT_FOR_END', 60) logger = logging.getLogger(__name__) @@ -43,7 +43,7 @@ def cond_wait_for_end_of_maintenance(self, request, retry_after): def process_request(self, request): # Allow access if middleware is not activated - value = settings.MAINTENANCE_MODE or maintenance.status() + value = getattr(settings, 'MAINTENANCE_MODE', False) or maintenance.status() if not value: return None From 19cd9bea345651fe121d2a3abd9bcda93f51f5b7 Mon Sep 17 00:00:00 2001 From: Hugo Defrance Date: Sun, 8 Jan 2017 00:24:14 +0100 Subject: [PATCH 5/8] Not checking status anymore if maintenance is disabled --- maintenancemode/middleware.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/maintenancemode/middleware.py b/maintenancemode/middleware.py index 2192cfb..199e783 100644 --- a/maintenancemode/middleware.py +++ b/maintenancemode/middleware.py @@ -43,8 +43,9 @@ def cond_wait_for_end_of_maintenance(self, request, retry_after): def process_request(self, request): # Allow access if middleware is not activated - value = getattr(settings, 'MAINTENANCE_MODE', False) or maintenance.status() - if not value: + if not getattr(settings, 'MAINTENANCE_MODE', False): + return None + if not maintenance.status() return None if isinstance(value, datetime): From 2536780abcf5881b58eb7ba6b7573d6727c9419b Mon Sep 17 00:00:00 2001 From: Hugo Defrance Date: Sun, 8 Jan 2017 00:30:46 +0100 Subject: [PATCH 6/8] Fixed typo --- maintenancemode/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maintenancemode/middleware.py b/maintenancemode/middleware.py index 199e783..ab8d4a1 100644 --- a/maintenancemode/middleware.py +++ b/maintenancemode/middleware.py @@ -45,7 +45,7 @@ def process_request(self, request): # Allow access if middleware is not activated if not getattr(settings, 'MAINTENANCE_MODE', False): return None - if not maintenance.status() + if not maintenance.status(): return None if isinstance(value, datetime): From 0d5c9e2c5d8c371aa526ed86cb12221a57b4c141 Mon Sep 17 00:00:00 2001 From: Quentin Date: Wed, 18 Jan 2017 17:17:24 +0100 Subject: [PATCH 7/8] Initialize cache backend only once --- maintenancemode/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/maintenancemode/utils.py b/maintenancemode/utils.py index 4a58a70..fc9286e 100644 --- a/maintenancemode/utils.py +++ b/maintenancemode/utils.py @@ -11,6 +11,10 @@ LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD +CACHE_BACKEND = settings.MAINTENANCE_CACHE_BACKEND + +cache = get_cache(CACHE_BACKEND) + class IPList(list): """Stolen from https://djangosnippets.org/snippets/1362/""" @@ -46,7 +50,6 @@ def activate(maintenance_duration=None): except OSError: pass # shit happens elif LOCKING_METHOD == "cache": - cache = get_cache(settings.MAINTENANCE_CACHE_BACKEND) cache.set( settings.MAINTENANCE_CACHE_KEY, cache_value, @@ -63,7 +66,6 @@ def deactivate(): if os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH): os.remove(settings.MAINTENANCE_LOCKFILE_PATH) elif LOCKING_METHOD == "cache": - cache = get_cache(settings.MAINTENANCE_CACHE_BACKEND) cache.delete( settings.MAINTENANCE_CACHE_KEY ) @@ -78,7 +80,6 @@ def status(): return settings.MAINTENANCE_MODE or os.path.isfile( settings.MAINTENANCE_LOCKFILE_PATH) elif LOCKING_METHOD == "cache": - cache = get_cache(settings.MAINTENANCE_CACHE_BACKEND) return cache.get(settings.MAINTENANCE_CACHE_KEY) else: raise ImproperlyConfigured( From ffbdd0645e31bda317207d87229720db63338123 Mon Sep 17 00:00:00 2001 From: Quentin Date: Thu, 27 Apr 2017 11:15:36 +0200 Subject: [PATCH 8/8] Fix 'maintenance.status()' check --- maintenancemode/middleware.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/maintenancemode/middleware.py b/maintenancemode/middleware.py index ab8d4a1..9f07afd 100644 --- a/maintenancemode/middleware.py +++ b/maintenancemode/middleware.py @@ -42,10 +42,13 @@ def cond_wait_for_end_of_maintenance(self, request, retry_after): return def process_request(self, request): - # Allow access if middleware is not activated - if not getattr(settings, 'MAINTENANCE_MODE', False): + if not hasattr(settings, 'MAINTENANCE_MODE'): + # package not setup return None - if not maintenance.status(): + + value = getattr(settings, 'MAINTENANCE_MODE', False) or maintenance.status() + if not value: + # maintenance not active return None if isinstance(value, datetime):