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 %}
+
+
+
+ {% trans 'Date/time' %} |
+ {% trans 'User' %} |
+ {% trans 'Action' %} |
+
+
+
+ {% for action in action_list %}
+
+ {{ 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 %} |
+
+ {% endfor %}
+
+
+{% 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 00000000..855b4982
Binary files /dev/null and b/orchestra/static/orchestra/icons/History.png differ
diff --git a/orchestra/static/orchestra/icons/History.svg b/orchestra/static/orchestra/icons/History.svg
new file mode 100644
index 00000000..623c89f5
--- /dev/null
+++ b/orchestra/static/orchestra/icons/History.svg
@@ -0,0 +1,554 @@
+
+
+
+
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