diff --git a/TODO.md b/TODO.md
index dfac45c0..662822ab 100644
--- a/TODO.md
+++ b/TODO.md
@@ -280,4 +280,9 @@ https://code.djangoproject.com/ticket/24576
# migrations accounts, bill, orders, auth -> migrate the rest (contacts lambda error)
-# MultiCHoiceField proper serialization
+* MultiCHoiceField proper serialization
+
+# Apache restart fails: detect if appache running, and execute start
+# PHP backend is retarded does not detect well the version
+# Change crons, create cron for deleted webapps and users
+* UNIFY PHP FPM settings name
diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py
index d55dfb38..bb910d17 100644
--- a/orchestra/contrib/domains/admin.py
+++ b/orchestra/contrib/domains/admin.py
@@ -103,14 +103,16 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
""" Order by structured name and imporve performance """
qs = super(DomainAdmin, self).get_queryset(request)
qs = qs.select_related('top', 'account')
- # For some reason if we do this we know for sure that join table will be called T4
- query = str(qs.query)
- table = re.findall(r'(T\d+)\."account_id"', query)[0]
- qs = qs.extra(
- select={
- 'structured_name': 'CONCAT({table}.name, domains_domain.name)'.format(table=table)
- },
- ).order_by('structured_name')
+ # Order by structured name
+ if request.method == 'GET':
+ # For some reason if we do this we know for sure that join table will be called T4
+ query = str(qs.query)
+ table = re.findall(r'(T\d+)\."account_id"', query)[0]
+ qs = qs.extra(
+ select={
+ 'structured_name': 'CONCAT({table}.name, domains_domain.name)'.format(table=table)
+ },
+ ).order_by('structured_name')
if apps.isinstalled('orchestra.contrib.websites'):
qs = qs.prefetch_related('websites')
return qs
diff --git a/orchestra/contrib/domains/forms.py b/orchestra/contrib/domains/forms.py
index 1b853a06..f1c2ad66 100644
--- a/orchestra/contrib/domains/forms.py
+++ b/orchestra/contrib/domains/forms.py
@@ -59,6 +59,7 @@ class BatchDomainCreationAdminForm(forms.ModelForm):
class RecordInlineFormSet(forms.models.BaseInlineFormSet):
def clean(self):
""" Checks if everything is consistent """
+ super(RecordInlineFormSet, self).clean()
if any(self.errors):
return
if self.instance.name:
diff --git a/orchestra/contrib/domains/helpers.py b/orchestra/contrib/domains/helpers.py
index 07b6e2c0..f1e1842b 100644
--- a/orchestra/contrib/domains/helpers.py
+++ b/orchestra/contrib/domains/helpers.py
@@ -9,7 +9,7 @@ def domain_for_validation(instance, records):
so when validation calls render_zone() it will use the new provided data
"""
domain = copy.copy(instance)
- def get_records():
+ def get_records(records=records):
for data in records:
yield Record(type=data['type'], value=data['value'])
domain.get_records = get_records
@@ -19,7 +19,8 @@ def domain_for_validation(instance, records):
domain.top = domain.get_parent(top=True)
if domain.top:
# is a subdomain
- subdomains = [sub for sub in domain.top.subdomains.all() if sub.pk != domain.pk]
+ subdomains = domain.top.subdomains.select_related('top').prefetch_related('records').all()
+ subdomains = [sub for sub in subdomains if sub.pk != domain.pk]
domain.top.get_subdomains = lambda: subdomains + [domain]
elif not domain.pk:
# is a new top domain
diff --git a/orchestra/contrib/lists/admin.py b/orchestra/contrib/lists/admin.py
index 00c26473..5a643579 100644
--- a/orchestra/contrib/lists/admin.py
+++ b/orchestra/contrib/lists/admin.py
@@ -6,17 +6,20 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.utils import admin_link
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
+from orchestra.contrib.accounts.filters import IsActiveListFilter
from .forms import ListCreationForm, ListChangeForm
from .models import List
class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
- list_display = ('name', 'address_name', 'address_domain_link', 'account_link')
+ list_display = (
+ 'name', 'address_name', 'address_domain_link', 'account_link', 'display_active'
+ )
add_fieldsets = (
(None, {
'classes': ('wide',),
- 'fields': ('account_link', 'name',)
+ 'fields': ('account_link', 'name', 'is_active')
}),
(_("Address"), {
'classes': ('wide',),
@@ -30,7 +33,7 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel
fieldsets = (
(None, {
'classes': ('wide',),
- 'fields': ('account_link', 'name',)
+ 'fields': ('account_link', 'name', 'is_active')
}),
(_("Address"), {
'classes': ('wide',),
@@ -42,6 +45,7 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel
}),
)
search_fields = ('name', 'address_name', 'address_domain__name', 'account__username')
+ list_filter = (IsActiveListFilter,)
readonly_fields = ('account_link',)
change_readonly_fields = ('name',)
form = ListChangeForm
diff --git a/orchestra/contrib/lists/backends.py b/orchestra/contrib/lists/backends.py
index 033b0682..cf8dbde0 100644
--- a/orchestra/contrib/lists/backends.py
+++ b/orchestra/contrib/lists/backends.py
@@ -119,7 +119,7 @@ class MailmanBackend(ServiceController):
postmap %(virtual_alias)s
fi
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
- /etc/init.d/postfix reload
+ service postfix reload
fi""") % context
)
diff --git a/orchestra/contrib/mailboxes/backends.py b/orchestra/contrib/mailboxes/backends.py
index 7e933658..4278e056 100644
--- a/orchestra/contrib/mailboxes/backends.py
+++ b/orchestra/contrib/mailboxes/backends.py
@@ -118,14 +118,17 @@ class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
def delete(self, mailbox):
context = self.get_context(mailbox)
- self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context)
- self.append("killall -u %(uid)s || true" % context)
- self.append("sed -i '/^%(user)s:.*/d' %(passwd_path)s" % context)
- self.append("sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s" % context)
- self.append("UPDATED_VIRTUAL_MAILBOX_MAPS=1")
- # TODO delete
- context['deleted'] = context['home'].rstrip('/') + '.deleted'
- self.append("mv %(home)s %(deleted)s" % context)
+ self.append(textwrap.dedent("""\
+ { sleep 2 && killall -u %(uid)s -s KILL; } &
+ killall -u %(uid)s || true
+ sed -i '/^%(user)s:.*/d' %(passwd_path)s
+ sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
+ UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context
+ )
+ if context['deleted_home']:
+ self.append("mv %(home)s %(deleted_home)s || exit_code=1" % context)
+ else:
+ self.append("rm -fr %(home)s" % context)
def get_extra_fields(self, mailbox, context):
context['quota'] = self.get_quota(mailbox)
@@ -159,13 +162,16 @@ class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
'group': self.DEFAULT_GROUP,
'quota': self.get_quota(mailbox),
'passwd_path': settings.MAILBOXES_PASSWD_PATH,
- 'home': mailbox.get_home().rstrip('/'),
+ 'home': mailbox.get_home(),
'banner': self.get_banner(),
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
}
context['extra_fields'] = self.get_extra_fields(mailbox, context)
- context['passwd'] = '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context)
+ context.update({
+ 'passwd': '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context),
+ 'deleted_home': settings.MAILBOXES_MOVE_ON_DELETE_PATH % context,
+ })
return replace(context, "'", '"')
@@ -177,11 +183,13 @@ class PostfixAddressBackend(ServiceController):
)
def include_virtual_alias_domain(self, context):
- self.append(textwrap.dedent("""
- [[ $(grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s) ]] || {
- echo '%(domain)s' >> %(virtual_alias_domains)s
- UPDATED_VIRTUAL_ALIAS_DOMAINS=1
- }""") % context)
+ if context['domain'] != context['local_domain']:
+ self.append(textwrap.dedent("""
+ [[ $(grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s) ]] || {
+ echo '%(domain)s' >> %(virtual_alias_domains)s
+ UPDATED_VIRTUAL_ALIAS_DOMAINS=1
+ }""") % context
+ )
def exclude_virtual_alias_domain(self, context):
domain = context['domain']
@@ -193,7 +201,7 @@ class PostfixAddressBackend(ServiceController):
# destination = []
# for mailbox in address.get_mailboxes():
# context['mailbox'] = mailbox
-# destination.append("%(mailbox)s@%(mailbox_domain)s" % context)
+# destination.append("%(mailbox)s@%(local_domain)s" % context)
# for forward in address.forward:
# if '@' in forward:
# destination.append(forward)
@@ -237,7 +245,7 @@ class PostfixAddressBackend(ServiceController):
context = self.get_context_files()
self.append(textwrap.dedent("""
[[ $UPDATED_VIRTUAL_ALIAS_MAPS == 1 ]] && { postmap %(virtual_alias_maps)s; }
- [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
+ [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { service postfix reload; }
""") % context
)
self.append('exit 0')
@@ -253,7 +261,7 @@ class PostfixAddressBackend(ServiceController):
context.update({
'domain': address.domain,
'email': address.email,
- 'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
+ 'local_domain': settings.MAILBOXES_LOCAL_DOMAIN,
})
return replace(context, "'", '"')
@@ -344,11 +352,13 @@ class PostfixMailscannerTraffic(ServiceMonitor):
def inside_period(month, day, time, ini_date):
global months
global end_datetime
- # Mar 19 17:13:22
+ # Mar 9 17:13:22
month = months[month]
year = end_datetime.year
if month == '12' and end_datetime.month == 1:
year = year+1
+ if len(day) == 1:
+ day = '0' + day
date = str(year) + month + day
date += time.replace(':', '')
return ini_date < int(date) < end_date
diff --git a/orchestra/contrib/mailboxes/settings.py b/orchestra/contrib/mailboxes/settings.py
index bd40ad11..8a65e6e6 100644
--- a/orchestra/contrib/mailboxes/settings.py
+++ b/orchestra/contrib/mailboxes/settings.py
@@ -47,7 +47,7 @@ MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_ALIA
)
-MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN = getattr(settings, 'MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN',
+MAILBOXES_LOCAL_DOMAIN = getattr(settings, 'MAILBOXES_LOCAL_DOMAIN',
ORCHESTRA_BASE_DOMAIN
)
@@ -94,3 +94,8 @@ MAILBOXES_LOCAL_ADDRESS_DOMAIN = getattr(settings, 'MAILBOXES_LOCAL_ADDRESS_DOMA
MAILBOXES_MAIL_LOG_PATH = getattr(settings, 'MAILBOXES_MAIL_LOG_PATH',
'/var/log/mail.log'
)
+
+
+MAILBOXES_MOVE_ON_DELETE_PATH = getattr(settings, 'MAILBOXES_MOVE_ON_DELETE_PATH',
+ ''
+)
diff --git a/orchestra/contrib/miscellaneous/admin.py b/orchestra/contrib/miscellaneous/admin.py
index 465b0c57..93c1af92 100644
--- a/orchestra/contrib/miscellaneous/admin.py
+++ b/orchestra/contrib/miscellaneous/admin.py
@@ -54,7 +54,7 @@ class MiscServiceAdmin(ExtendedModelAdmin):
class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelAdmin):
list_display = (
- '__str__', 'service_link', 'amount', 'dispaly_active', 'account_link'
+ '__str__', 'service_link', 'amount', 'dispaly_active', 'account_link', 'is_active'
)
list_filter = ('service__name', 'is_active')
list_select_related = ('service', 'account')
diff --git a/orchestra/contrib/miscellaneous/models.py b/orchestra/contrib/miscellaneous/models.py
index 3db29560..663b4232 100644
--- a/orchestra/contrib/miscellaneous/models.py
+++ b/orchestra/contrib/miscellaneous/models.py
@@ -56,10 +56,7 @@ class Miscellaneous(models.Model):
@cached_property
def active(self):
- try:
- return self.is_active and self.account.is_active
- except type(self).account.field.rel.to.DoesNotExist:
- return self.is_active
+ return self.is_active and self.service.is_active and self.account.is_active
def get_description(self):
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
diff --git a/orchestra/contrib/orchestration/__init__.py b/orchestra/contrib/orchestration/__init__.py
index 297c2b1d..c4d3945d 100644
--- a/orchestra/contrib/orchestration/__init__.py
+++ b/orchestra/contrib/orchestration/__init__.py
@@ -50,7 +50,7 @@ class Operation():
if hasattr(self.backend, 'get_context'):
self.backend().get_context(self.instance)
- def create(self, log):
+ def store(self, log):
from .models import BackendOperation
return BackendOperation.objects.create(
log=log,
diff --git a/orchestra/contrib/orchestration/management/commands/orchestrate.py b/orchestra/contrib/orchestration/management/commands/orchestrate.py
index 9e4ffb3c..c7afb3f0 100644
--- a/orchestra/contrib/orchestration/management/commands/orchestrate.py
+++ b/orchestra/contrib/orchestration/management/commands/orchestrate.py
@@ -18,11 +18,14 @@ class Command(BaseCommand):
help='Tells Django to NOT prompt the user for input of any kind.')
parser.add_argument('--action', action='store', dest='action',
default='save', help='Executes action. Defaults to "save".')
+ parser.add_argument('--dry-run', action='store_true', dest='dry', default=False,
+ help='Only prints scrtipt.')
def handle(self, *args, **options):
model = get_model(*options['model'].split('.'))
action = options.get('action')
interactive = options.get('interactive')
+ dry = options.get('dry')
kwargs = {}
for comp in options.get('query', []):
comps = iter(comp.split('='))
@@ -42,7 +45,9 @@ class Command(BaseCommand):
servers.append(server.name)
sys.stdout.write('# Execute on %s\n' % server.name)
for method, commands in backend.scripts:
- sys.stdout.write('\n'.join(commands) + '\n')
+ script = '\n'.join(commands) + '\n'
+ script = script.encode('ascii', errors='replace')
+ sys.stdout.write(script.decode('ascii'))
if interactive:
context = {
'servers': ', '.join(servers),
@@ -56,4 +61,10 @@ class Command(BaseCommand):
if confirm == 'no':
return
break
-# manager.execute(scripts, block=block)
+ if not dry:
+ logs = manager.execute(scripts, block=block)
+ for log in logs:
+ print(log.stdout)
+ sys.stderr.write(log.stderr)
+ for log in logs:
+ print(log.backend, log.state)
diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py
index e2cfb735..18c737f6 100644
--- a/orchestra/contrib/orchestration/manager.py
+++ b/orchestra/contrib/orchestration/manager.py
@@ -125,7 +125,7 @@ def execute(scripts, block=False, async=False):
logger.info("Executed %s" % str(operation))
if operation.instance.pk:
# Not all backends are called with objects saved on the database
- operation.create(execution.log)
+ operation.store(execution.log)
stdout = execution.log.stdout.strip()
stdout and logger.debug('STDOUT %s', stdout)
stderr = execution.log.stderr.strip()
diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py
index d40bcc72..e30dc1c9 100644
--- a/orchestra/contrib/plans/admin.py
+++ b/orchestra/contrib/plans/admin.py
@@ -14,8 +14,8 @@ class RateInline(admin.TabularInline):
class PlanAdmin(ExtendedModelAdmin):
- list_display = ('name', 'is_default', 'is_combinable', 'allow_multiple')
- list_filter = ('is_default', 'is_combinable', 'allow_multiple')
+ list_display = ('name', 'is_default', 'is_combinable', 'allow_multiple', 'is_active')
+ list_filter = ('is_default', 'is_combinable', 'allow_multiple', 'is_active')
fields = ('verbose_name', 'name', 'is_default', 'is_combinable', 'allow_multiple')
prepopulated_fields = {
'name': ('verbose_name',)
diff --git a/orchestra/contrib/resources/admin.py b/orchestra/contrib/resources/admin.py
index 5479e2ee..6685490d 100644
--- a/orchestra/contrib/resources/admin.py
+++ b/orchestra/contrib/resources/admin.py
@@ -126,7 +126,7 @@ class ResourceDataAdmin(ExtendedModelAdmin):
display_unit.admin_order_field = 'resource__unit'
def display_used(self, data):
- if not data.used:
+ if data.used is None:
return ''
url = reverse('admin:resources_resourcedata_used_monitordata', args=(data.pk,))
return '%s' % (url, data.used)
diff --git a/orchestra/contrib/resources/aggregations.py b/orchestra/contrib/resources/aggregations.py
index 7b4c993c..be36e01d 100644
--- a/orchestra/contrib/resources/aggregations.py
+++ b/orchestra/contrib/resources/aggregations.py
@@ -19,12 +19,13 @@ class Aggregation(plugins.Plugin, metaclass=plugins.PluginMount):
class Last(Aggregation):
+ """ Sum of the last value of all monitors """
name = 'last'
verbose_name = _("Last value")
def filter(self, dataset):
try:
- return dataset.order_by('object_id', '-id').distinct('object_id')
+ return dataset.order_by('object_id', '-id').distinct('monitor')
except dataset.model.DoesNotExist:
return dataset.none()
@@ -38,6 +39,7 @@ class Last(Aggregation):
class MonthlySum(Last):
+ """ Monthly sum the values of all monitors """
name = 'monthly-sum'
verbose_name = _("Monthly Sum")
@@ -50,9 +52,14 @@ class MonthlySum(Last):
class MonthlyAvg(MonthlySum):
+ """ sum of the monthly averages of each monitor """
name = 'monthly-avg'
verbose_name = _("Monthly AVG")
+ def filter(self, dataset):
+ qs = super(MonthlyAvg, self).filter(dataset)
+ return qs.order_by('created_at')
+
def get_epoch(self):
today = timezone.now()
return datetime(
@@ -64,21 +71,27 @@ class MonthlyAvg(MonthlySum):
def compute_usage(self, dataset):
result = 0
- try:
- last = dataset.latest()
- except dataset.model.DoesNotExist:
+ has_result = False
+ for monitor, dataset in dataset.group_by('monitor').items():
+ try:
+ last = dataset[-1]
+ except IndexError:
+ continue
+ epoch = self.get_epoch()
+ total = (last.created_at-epoch).total_seconds()
+ ini = epoch
+ for data in dataset:
+ has_result = True
+ slot = (data.created_at-ini).total_seconds()
+ result += data.value * decimal.Decimal(str(slot/total))
+ ini = data.created_at
+ if has_result:
return result
- epoch = self.get_epoch()
- total = (last.created_at-epoch).total_seconds()
- ini = epoch
- for data in dataset:
- slot = (data.created_at-ini).total_seconds()
- result += data.value * decimal.Decimal(str(slot/total))
- ini = data.created_at
- return result
+ return None
class Last10DaysAvg(MonthlyAvg):
+ """ sum of the last 10 days averages of each monitor """
name = 'last-10-days-avg'
verbose_name = _("Last 10 days AVG")
days = 10
@@ -88,4 +101,5 @@ class Last10DaysAvg(MonthlyAvg):
return today - datetime.timedelta(days=self.days)
def filter(self, dataset):
- return dataset.filter(created_at__gt=self.get_epoch())
+ epoch = self.get_epoch()
+ return dataset.filter(created_at__gt=epoch).order_by('created_at')
diff --git a/orchestra/contrib/resources/models.py b/orchestra/contrib/resources/models.py
index cf09abee..a11d742c 100644
--- a/orchestra/contrib/resources/models.py
+++ b/orchestra/contrib/resources/models.py
@@ -262,6 +262,10 @@ class ResourceData(models.Model):
return datasets
+class MonitorDataQuerySet(models.QuerySet):
+ group_by = queryset.group_by
+
+
class MonitorData(models.Model):
""" Stores monitored data """
monitor = models.CharField(_("monitor"), max_length=256,
@@ -272,6 +276,7 @@ class MonitorData(models.Model):
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
content_object = GenericForeignKey()
+ objects = MonitorDataQuerySet.as_manager()
class Meta:
get_latest_by = 'id'
diff --git a/orchestra/contrib/resources/tasks.py b/orchestra/contrib/resources/tasks.py
index 39b95e09..bb1cde0c 100644
--- a/orchestra/contrib/resources/tasks.py
+++ b/orchestra/contrib/resources/tasks.py
@@ -27,7 +27,7 @@ def monitor(resource_id, ids=None, async=True):
# Execute monitor
monitorings = []
for obj in model.objects.filter(**kwargs):
- op = Operation.create(backend, obj, Operation.MONITOR)
+ op = Operation(backend, obj, Operation.MONITOR)
operations.append(op)
monitorings.append(op)
# TODO async=True only when running with celery
@@ -44,10 +44,10 @@ def monitor(resource_id, ids=None, async=True):
a = data.used
b = data.allocated
if data.used > (data.allocated or 0):
- op = Operation.create(backend, obj, Operation.EXCEEDED)
+ op = Operation(backend, obj, Operation.EXCEEDED)
triggers.append(op)
elif data.used < (data.allocated or 0):
- op = Operation.create(backend, obj, Operation.RECOVERY)
+ op = Operation(backend, obj, Operation.RECOVERY)
triggers.append(op)
Operation.execute(triggers)
return operations
diff --git a/orchestra/contrib/saas/backends/wordpressmu.py b/orchestra/contrib/saas/backends/wordpressmu.py
index 604a1353..6fa8f94e 100644
--- a/orchestra/contrib/saas/backends/wordpressmu.py
+++ b/orchestra/contrib/saas/backends/wordpressmu.py
@@ -13,10 +13,6 @@ class WordpressMuBackend(ServiceController):
model = 'webapps.WebApp'
default_route_match = "webapp.type == 'wordpress-mu'"
- @property
- def script(self):
- return self.cmds
-
def login(self, session):
base_url = self.get_base_url()
login_url = base_url + '/wp-login.php'
@@ -113,11 +109,7 @@ class WordpressMuBackend(ServiceController):
self.validate_response(response)
def save(self, webapp):
- if webapp.type != 'wordpress-mu':
- return
self.append(self.create_blog, webapp)
def delete(self, webapp):
- if webapp.type != 'wordpress-mu':
- return
self.append(self.delete_blog, webapp)
diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py
index 4c00b594..ffd50393 100644
--- a/orchestra/contrib/systemusers/backends.py
+++ b/orchestra/contrib/systemusers/backends.py
@@ -39,9 +39,9 @@ class UNIXUserBackend(ServiceController):
)
for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
context['member'] = member
- self.append('usermod -a -G %(user)s %(member)s' % context)
+ self.append('usermod -a -G %(user)s %(member)s || exit_code=$?' % context)
if not user.is_main:
- self.append('usermod -a -G %(user)s %(mainuser)s' % context)
+ self.append('usermod -a -G %(user)s %(mainuser)s || exit_code=$?' % context)
def delete(self, user):
context = self.get_context(user)
@@ -52,9 +52,12 @@ class UNIXUserBackend(ServiceController):
killall -u %(user)s || true
userdel %(user)s || exit_code=1
groupdel %(group)s || exit_code=1
- mv %(base_home)s %(base_home)s.deleted || exit_code=1
""") % context
)
+ if context['deleted_home']:
+ self.append("mv %(base_home)s %(deleted_home)s || exit_code=1" % context)
+ else:
+ self.append("rm -fr %(base_home)s" % context)
def grant_permission(self, user):
context = self.get_context(user)
@@ -76,6 +79,7 @@ class UNIXUserBackend(ServiceController):
'home': user.get_home(),
'base_home': user.get_base_home(),
}
+ context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context
return replace(context, "'", '"')
diff --git a/orchestra/contrib/systemusers/forms.py b/orchestra/contrib/systemusers/forms.py
index 84131d64..0eeee379 100644
--- a/orchestra/contrib/systemusers/forms.py
+++ b/orchestra/contrib/systemusers/forms.py
@@ -60,6 +60,7 @@ class SystemUserFormMixin(object):
}
def clean(self):
+ super(SystemUserFormMixin, self).clean()
home = self.cleaned_data.get('home')
if home and self.MOCK_USERNAME in home:
username = self.cleaned_data.get('username', '')
diff --git a/orchestra/contrib/systemusers/settings.py b/orchestra/contrib/systemusers/settings.py
index ee42bd08..bb658276 100644
--- a/orchestra/contrib/systemusers/settings.py
+++ b/orchestra/contrib/systemusers/settings.py
@@ -39,3 +39,8 @@ SYSTEMUSERS_MAIL_LOG_PATH = getattr(settings, 'SYSTEMUSERS_MAIL_LOG_PATH',
SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = getattr(settings, 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
('www-data',)
)
+
+
+SYSTEMUSERS_MOVE_ON_DELETE_PATH = getattr(settings, 'SYSTEMUSERS_MOVE_ON_DELETE_PATH',
+ ''
+)
diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py
index db40af67..19f53b16 100644
--- a/orchestra/contrib/webapps/admin.py
+++ b/orchestra/contrib/webapps/admin.py
@@ -5,14 +5,15 @@ from django.utils.encoding import force_text
from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
-from orchestra.admin.utils import change_url
+from orchestra.admin.utils import change_url, get_modeladmin
from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.forms.widgets import DynamicHelpTextSelect
from orchestra.plugins.admin import SelectPluginAdminMixin
+from .filters import HasWebsiteListFilter
+from .models import WebApp, WebAppOption
from .options import AppOption
from .types import AppType
-from .models import WebApp, WebAppOption
class WebAppOptionInline(admin.TabularInline):
@@ -36,7 +37,9 @@ class WebAppOptionInline(admin.TabularInline):
plugin = self.parent_object.type_class
else:
request = kwargs['request']
- plugin = AppType.get(request.GET['type'])
+ webapp_modeladmin = get_modeladmin(self.parent_model)
+ plugin_value = webapp_modeladmin.get_plugin_value(request)
+ plugin = AppType.get(plugin_value)
kwargs['choices'] = plugin.get_options_choices()
# Help text based on select widget
target = 'this.id.replace("name", "value")'
@@ -46,7 +49,7 @@ class WebAppOptionInline(admin.TabularInline):
class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link')
- list_filter = ('type',)
+ list_filter = ('type', HasWebsiteListFilter)
inlines = [WebAppOptionInline]
readonly_fields = ('account_link', )
change_readonly_fields = ('name', 'type', 'display_websites')
diff --git a/orchestra/contrib/webapps/backends/__init__.py b/orchestra/contrib/webapps/backends/__init__.py
index 15b601ea..d6e88dd3 100644
--- a/orchestra/contrib/webapps/backends/__init__.py
+++ b/orchestra/contrib/webapps/backends/__init__.py
@@ -29,7 +29,10 @@ class WebAppServiceMixin(object):
)
def delete_webapp_dir(self, context):
- self.append("rm -fr %(app_path)s" % context)
+ if context['deleted_app_path']:
+ self.append("mv %(app_path)s %(deleted_app_path)s || exit_code=1" % context)
+ else:
+ self.append("rm -fr %(app_path)s" % context)
def get_context(self, webapp):
context = {
@@ -37,11 +40,12 @@ class WebAppServiceMixin(object):
'group': webapp.get_groupname(),
'app_name': webapp.name,
'type': webapp.type,
- 'app_path': webapp.get_path().rstrip('/'),
+ 'app_path': webapp.get_path(),
'banner': self.get_banner(),
'under_construction_path': settings.settings.WEBAPPS_UNDER_CONSTRUCTION_PATH,
'is_mounted': webapp.content_set.exists(),
}
+ context['deleted_app_path'] = settings.WEBAPPS_MOVE_ON_DELETE_PATH % context
return replace(context, "'", '"')
diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py
index d57b62ac..bf9eb3ab 100644
--- a/orchestra/contrib/webapps/backends/php.py
+++ b/orchestra/contrib/webapps/backends/php.py
@@ -17,6 +17,8 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
def save(self, webapp):
context = self.get_context(webapp)
+ self.create_webapp_dir(context)
+ self.set_under_construction(context)
if webapp.type_instance.is_fpm:
self.save_fpm(webapp, context)
self.delete_fcgid(webapp, context)
@@ -25,8 +27,6 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
self.delete_fpm(webapp, context)
def save_fpm(self, webapp, context):
- self.create_webapp_dir(context)
- self.set_under_construction(context)
self.append(textwrap.dedent("""\
fpm_config='%(fpm_config)s'
{
@@ -39,8 +39,6 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
)
def save_fcgid(self, webapp, context):
- self.create_webapp_dir(context)
- self.set_under_construction(context)
self.append("mkdir -p %(wrapper_dir)s" % context)
self.append(textwrap.dedent("""\
wrapper='%(wrapper)s'
@@ -104,7 +102,8 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
merge = settings.WEBAPPS_MERGE_PHP_WEBAPPS
context.update({
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
- 'max_children': webapp.get_options().get('processes', False),
+ 'max_children': webapp.get_options().get('processes',
+ settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
'request_terminate_timeout': webapp.get_options().get('timeout', False),
})
context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context
@@ -119,7 +118,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
listen.group = {{ group }}
pm = ondemand
pm.max_requests = {{ max_requests }}
- {% if max_children %}pm.max_children = {{ max_children }}{% endif %}
+ pm.max_children = {{ max_children }}
{% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
{% for name, value in init_vars.iteritems %}
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
@@ -133,7 +132,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
init_vars = opt.get_php_init_vars(merge=self.MERGE)
if init_vars:
init_vars = [ "-d %s='%s'" % (k, v.replace("'", '"')) for k,v in init_vars.items() ]
- init_vars = ', '.join(init_vars)
+ init_vars = ' \\\n '.join(init_vars)
context.update({
'php_binary': os.path.normpath(settings.WEBAPPS_PHP_CGI_BINARY_PATH % context),
'php_rc': os.path.normpath(settings.WEBAPPS_PHP_CGI_RC_DIR % context),
diff --git a/orchestra/contrib/webapps/backends/symboliclink.py b/orchestra/contrib/webapps/backends/symboliclink.py
index 6ac994f7..ec9c9459 100644
--- a/orchestra/contrib/webapps/backends/symboliclink.py
+++ b/orchestra/contrib/webapps/backends/symboliclink.py
@@ -1,23 +1,28 @@
+import textwrap
+
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController, replace
-from . import WebAppServiceMixin
+from .php import PHPBackend
-class SymbolicLinkBackend(WebAppServiceMixin, ServiceController):
+class SymbolicLinkBackend(PHPBackend, ServiceController):
verbose_name = _("Symbolic link webapp")
model = 'webapps.WebApp'
default_route_match = "webapp.type == 'symbolic-link'"
- def save(self, webapp):
- context = self.get_context(webapp)
- self.append("ln -s '%(link_path)s' %(app_path)s" % context)
- self.append("chown -h %(user)s:%(group)s %(app_path)s" % context)
+ def create_webapp_dir(self, context):
+ self.append(textwrap.dedent("""\
+ if [[ ! -e %(app_path)s ]]; then
+ ln -s '%(link_path)s' %(app_path)s
+ fi
+ chown -h %(user)s:%(group)s %(app_path)s
+ """) % context
+ )
- def delete(self, webapp):
- context = self.get_context(webapp)
- self.delete_webapp_dir(context)
+ def set_under_construction(self, context):
+ pass
def get_context(self, webapp):
context = super(SymbolicLinkBackend, self).get_context(webapp)
diff --git a/orchestra/contrib/webapps/filters.py b/orchestra/contrib/webapps/filters.py
new file mode 100644
index 00000000..d0b328d9
--- /dev/null
+++ b/orchestra/contrib/webapps/filters.py
@@ -0,0 +1,22 @@
+from django.contrib.admin import SimpleListFilter
+from django.utils.translation import ugettext_lazy as _
+
+
+class HasWebsiteListFilter(SimpleListFilter):
+ title = _("Has website")
+ parameter_name = 'has_website'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('True', _("True")),
+ ('False', _("False")),
+ )
+
+ def queryset(self, request, queryset):
+ if self.value() == 'True':
+ return queryset.filter(content__isnull=False)
+ elif self.value() == 'False':
+ return queryset.filter(content__isnull=True)
+ return queryset
+
+
diff --git a/orchestra/contrib/webapps/options.py b/orchestra/contrib/webapps/options.py
index 0b9cae8d..c3732dab 100644
--- a/orchestra/contrib/webapps/options.py
+++ b/orchestra/contrib/webapps/options.py
@@ -180,14 +180,6 @@ class PHPMaginQuotesSybase(PHPAppOption):
regex = r'^(On|Off|on|off)$'
-class PHPMaxExecutonTime(PHPAppOption):
- name = 'max_execution_time'
- verbose_name = _("Max execution time")
- help_text = _("Maximum time in seconds a script is allowed to run before it is terminated by "
- "the parser (Integer between 0 and 999).")
- regex = r'^[0-9]{1,3}$'
-
-
class PHPMaxInputTime(PHPAppOption):
name = 'max_input_time'
verbose_name = _("Max input time")
diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py
index a68c342a..58ded0e4 100644
--- a/orchestra/contrib/webapps/settings.py
+++ b/orchestra/contrib/webapps/settings.py
@@ -13,6 +13,11 @@ WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN',
'/opt/php/5.4/socks/%(user)s-%(app_name)s.sock'
)
+WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = getattr(settings, 'WEBAPPS_FPM_DEFAULT_MAX_CHILDREN',
+ 3
+)
+
+
WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH',
'/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf')
@@ -145,7 +150,6 @@ WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', (
'orchestra.contrib.webapps.options.PHPMagicQuotesGPC',
'orchestra.contrib.webapps.options.PHPMagicQuotesRuntime',
'orchestra.contrib.webapps.options.PHPMaginQuotesSybase',
- 'orchestra.contrib.webapps.options.PHPMaxExecutonTime',
'orchestra.contrib.webapps.options.PHPMaxInputTime',
'orchestra.contrib.webapps.options.PHPMaxInputVars',
'orchestra.contrib.webapps.options.PHPMemoryLimit',
@@ -171,3 +175,8 @@ WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', (
WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST',
'mysql.{}'.format(ORCHESTRA_BASE_DOMAIN)
)
+
+
+WEBAPPS_MOVE_ON_DELETE_PATH = getattr(settings, 'WEBAPPS_MOVE_ON_DELETE_PATH',
+ ''
+)
diff --git a/orchestra/contrib/webapps/types/php.py b/orchestra/contrib/webapps/types/php.py
index 99bbab06..e210fcaf 100644
--- a/orchestra/contrib/webapps/types/php.py
+++ b/orchestra/contrib/webapps/types/php.py
@@ -77,15 +77,16 @@ class PHPApp(AppType):
php_version = self.get_php_version()
webapps = self.instance.account.webapps.filter(type=self.instance.type)
for webapp in webapps:
- if webapp.type_instance.get_php_version == php_version:
+ if webapp.type_instance.get_php_version() == php_version:
options += list(webapp.options.all())
php_options = [option.name for option in self.get_php_options()]
enabled_functions = set()
for opt in options:
if opt.name in php_options:
- init_vars[opt.name] = opt.value
- elif opt.name == 'enabled_functions':
- enabled_functions.union(set(opt.value.split(',')))
+ if opt.name == 'enabled_functions':
+ enabled_functions = enabled_functions.union(set(opt.value.split(',')))
+ else:
+ init_vars[opt.name] = opt.value
if enabled_functions:
disabled_functions = []
for function in self.PHP_DISABLED_FUNCTIONS:
@@ -94,7 +95,9 @@ class PHPApp(AppType):
init_vars['dissabled_functions'] = ','.join(disabled_functions)
timeout = self.instance.options.filter(name='timeout').first()
if timeout:
- init_vars['max_execution_time'] = timeout.value
+ # Give a little slack here
+ timeout = str(int(timeout.value)-2)
+ init_vars['max_execution_time'] = timeout
if self.PHP_ERROR_LOG_PATH and 'error_log' not in init_vars:
context = self.get_directive_context()
error_log_path = os.path.normpath(self.PHP_ERROR_LOG_PATH % context)
diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py
index b42c3ec1..8174ec71 100644
--- a/orchestra/contrib/websites/backends/apache.py
+++ b/orchestra/contrib/websites/backends/apache.py
@@ -40,6 +40,7 @@ class Apache2Backend(ServiceController):
context['extra_conf'] = '\n'.join([conf for location, conf in extra_conf])
return Template(textwrap.dedent("""\
+ IncludeOptional /etc/apache2/site[s]-override/{{ site_unique_name }}.con[f]
ServerName {{ site.domains.all|first }}\
{% if site.domains.all|slice:"1:" %}
ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\
@@ -50,7 +51,6 @@ class Apache2Backend(ServiceController):
SuexecUserGroup {{ user }} {{ group }}\
{% for line in extra_conf.splitlines %}
{{ line | safe }}{% endfor %}
- IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f]
""")
).render(Context(context))
@@ -181,8 +181,8 @@ class Apache2Backend(ServiceController):
def get_security(self, directives):
security = []
- for rules in directives.get('sec-rule-remove', []):
- for rule in rules.value.split():
+ for values in directives.get('sec-rule-remove', []):
+ for rule in values.split():
sec_rule = "SecRuleRemoveById %i" % int(rule)
security.append(('', sec_rule))
for location in directives.get('sec-engine', []):
@@ -267,12 +267,12 @@ class Apache2Backend(ServiceController):
'site': site,
'site_name': site.name,
'ip': settings.WEBSITES_DEFAULT_IP,
- 'site_unique_name': site.unique_name,
+ 'site_unique_name': '0-'+site.unique_name,
'user': self.get_username(site),
'group': self.get_groupname(site),
# TODO remove '0-'
'sites_enabled': "%s.conf" % os.path.join(sites_enabled, '0-'+site.unique_name),
- 'sites_available': "%s.conf" % os.path.join(sites_available, site.unique_name),
+ 'sites_available': "%s.conf" % os.path.join(sites_available, '0-'+site.unique_name),
'access_log': site.get_www_access_log_path(),
'error_log': site.get_www_error_log_path(),
'banner': self.get_banner(),
diff --git a/orchestra/contrib/websites/forms.py b/orchestra/contrib/websites/forms.py
index afbf7110..7c241ea7 100644
--- a/orchestra/contrib/websites/forms.py
+++ b/orchestra/contrib/websites/forms.py
@@ -9,6 +9,7 @@ from .validators import validate_domain_protocol
class WebsiteAdminForm(forms.ModelForm):
def clean(self):
""" Prevent multiples domains on the same protocol """
+ super(WebsiteAdminForm, self).clean()
domains = self.cleaned_data.get('domains')
if not domains:
return self.cleaned_data
diff --git a/orchestra/management/commands/makemessages.py b/orchestra/management/commands/makemessages.py
index 88f6dcd6..449bc1d7 100644
--- a/orchestra/management/commands/makemessages.py
+++ b/orchestra/management/commands/makemessages.py
@@ -51,7 +51,7 @@ class Command(makemessages.Command):
tmpcontent = '\n'.join(tmpcontent) + '\n'
filename = 'database_%s.sql.py' % name
self.database_files.append(filename)
- with open(filename, 'w') as tmpfile:
+ with open(filename, 'wb') as tmpfile:
tmpfile.write(tmpcontent.encode('utf-8'))
def remove_database_files(self):
diff --git a/orchestra/plugins/admin.py b/orchestra/plugins/admin.py
index e4b80fb1..e58f8d21 100644
--- a/orchestra/plugins/admin.py
+++ b/orchestra/plugins/admin.py
@@ -1,3 +1,5 @@
+import re
+
from django.conf.urls import patterns, url
from django.contrib.admin.utils import unquote
from django.shortcuts import render, redirect
@@ -58,10 +60,19 @@ class SelectPluginAdminMixin(object):
template = 'admin/plugins/select_plugin.html'
return render(request, template, context)
+ def get_plugin_value(self, request):
+ plugin_value = request.GET.get(self.plugin_field) or request.POST.get(self.plugin_field)
+ if not plugin_value and request.method == 'POST':
+ # HACK baceuse django add_preserved_filters removes extising queryargs
+ value = re.search(r"type=([^&^']+)[&']", request.META.get('HTTP_REFERER', ''))
+ if value:
+ plugin_value = value.groups()[0]
+ return plugin_value
+
def add_view(self, request, form_url='', extra_context=None):
""" Redirects to select account view if required """
if request.user.is_superuser:
- plugin_value = request.GET.get(self.plugin_field) or request.POST.get(self.plugin_field)
+ plugin_value = self.get_plugin_value(request)
if plugin_value or len(self.plugin.get_plugins()) == 1:
self.plugin_value = plugin_value
if not plugin_value:
diff --git a/orchestra/plugins/forms.py b/orchestra/plugins/forms.py
index 273b6a49..c9d7780d 100644
--- a/orchestra/plugins/forms.py
+++ b/orchestra/plugins/forms.py
@@ -29,6 +29,7 @@ class PluginDataForm(forms.ModelForm):
self.fields[field].widget = ReadOnlyWidget(value, display)
def clean(self):
+ super(PluginDataForm, self).clean()
data = {}
# Update data fields
for field in self.declared_fields: