From ad37c5fd7199a82a0c0e87cdb2f4e9326ceaa0e9 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Mon, 31 Aug 2015 11:58:59 +0000 Subject: [PATCH] Added History app and setuplog --- INSTALL.md | 5 + TODO.md | 10 +- orchestra/admin/__init__.py | 12 +- orchestra/contrib/history/__init__.py | 1 + orchestra/contrib/history/admin.py | 94 +++ orchestra/contrib/history/apps.py | 19 + .../admin/admin/logentry/change_form.html | 22 + .../templates/admin/object_history.html | 43 ++ orchestra/contrib/mailboxes/backends.py | 10 +- orchestra/contrib/mailer/admin.py | 1 + orchestra/contrib/mailer/engine.py | 30 +- orchestra/contrib/orchestration/admin.py | 1 + .../management/commands/orchestrate.py | 12 +- orchestra/contrib/systemusers/admin.py | 2 +- orchestra/management/commands/setuplog.py | 82 +++ orchestra/management/commands/setupnginx.py | 15 +- orchestra/static/orchestra/icons/History.png | Bin 0 -> 2675 bytes orchestra/static/orchestra/icons/History.svg | 554 ++++++++++++++++++ orchestra/utils/sys.py | 11 + scripts/container/deploy.sh | 3 +- 20 files changed, 876 insertions(+), 51 deletions(-) create mode 100644 orchestra/contrib/history/__init__.py create mode 100644 orchestra/contrib/history/admin.py create mode 100644 orchestra/contrib/history/apps.py create mode 100644 orchestra/contrib/history/templates/admin/admin/logentry/change_form.html create mode 100644 orchestra/contrib/history/templates/admin/object_history.html create mode 100644 orchestra/management/commands/setuplog.py create mode 100644 orchestra/static/orchestra/icons/History.png create mode 100644 orchestra/static/orchestra/icons/History.svg diff --git a/INSTALL.md b/INSTALL.md index f501d473..7ef1fe56 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -61,6 +61,10 @@ Django-orchestra can be installed on any Linux system, however it is **strongly sudo python3 manage.py setupcelery --username orchestra ``` +7. (Optional) Configure logging + ```bash + sudo python3 manage.py setuplog + ``` 8. Configure the web server: ```bash @@ -69,6 +73,7 @@ Django-orchestra can be installed on any Linux system, however it is **strongly sudo python3 manage.py setupnginx --user orchestra ``` + 9. Start all services: ```bash sudo python manage.py startservices diff --git a/TODO.md b/TODO.md index bada898b..70b24f3c 100644 --- a/TODO.md +++ b/TODO.md @@ -15,8 +15,6 @@ * help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla ) -* create log file at /var/log/orchestra.log and rotate - * order.register_at @property def register_on(self): @@ -363,8 +361,6 @@ serailzer self.instance on create. * check certificate: websites directive ssl + domains search on miscellaneous -# ValueError: Unable to configure handler 'file': [Errno 13] Permission denied: '/home/orchestra/panel/orchestra.log' - # billing invoice link on related invoices not overflow nginx GET vars * backendLog store method and language... and use it for display_script with correct lexer @@ -410,3 +406,9 @@ Case # messages SMTP errors: temporary->deferre else Failed # Don't enforce one contact per account? remove account.email in favour of contacts? + +#change class LogEntry(models.Model): + action_time = models.DateTimeField(_('action time'), auto_now=True) to auto_now_add + +# Model operations on Manager instead of model method +# Mailer: mark as sent diff --git a/orchestra/admin/__init__.py b/orchestra/admin/__init__.py index 75b17e58..b61f7980 100644 --- a/orchestra/admin/__init__.py +++ b/orchestra/admin/__init__.py @@ -1,6 +1,6 @@ from functools import update_wrapper -from django.contrib.admin import site +from django.contrib import admin from .dashboard import * from .options import * @@ -12,15 +12,15 @@ urls = [] def register_url(pattern, view, name=""): global urls urls.append((pattern, view, name)) -site.register_url = register_url +admin.site.register_url = register_url -site_get_urls = site.get_urls +site_get_urls = admin.site.get_urls def get_urls(): def wrap(view, cacheable=False): def wrapper(*args, **kwargs): - return site.admin_view(view, cacheable)(*args, **kwargs) - wrapper.admin_site = site + return admin.site.admin_view(view, cacheable)(*args, **kwargs) + wrapper.admin_site = admin.site return update_wrapper(wrapper, view) global urls extra_patterns = [] @@ -29,4 +29,4 @@ def get_urls(): url(pattern, wrap(view), name=name) ) return site_get_urls() + extra_patterns -site.get_urls = get_urls +admin.site.get_urls = get_urls diff --git a/orchestra/contrib/history/__init__.py b/orchestra/contrib/history/__init__.py new file mode 100644 index 00000000..d39042d6 --- /dev/null +++ b/orchestra/contrib/history/__init__.py @@ -0,0 +1 @@ +default_app_config = 'orchestra.contrib.history.apps.HistoryConfig' diff --git a/orchestra/contrib/history/admin.py b/orchestra/contrib/history/admin.py new file mode 100644 index 00000000..a6d9e738 --- /dev/null +++ b/orchestra/contrib/history/admin.py @@ -0,0 +1,94 @@ +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse, NoReverseMatch +from django.contrib.admin.templatetags.admin_urls import add_preserved_filters +from django.http import HttpResponseRedirect +from django.contrib.admin.utils import unquote + +from orchestra.admin.utils import admin_link, admin_date + + +class LogEntryAdmin(admin.ModelAdmin): + list_display = ( + '__str__', 'display_action_time', 'user_link', + ) + list_filter = ('action_flag', 'content_type',) + date_hierarchy = 'action_time' + search_fields = ('object_repr', 'change_message') + fields = ( + 'user_link', 'content_object_link', 'display_action_time', 'display_action', 'change_message' + ) + readonly_fields = ( + 'user_link', 'content_object_link', 'display_action_time', 'display_action', + ) + actions = None + + user_link = admin_link('user') + display_action_time = admin_date('action_time', short_description=_("Time")) + + def display_action(self, log): + if log.is_addition(): + return _("Added") + elif log.is_change(): + return _("Changed") + return _("Deleted") + display_action.short_description = _("Action") + display_action.admin_order_field = 'action_flag' + + def content_object_link(self, log): + ct = log.content_type + try: + url = reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(log.object_id,)) + except NoReverseMatch: + return log.object_repr + return '%s' % (url, log.object_repr) + content_object_link.short_description = _("Content object") + content_object_link.admin_order_field = 'object_repr' + content_object_link.allow_tags = True + + def changeform_view(self, request, object_id=None, form_url='', extra_context=None): + """ Add rel_opts and object to context """ + context = {} + if 'edit' in request.GET.urlencode(): + obj = self.get_object(request, unquote(object_id)) + context = { + 'rel_opts': obj.content_type.model_class()._meta, + 'object': obj, + } + context.update(extra_context or {}) + return super(LogEntryAdmin, self).changeform_view(request, object_id, form_url, extra_context=context) + + def response_change(self, request, obj): + """ save and continue preserve edit query string """ + response = super(LogEntryAdmin, self).response_change(request, obj) + if 'edit' in request.GET.urlencode() and 'edit' not in response.url: + return HttpResponseRedirect(response.url + '?edit=True') + return response + + def response_post_save_change(self, request, obj): + """ save redirect to object history """ + if 'edit' in request.GET.urlencode(): + opts = obj.content_type.model_class()._meta + post_url = reverse('admin:%s_%s_history' % (opts.app_label, opts.model_name), args=(obj.object_id,)) + preserved_filters = self.get_preserved_filters(request) + post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url) + return HttpResponseRedirect(post_url) + return super(LogEntryAdmin, self).response_post_save_change(request, obj) + + def has_add_permission(self, *args, **kwargs): + return False + + def has_delete_permission(self, *args, **kwargs): + return False + + def log_addition(self, *args, **kwargs): + pass + + def log_change(self, *args, **kwargs): + pass + + def log_deletion(self, *args, **kwargs): + pass + + +admin.site.register(admin.models.LogEntry, LogEntryAdmin) diff --git a/orchestra/contrib/history/apps.py b/orchestra/contrib/history/apps.py new file mode 100644 index 00000000..04d2233c --- /dev/null +++ b/orchestra/contrib/history/apps.py @@ -0,0 +1,19 @@ +from django import db +from django.apps import AppConfig + +from orchestra.core import administration + + +class HistoryConfig(AppConfig): + name = 'orchestra.contrib.history' + verbose_name = 'History' + + def ready(self): + from django.contrib.admin.models import LogEntry + administration.register( + LogEntry, verbose_name='History', verbose_name_plural='History', icon='History.png' + ) + # prevent loosing creation time on log entry edition + action_time = LogEntry._meta.get_field_by_name('action_time')[0] + action_time.auto_now = False + action_time.auto_now_add = True diff --git a/orchestra/contrib/history/templates/admin/admin/logentry/change_form.html b/orchestra/contrib/history/templates/admin/admin/logentry/change_form.html new file mode 100644 index 00000000..95c693c0 --- /dev/null +++ b/orchestra/contrib/history/templates/admin/admin/logentry/change_form.html @@ -0,0 +1,22 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} +{% endblock %} + + +{% block breadcrumbs %} +{% if rel_opts %} + +{% else %} +{{ block.super }} +{% endif %} +{% endblock %} + diff --git a/orchestra/contrib/history/templates/admin/object_history.html b/orchestra/contrib/history/templates/admin/object_history.html new file mode 100644 index 00000000..26eed2d0 --- /dev/null +++ b/orchestra/contrib/history/templates/admin/object_history.html @@ -0,0 +1,43 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+
+ +{% if action_list %} + + + + + + + + + + {% for action in action_list %} + + + + + + {% endfor %} + +
{% trans 'Date/time' %}{% trans 'User' %}{% trans 'Action' %}
{{ action.action_time|date:"DATETIME_FORMAT" }}{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}{% if action.is_addition and not action.change_message %}{% trans 'Added' %}{% else %}{{ action.change_message }}{% endif %}
+{% else %} +

{% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}

+{% endif %} +
+
+{% endblock %} + diff --git a/orchestra/contrib/mailboxes/backends.py b/orchestra/contrib/mailboxes/backends.py index c9018973..54c0c2c3 100644 --- a/orchestra/contrib/mailboxes/backends.py +++ b/orchestra/contrib/mailboxes/backends.py @@ -24,12 +24,12 @@ class SieveFilteringMixin(object): context['box'] = box self.append(textwrap.dedent(""" # Create %(box)s mailbox - su $user --shell /bin/bash << 'EOF' + su %(user)s --shell /bin/bash << 'EOF' mkdir -p "%(maildir)s/.%(box)s" EOF if [[ ! $(grep '%(box)s' %(maildir)s/subscriptions) ]]; then echo '%(box)s' >> %(maildir)s/subscriptions - chown $user:$user %(maildir)s/subscriptions + chown %(user)s:%(user)s %(maildir)s/subscriptions fi """) % context ) @@ -39,7 +39,7 @@ class SieveFilteringMixin(object): context['filtering'] = ('# %(banner)s\n' + content) % context self.append(textwrap.dedent("""\ # Create and compile orchestra sieve filtering - su $user --shell /bin/bash << 'EOF' + su %(user)s --shell /bin/bash << 'EOF' mkdir -p $(dirname "%(filtering_path)s") cat << ' EOF' > %(filtering_path)s %(filtering)s @@ -50,7 +50,7 @@ class SieveFilteringMixin(object): ) else: self.append("echo '' > %(filtering_path)s" % context) - self.append('chown $user:$group %(filtering_path)s' % context) + self.append('chown %(user)s:%(group)s %(filtering_path)s' % context) class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController): @@ -97,7 +97,7 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController): #unit_to_bytes(mailbox.resources.disk.unit) self.append(textwrap.dedent(""" # Set Maildir quota for %(user)s - su $user --shell /bin/bash << 'EOF' + su %(user)s --shell /bin/bash << 'EOF' mkdir -p %(maildir)s EOF if [[ ! -f %(maildir)s/maildirsize ]]; then diff --git a/orchestra/contrib/mailer/admin.py b/orchestra/contrib/mailer/admin.py index 6ee69e21..5ed378da 100644 --- a/orchestra/contrib/mailer/admin.py +++ b/orchestra/contrib/mailer/admin.py @@ -32,6 +32,7 @@ class MessageAdmin(admin.ModelAdmin): ) list_filter = ('state', 'priority', 'retries') list_prefetch_related = ('logs__id') + search_fields = ('to_address', 'from_address', 'subject',) fieldsets = ( (None, { 'fields': ('state', 'priority', ('retries', 'last_try_delta', 'created_at_delta'), diff --git a/orchestra/contrib/mailer/engine.py b/orchestra/contrib/mailer/engine.py index ddf06c2f..14f65c49 100644 --- a/orchestra/contrib/mailer/engine.py +++ b/orchestra/contrib/mailer/engine.py @@ -13,13 +13,9 @@ from . import settings from .models import Message -def send_message(message, num=0, connection=None, bulk=settings.MAILER_BULK_MESSAGES): - if num >= bulk and connection is not None: - connection.close() - connection = None +def send_message(message, connection=None, bulk=settings.MAILER_BULK_MESSAGES): if connection is None: # Reset connection with django - num = 0 connection = get_connection(backend='django.core.mail.backends.smtp.EmailBackend') connection.open() error = None @@ -47,20 +43,28 @@ def send_pending(bulk=settings.MAILER_BULK_MESSAGES): try: with LockFile('/dev/shm/mailer.send_pending.lock'): connection = None - num = 0 - for message in Message.objects.filter(state=Message.QUEUED).order_by('priority'): - connection = send_message(message, num, connection, bulk) - num += 1 + cur, total = 0, 0 + for message in Message.objects.filter(state=Message.QUEUED).order_by('priority', 'last_try', 'created_at'): + if cur >= bulk and connection is not None: + connection.close() + cur = 0 + connection = send_message(message, connection, bulk) + cur += 1 + total += 1 now = timezone.now() qs = Q() for retries, seconds in enumerate(settings.MAILER_DEFERE_SECONDS): delta = timedelta(seconds=seconds) qs = qs | Q(retries=retries, last_try__lte=now-delta) - for message in Message.objects.filter(state=Message.DEFERRED).filter(qs).order_by('priority'): - connection = send_message(message, num, connection, bulk) - num += 1 + for message in Message.objects.filter(state=Message.DEFERRED).filter(qs).order_by('priority', 'last_try'): + if cur >= bulk and connection is not None: + connection.close() + cur = 0 + connection = send_message(message, connection, bulk) + cur += 1 + total += 1 if connection is not None: connection.close() - return num + return total except OperationLocked: pass diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index bae2e228..c7242b96 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/admin.py @@ -119,6 +119,7 @@ class BackendLogAdmin(admin.ModelAdmin): ) list_display_links = ('id', 'backend') list_filter = ('state', 'backend', 'server') + search_fields = ('script',) date_hierarchy = 'created_at' inlines = (BackendOperationInline,) fields = ( diff --git a/orchestra/contrib/orchestration/management/commands/orchestrate.py b/orchestra/contrib/orchestration/management/commands/orchestrate.py index 67a708ac..b12b4a0c 100644 --- a/orchestra/contrib/orchestration/management/commands/orchestrate.py +++ b/orchestra/contrib/orchestration/management/commands/orchestrate.py @@ -6,6 +6,7 @@ from orchestra.contrib.orchestration import manager, Operation from orchestra.contrib.orchestration.models import Server from orchestra.contrib.orchestration.backends import ServiceBackend from orchestra.utils.python import import_class, OrderedSet, AttrDict +from orchestra.utils.sys import confirm class Command(BaseCommand): @@ -91,15 +92,8 @@ class Command(BaseCommand): context = { 'servers': ', '.join(servers), } - msg = ("\n\nAre your sure to execute the previous scripts on %(servers)s (yes/no)? " % context) - confirm = input(msg) - while 1: - if confirm not in ('yes', 'no'): - confirm = input('Please enter either "yes" or "no": ') - continue - if confirm == 'no': - return - break + if not confirm("\n\nAre your sure to execute the previous scripts on %(servers)s (yes/no)? " % context) + return if not dry: logs = manager.execute(scripts, serialize=serialize, async=True) running = list(logs) diff --git a/orchestra/contrib/systemusers/admin.py b/orchestra/contrib/systemusers/admin.py index c27577b3..6acf85fc 100644 --- a/orchestra/contrib/systemusers/admin.py +++ b/orchestra/contrib/systemusers/admin.py @@ -42,7 +42,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende add_form = SystemUserCreationForm form = SystemUserChangeForm ordering = ('-id',) - change_view_actions = (set_permission, disable) + change_view_actions = (set_permission,) actions = (delete_selected, list_accounts) + change_view_actions def display_main(self, user): diff --git a/orchestra/management/commands/setuplog.py b/orchestra/management/commands/setuplog.py new file mode 100644 index 00000000..4092b9c8 --- /dev/null +++ b/orchestra/management/commands/setuplog.py @@ -0,0 +1,82 @@ +import os +import textwrap + +from django.core.management.base import BaseCommand + +from orchestra.contrib.settings import parser as settings_parser +from orchestra.utils.paths import get_project_dir, get_site_dir, get_project_name +from orchestra.utils.sys import run, check_root, confirm + + +class Command(BaseCommand): + help = 'Configures LOGGING setting, creates logging dir and configures logrotate.' + + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=True, + help='Tells Django to NOT prompt the user for input of any kind.') + + @check_root + def handle(self, *args, **options): + interactive = options.get('interactive') + context = { + 'site_dir': get_site_dir(), + 'settings_path': os.path.join(get_project_dir(), 'settings.py'), + 'project_name': get_project_name(), + 'log_dir': os.path.join(get_site_dir(), 'log'), + 'log_path': os.path.join(get_site_dir(), 'log', 'orchestra.log') + } + has_logging = not run('grep "^LOGGING\s*=" %(settings_path)s' % context, valid_codes=(0,1)).exit_code + if has_logging: + if not interactive: + self.stderr.write("Project settings already defines LOGGING setting, doing nothing.") + return + msg = ("\n\nYour project settings file already defines a LOGGING setting.\n" + "Do you want to override it? (yes/no): ") + if not confirm(msg): + return + settings_parser.save({ + 'LOGGING': settings_parser.Remove(), + }) + setuplogrotate = textwrap.dedent("""\ + mkdir %(log_dir)s && chown $(ls -dl %(site_dir)s|awk {'print $3":"$4'}) %(log_dir)s + echo '%(log_dir)s/*.log { + copytruncate + daily + rotate 5 + compress + delaycompress + missingok + notifempty + }' > /etc/logrotate.d/orchestra.%(project_name)s + cat << 'EOF' >> %(settings_path)s + + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'simple': { + 'format': '%%(asctime)s %%(name)s %%(levelname)s %%(message)s' + }, + }, + 'handlers': { + 'file': { + 'class': 'logging.FileHandler', + 'filename': '%(log_path)s', + 'formatter': 'simple' + }, + }, + 'loggers': { + 'orchestra': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': True, + }, + 'orm': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': True, + }, + }, + } + EOF""") % context + run(setuplogrotate, display=True) diff --git a/orchestra/management/commands/setupnginx.py b/orchestra/management/commands/setupnginx.py index 0ade3e44..0797d83d 100644 --- a/orchestra/management/commands/setupnginx.py +++ b/orchestra/management/commands/setupnginx.py @@ -7,7 +7,7 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError from orchestra.utils import paths -from orchestra.utils.sys import run, check_root +from orchestra.utils.sys import run, check_root, confirm class Command(BaseCommand): @@ -230,16 +230,9 @@ class Command(BaseCommand): elif diff.exit_code == 1: # File is different, save the old one if interactive: - msg = ("\n\nFile %(file)s be updated, do you like to overide " - "it? (yes/no): " % context) - confirm = input(msg) - while 1: - if confirm not in ('yes', 'no'): - confirm = input('Please enter either "yes" or "no": ') - continue - if confirm == 'no': - return - break + if not confirm("\n\nFile %(file)s be updated, do you like to overide " + "it? (yes/no): " % context) + return run("cp %(file)s %(file)s.save" % context, display=True) run("cat << 'EOF' > %(file)s\n%(conf)s\nEOF" % context, display=True) self.stdout.write("\033[1;31mA new version of %(file)s has been installed.\n " diff --git a/orchestra/static/orchestra/icons/History.png b/orchestra/static/orchestra/icons/History.png new file mode 100644 index 0000000000000000000000000000000000000000..855b4982a020bda202991eeca319f34039d1b59c GIT binary patch literal 2675 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/utils/sys.py b/orchestra/utils/sys.py index 734ee474..bd98409b 100644 --- a/orchestra/utils/sys.py +++ b/orchestra/utils/sys.py @@ -32,6 +32,17 @@ def check_non_root(func): return wrapped +def confirm(msg): + confirmation = input(msg) + while True: + if confirmation not in ('yes', 'no'): + confirmation = input('Please enter either "yes" or "no": ') + continue + if confirmation == 'no': + return False + return True + + class _Attribute(object): """ Simple string subclass to allow arbitrary attribute access. """ def __init__(self, stdout): diff --git a/scripts/container/deploy.sh b/scripts/container/deploy.sh index d894cc3d..d60a7f44 100755 --- a/scripts/container/deploy.sh +++ b/scripts/container/deploy.sh @@ -88,8 +88,7 @@ if [[ ! $(sudo su postgres -c "psql -lqt" | awk {'print $1'} | grep '^orchestra$ fi # create logfile -touch /home/orchestra/panel/orchestra.log -chown $USER:$USER /home/orchestra/panel/orchestra.log +surun "$PYTHON_BIN $MANAGE setuplog --noinput" # admin_tools needs accounts and does not have migrations