Fixes on payments and php webapps
This commit is contained in:
parent
472eb70cb0
commit
e0d31f67e4
12
TODO.md
12
TODO.md
|
@ -418,15 +418,19 @@ serailzer self.instance on create.
|
||||||
|
|
||||||
# set_password serializer: "just-the-password" not {"password": "password"}
|
# set_password serializer: "just-the-password" not {"password": "password"}
|
||||||
|
|
||||||
# use namedtuples!
|
# use namedtuples?
|
||||||
|
|
||||||
# Negative transactionsx
|
# Negative transactionsx
|
||||||
|
|
||||||
|
|
||||||
# check certificate: websites directive ssl + domains search on miscellaneous
|
* check certificate: websites directive ssl + domains search on miscellaneous
|
||||||
|
|
||||||
|
|
||||||
# IF modsecurity... and Merge websites locations
|
# Merge websites locations
|
||||||
# backend email error log with link to backend log on admin
|
|
||||||
# ValueError: Unable to configure handler 'file': [Errno 13] Permission denied: '/home/orchestra/panel/orchestra.log'
|
# 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
|
||||||
|
|
||||||
|
# process monitor data to represent state, or maybe create new resource datas when period expires?
|
||||||
|
|
|
@ -8,3 +8,13 @@ MONOSPACE_FONTS = ('Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans M
|
||||||
def monospace_format(text):
|
def monospace_format(text):
|
||||||
style="font-family:%s;padding-left:110px;" % MONOSPACE_FONTS
|
style="font-family:%s;padding-left:110px;" % MONOSPACE_FONTS
|
||||||
return mark_safe('<pre style="%s">%s</pre>' % (style, text))
|
return mark_safe('<pre style="%s">%s</pre>' % (style, text))
|
||||||
|
|
||||||
|
|
||||||
|
def code_format(text, language='bash'):
|
||||||
|
from pygments import highlight
|
||||||
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
from pygments.formatters import HtmlFormatter
|
||||||
|
lexer = get_lexer_by_name(language, stripall=True)
|
||||||
|
formatter = HtmlFormatter(linenos=True)
|
||||||
|
code = highlight(text, lexer, formatter)
|
||||||
|
return mark_safe('<div style="padding-left:110px;">%s</div>' % code)
|
||||||
|
|
|
@ -16,7 +16,7 @@ from orchestra.models.utils import get_field_value
|
||||||
from orchestra.utils import humanize
|
from orchestra.utils import humanize
|
||||||
|
|
||||||
from .decorators import admin_field
|
from .decorators import admin_field
|
||||||
from .html import monospace_format
|
from .html import monospace_format, code_format
|
||||||
|
|
||||||
|
|
||||||
def get_modeladmin(model, import_module=True):
|
def get_modeladmin(model, import_module=True):
|
||||||
|
@ -165,3 +165,10 @@ def display_mono(field):
|
||||||
return monospace_format(escape(getattr(log, field)))
|
return monospace_format(escape(getattr(log, field)))
|
||||||
display.short_description = field
|
display.short_description = field
|
||||||
return display
|
return display
|
||||||
|
|
||||||
|
|
||||||
|
def display_code(field):
|
||||||
|
def display(self, log):
|
||||||
|
return code_format(getattr(log, field))
|
||||||
|
display.short_description = field
|
||||||
|
return display
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
|
from django.utils import translation
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ from orchestra.utils.html import html_to_pdf
|
||||||
|
|
||||||
from .forms import SelectSourceForm
|
from .forms import SelectSourceForm
|
||||||
from .helpers import validate_contact
|
from .helpers import validate_contact
|
||||||
|
from .models import Bill, BillLine
|
||||||
|
|
||||||
|
|
||||||
def download_bills(modeladmin, request, queryset):
|
def download_bills(modeladmin, request, queryset):
|
||||||
|
@ -209,3 +211,44 @@ def move_lines(modeladmin, request, queryset, action=None):
|
||||||
def copy_lines(modeladmin, request, queryset):
|
def copy_lines(modeladmin, request, queryset):
|
||||||
# same as move, but changing action behaviour
|
# same as move, but changing action behaviour
|
||||||
return move_lines(modeladmin, request, queryset)
|
return move_lines(modeladmin, request, queryset)
|
||||||
|
|
||||||
|
|
||||||
|
def amend_bills(modeladmin, request, queryset):
|
||||||
|
if queryset.filter(is_open=True).exists():
|
||||||
|
messages.warning(request, _("Selected bills should be in closed state"))
|
||||||
|
return
|
||||||
|
ids = []
|
||||||
|
for bill in queryset:
|
||||||
|
with translation.override(bill.account.language):
|
||||||
|
amend_type = bill.get_amend_type()
|
||||||
|
context = {
|
||||||
|
'related_type': _(bill.get_type_display()),
|
||||||
|
'number': bill.number,
|
||||||
|
'date': bill.created_on,
|
||||||
|
}
|
||||||
|
amend = Bill.objects.create(
|
||||||
|
account=bill.account,
|
||||||
|
type=amend_type
|
||||||
|
)
|
||||||
|
context['type'] = _(amend.get_type_display())
|
||||||
|
amend.comments = _("%(type)s of %(related_type)s %(number)s and creation date %(date)s") % context
|
||||||
|
amend.save(update_fields=('comments',))
|
||||||
|
for tax, subtotals in bill.compute_subtotals().items():
|
||||||
|
context['tax'] = tax
|
||||||
|
line = BillLine.objects.create(
|
||||||
|
bill=amend,
|
||||||
|
start_on=bill.created_on,
|
||||||
|
description=_("Amend of %(related_type)s %(number)s, tax %(tax)s%%") % context,
|
||||||
|
subtotal=subtotals[0],
|
||||||
|
tax=tax
|
||||||
|
)
|
||||||
|
ids.append(bill.pk)
|
||||||
|
amend_url = reverse('admin:bills_bill_changelist')
|
||||||
|
amend_url += '?id=%s' % ','.join(map(str, ids))
|
||||||
|
messages.success(request, mark_safe(ungettext(
|
||||||
|
_('<a href="%s">One amendment bill</a> have been generated.') % amend_url,
|
||||||
|
_('<a href="%s">%i amendment bills</a> have been generated.') % (amend_url, len(ids)),
|
||||||
|
len(ids)
|
||||||
|
)))
|
||||||
|
amend_bills.verbose_name = _("Amend")
|
||||||
|
amend_bills.url_name = 'amend'
|
||||||
|
|
|
@ -196,10 +196,11 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
search_fields = ('number', 'account__username', 'comments')
|
search_fields = ('number', 'account__username', 'comments')
|
||||||
change_view_actions = [
|
change_view_actions = [
|
||||||
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
|
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
|
||||||
actions.close_bills
|
actions.close_bills, actions.amend_bills,
|
||||||
]
|
]
|
||||||
actions = [
|
actions = [
|
||||||
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills
|
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills,
|
||||||
|
actions.amend_bills,
|
||||||
]
|
]
|
||||||
change_readonly_fields = ('account_link', 'type', 'is_open')
|
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||||
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
||||||
|
|
|
@ -20,7 +20,7 @@ class BillTypeListFilter(SimpleListFilter):
|
||||||
('invoice', _("Invoice")),
|
('invoice', _("Invoice")),
|
||||||
('amendmentinvoice', _("Amendment invoice")),
|
('amendmentinvoice', _("Amendment invoice")),
|
||||||
('fee', _("Fee")),
|
('fee', _("Fee")),
|
||||||
('fee', _("Amendment fee")),
|
('amendmentfee', _("Amendment fee")),
|
||||||
('proforma', _("Pro-forma")),
|
('proforma', _("Pro-forma")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,10 +31,11 @@ class BillTypeListFilter(SimpleListFilter):
|
||||||
return self.request.path.split('/')[-2]
|
return self.request.path.split('/')[-2]
|
||||||
|
|
||||||
def choices(self, cl):
|
def choices(self, cl):
|
||||||
|
query = self.request.GET.urlencode()
|
||||||
for lookup, title in self.lookup_choices:
|
for lookup, title in self.lookup_choices:
|
||||||
yield {
|
yield {
|
||||||
'selected': self.value() == lookup,
|
'selected': self.value() == lookup,
|
||||||
'query_string': reverse('admin:bills_%s_changelist' % lookup),
|
'query_string': reverse('admin:bills_%s_changelist' % lookup) + '?%s' % query,
|
||||||
'display': title,
|
'display': title,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -60,10 +60,17 @@ class BillManager(models.Manager):
|
||||||
|
|
||||||
class Bill(models.Model):
|
class Bill(models.Model):
|
||||||
OPEN = ''
|
OPEN = ''
|
||||||
|
CREATED = 'CREATED'
|
||||||
|
PROCESSED = 'PROCESSED'
|
||||||
|
AMENDED = 'AMENDED'
|
||||||
PAID = 'PAID'
|
PAID = 'PAID'
|
||||||
PENDING = 'PENDING'
|
PENDING = 'PENDING'
|
||||||
BAD_DEBT = 'BAD_DEBT'
|
BAD_DEBT = 'BAD_DEBT'
|
||||||
PAYMENT_STATES = (
|
PAYMENT_STATES = (
|
||||||
|
(OPEN, _("Open")),
|
||||||
|
(CREATED, _("Created")),
|
||||||
|
(PROCESSED, _("Processed")),
|
||||||
|
(AMENDED, _("Amended")),
|
||||||
(PAID, _("Paid")),
|
(PAID, _("Paid")),
|
||||||
(PENDING, _("Pending")),
|
(PENDING, _("Pending")),
|
||||||
(BAD_DEBT, _("Bad debt")),
|
(BAD_DEBT, _("Bad debt")),
|
||||||
|
@ -84,7 +91,8 @@ class Bill(models.Model):
|
||||||
|
|
||||||
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
related_name='%(class)s')
|
||||||
|
# amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"), related_name='amends')
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateField(_("closed on"), blank=True, null=True)
|
closed_on = models.DateField(_("closed on"), blank=True, null=True)
|
||||||
|
@ -125,6 +133,9 @@ class Bill(models.Model):
|
||||||
def payment_state(self):
|
def payment_state(self):
|
||||||
if self.is_open or self.get_type() == self.PROFORMA:
|
if self.is_open or self.get_type() == self.PROFORMA:
|
||||||
return self.OPEN
|
return self.OPEN
|
||||||
|
# elif self.amends.filter(is_open=False).exists():
|
||||||
|
# return self.AMENDED
|
||||||
|
# TODO optimize this with a single query
|
||||||
secured = self.transactions.secured().amount() or 0
|
secured = self.transactions.secured().amount() or 0
|
||||||
if abs(secured) >= abs(self.get_total()):
|
if abs(secured) >= abs(self.get_total()):
|
||||||
return self.PAID
|
return self.PAID
|
||||||
|
@ -151,6 +162,16 @@ class Bill(models.Model):
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
return self.type or self.get_class_type()
|
return self.type or self.get_class_type()
|
||||||
|
|
||||||
|
def get_amend_type(self):
|
||||||
|
amend_map = {
|
||||||
|
self.INVOICE: self.AMENDMENTINVOICE,
|
||||||
|
self.FEE: self.AMENDMENTFEE,
|
||||||
|
}
|
||||||
|
amend_type = amend_map.get(self.type)
|
||||||
|
if amend_type is None:
|
||||||
|
raise TypeError("%s has no associated amend type." % self.type)
|
||||||
|
return amend_type
|
||||||
|
|
||||||
def get_number(self):
|
def get_number(self):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
bill_type = self.get_type()
|
bill_type = self.get_type()
|
||||||
|
@ -298,7 +319,8 @@ class BillLine(models.Model):
|
||||||
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
|
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
||||||
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
||||||
|
decimal_places=2)
|
||||||
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
|
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
|
||||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||||
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
|
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
<br>
|
<br>
|
||||||
{% for line in lines %}
|
{% for line in lines %}
|
||||||
{% with sublines=line.sublines.all description=line.description|slice:"38:" %}
|
{% with sublines=line.sublines.all description=line.description|slice:"38:" %}
|
||||||
<span class="{% if not sublines and not description %}last {% endif %}column-id">{% if not line.order_id %}L{% endif %}{{ line.order_id }}</span>
|
<span class="{% if not sublines and not description %}last {% endif %}column-id">{% if not line.order_id %}L{% endif %}{{ line.order_id|default:line.pk }}</span>
|
||||||
<span class="{% if not sublines and not description %}last {% endif %}column-description">{{ line.description|slice:":38" }}</span>
|
<span class="{% if not sublines and not description %}last {% endif %}column-description">{{ line.description|slice:":38" }}</span>
|
||||||
<span class="{% if not sublines and not description %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
|
<span class="{% if not sublines and not description %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
|
||||||
<span class="{% if not sublines and not description %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
<span class="{% if not sublines and not description %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
||||||
|
|
|
@ -36,7 +36,10 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
'name', 'account_link', 'display_filtering', 'display_addresses', 'display_active',
|
'name', 'account_link', 'display_filtering', 'display_addresses', 'display_active',
|
||||||
)
|
)
|
||||||
list_filter = (IsActiveListFilter, HasAddressListFilter, 'filtering')
|
list_filter = (IsActiveListFilter, HasAddressListFilter, 'filtering')
|
||||||
search_fields = ('account__username', 'account__short_name', 'account__full_name', 'name')
|
search_fields = (
|
||||||
|
'account__username', 'account__short_name', 'account__full_name', 'name',
|
||||||
|
'addresses__name', 'addresses__domain__name',
|
||||||
|
)
|
||||||
add_fieldsets = (
|
add_fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('account_link', 'name', 'password1', 'password2', 'filtering'),
|
'fields': ('account_link', 'name', 'password1', 'password2', 'filtering'),
|
||||||
|
@ -111,6 +114,13 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
form.modeladmin = self
|
form.modeladmin = self
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
def get_search_results(self, request, queryset, search_term):
|
||||||
|
# Remove local domain from the search term if present (implicit local addreç)
|
||||||
|
search_term = search_term.replace('@'+settings.MAILBOXES_LOCAL_DOMAIN, '')
|
||||||
|
# Split address name from domain in order to support address searching
|
||||||
|
search_term = search_term.replace('@', ' ')
|
||||||
|
return super(MailboxAdmin, self).get_search_results(request, queryset, search_term)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
""" save hacky mailbox.addresses """
|
""" save hacky mailbox.addresses """
|
||||||
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
||||||
|
|
|
@ -240,13 +240,13 @@ class PostfixAddressVirtualDomainBackend(ServiceController):
|
||||||
('MAILBOXES_LOCAL_DOMAIN', 'MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH')
|
('MAILBOXES_LOCAL_DOMAIN', 'MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH')
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_local_domain(self, domain):
|
def is_hosted_domain(self, domain):
|
||||||
""" whether or not domain MX points to this server """
|
""" whether or not domain MX points to this server """
|
||||||
return domain.has_default_mx()
|
return domain.has_default_mx()
|
||||||
|
|
||||||
def include_virtual_alias_domain(self, context):
|
def include_virtual_alias_domain(self, context):
|
||||||
domain = context['domain']
|
domain = context['domain']
|
||||||
if domain.name != context['local_domain'] and self.is_local_domain(domain):
|
if domain.name != context['local_domain'] and self.is_hosted_domain(domain):
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
# %(domain)s is a virtual domain belonging to this server
|
# %(domain)s is a virtual domain belonging to this server
|
||||||
if [[ ! $(grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s) ]]; then
|
if [[ ! $(grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s) ]]; then
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_link, admin_date, admin_colored, display_mono
|
from orchestra.admin.utils import admin_link, admin_date, admin_colored, display_mono, display_code
|
||||||
|
|
||||||
from . import settings, helpers
|
from . import settings, helpers
|
||||||
from .backends import ServiceBackend
|
from .backends import ServiceBackend
|
||||||
|
@ -122,7 +122,7 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
inlines = (BackendOperationInline,)
|
inlines = (BackendOperationInline,)
|
||||||
fields = (
|
fields = (
|
||||||
'backend', 'server_link', 'state', 'mono_script', 'mono_stdout',
|
'backend', 'server_link', 'state', 'display_script', 'mono_stdout',
|
||||||
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
|
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
|
||||||
'execution_time'
|
'execution_time'
|
||||||
)
|
)
|
||||||
|
@ -131,11 +131,16 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
server_link = admin_link('server')
|
server_link = admin_link('server')
|
||||||
display_created = admin_date('created_at', short_description=_("Created"))
|
display_created = admin_date('created_at', short_description=_("Created"))
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
mono_script = display_mono('script')
|
display_script = display_code('script')
|
||||||
mono_stdout = display_mono('stdout')
|
mono_stdout = display_mono('stdout')
|
||||||
mono_stderr = display_mono('stderr')
|
mono_stderr = display_mono('stderr')
|
||||||
mono_traceback = display_mono('traceback')
|
mono_traceback = display_mono('traceback')
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
'all': ('orchestra/css/pygments/github.css',)
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(BackendLogAdmin, self).get_queryset(request)
|
qs = super(BackendLogAdmin, self).get_queryset(request)
|
||||||
|
|
|
@ -61,8 +61,9 @@ def send_report(method, args, log):
|
||||||
backend = method.__self__.__class__.__name__
|
backend = method.__self__.__class__.__name__
|
||||||
subject = '[Orchestra] %s execution %s on %s' % (backend, log.state, server)
|
subject = '[Orchestra] %s execution %s on %s' % (backend, log.state, server)
|
||||||
separator = "\n%s\n\n" % ('~ '*40,)
|
separator = "\n%s\n\n" % ('~ '*40,)
|
||||||
print(log.operations.all())
|
|
||||||
operations = '\n'.join([' '.join((op.action, get_instance_url(op))) for op in log.operations.all()])
|
operations = '\n'.join([' '.join((op.action, get_instance_url(op))) for op in log.operations.all()])
|
||||||
|
log_url = reverse('admin:orchestration_backendlog_change', args=(log.pk,))
|
||||||
|
log_url = orchestra_settings.ORCHESTRA_SITE_URL + log_url
|
||||||
message = separator.join([
|
message = separator.join([
|
||||||
"[EXIT CODE] %s" % log.exit_code,
|
"[EXIT CODE] %s" % log.exit_code,
|
||||||
"[STDERR]\n%s" % log.stderr,
|
"[STDERR]\n%s" % log.stderr,
|
||||||
|
@ -70,6 +71,7 @@ def send_report(method, args, log):
|
||||||
"[SCRIPT]\n%s" % log.script,
|
"[SCRIPT]\n%s" % log.script,
|
||||||
"[TRACEBACK]\n%s" % log.traceback,
|
"[TRACEBACK]\n%s" % log.traceback,
|
||||||
"[OPERATIONS]\n%s" % operations,
|
"[OPERATIONS]\n%s" % operations,
|
||||||
|
"[BACKEND LOG] %s" % log_url,
|
||||||
])
|
])
|
||||||
html_message = '\n\n'.join([
|
html_message = '\n\n'.join([
|
||||||
'<h4 style="color:#505050;">Exit code %s</h4>' % log.exit_code,
|
'<h4 style="color:#505050;">Exit code %s</h4>' % log.exit_code,
|
||||||
|
@ -83,6 +85,7 @@ def send_report(method, args, log):
|
||||||
'<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(log.traceback),
|
'<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(log.traceback),
|
||||||
'<h4 style="color:#505050;">Operations</h4>'
|
'<h4 style="color:#505050;">Operations</h4>'
|
||||||
'<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(operations),
|
'<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(operations),
|
||||||
|
'<h4 style="color:#505050;">Backend log <a href="%s">%s</h4>' % (log_url, log_url),
|
||||||
])
|
])
|
||||||
mail_admins(subject, message, html_message=html_message)
|
mail_admins(subject, message, html_message=html_message)
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,11 @@ class Command(BaseCommand):
|
||||||
server = Server(name=server, address=server)
|
server = Server(name=server, address=server)
|
||||||
server.full_clean()
|
server.full_clean()
|
||||||
server.save()
|
server.save()
|
||||||
routes.append(AttrDict(host=server, async=False))
|
routes.append(AttrDict(
|
||||||
|
host=server,
|
||||||
|
async=False,
|
||||||
|
action_is_async=lambda self: False,
|
||||||
|
))
|
||||||
# Generate operations for the given backend
|
# Generate operations for the given backend
|
||||||
for instance in queryset:
|
for instance in queryset:
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
|
@ -79,7 +83,7 @@ class Command(BaseCommand):
|
||||||
route, __, __ = key
|
route, __, __ = key
|
||||||
backend, operations = value
|
backend, operations = value
|
||||||
servers.append(str(route.host))
|
servers.append(str(route.host))
|
||||||
self.stdout.write('# Execute on %s' % route.host)
|
self.stdout.write('# Execute %s on %s' % (backend.get_name(), route.host))
|
||||||
for method, commands in backend.scripts:
|
for method, commands in backend.scripts:
|
||||||
script = '\n'.join(commands)
|
script = '\n'.join(commands)
|
||||||
self.stdout.write(script)
|
self.stdout.write(script)
|
||||||
|
|
|
@ -96,6 +96,7 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
change_readonly_fields = ('amount', 'currency')
|
change_readonly_fields = ('amount', 'currency')
|
||||||
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link')
|
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link')
|
||||||
list_select_related = ('source', 'bill__account')
|
list_select_related = ('source', 'bill__account')
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
bill_link = admin_link('bill')
|
bill_link = admin_link('bill')
|
||||||
source_link = admin_link('source')
|
source_link = admin_link('source')
|
||||||
|
|
|
@ -76,7 +76,7 @@ class TransactionQuerySet(models.QuerySet):
|
||||||
return self.exclude(state=Transaction.REJECTED)
|
return self.exclude(state=Transaction.REJECTED)
|
||||||
|
|
||||||
def amount(self):
|
def amount(self):
|
||||||
return next(iter(self.aggregate(models.Sum('amount')).values()))
|
return next(iter(self.aggregate(models.Sum('amount')).values())) or 0
|
||||||
|
|
||||||
def processing(self):
|
def processing(self):
|
||||||
return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION])
|
return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION])
|
||||||
|
|
|
@ -141,17 +141,19 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
super(PHPBackend, self).prepare()
|
super(PHPBackend, self).prepare()
|
||||||
# Coordinate apache restart with php backend in order not to overdo it
|
# Coordinate apache restart with php backend in order not to overdo it
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""
|
||||||
backend="PHPBackend"
|
backend="PHPBackend"
|
||||||
echo "$backend" >> /dev/shm/restart.apache2
|
echo "$backend" >> /dev/shm/restart.apache2""")
|
||||||
""")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
|
context = {
|
||||||
|
'reload_pool': settings.WEBAPPS_PHPFPM_RELOAD_POOL,
|
||||||
|
}
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
# Apply changes if needed
|
# Apply changes if needed
|
||||||
if [[ $UPDATED_FPM -eq 1 ]]; then
|
if [[ $UPDATED_FPM -eq 1 ]]; then
|
||||||
service php5-fpm reload
|
%(reload_pool)s
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Coordinate Apache restart with other concurrent backends (e.g. Apache2Backend)
|
# Coordinate Apache restart with other concurrent backends (e.g. Apache2Backend)
|
||||||
|
@ -182,7 +184,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
mv /dev/shm/restart.apache2.locked /dev/shm/restart.apache2
|
mv /dev/shm/restart.apache2.locked /dev/shm/restart.apache2
|
||||||
fi
|
fi
|
||||||
# End of coordination
|
# End of coordination
|
||||||
""")
|
""") % context
|
||||||
)
|
)
|
||||||
super(PHPBackend, self).commit()
|
super(PHPBackend, self).commit()
|
||||||
|
|
||||||
|
@ -207,13 +209,10 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
pm = ondemand
|
pm = ondemand
|
||||||
pm.max_requests = {{ max_requests }}
|
pm.max_requests = {{ max_requests }}
|
||||||
pm.max_children = {{ max_children }}
|
pm.max_children = {{ max_children }}
|
||||||
|
{% if request_terminate_timeout %}
|
||||||
{% if request_terminate_timeout %}\
|
request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
|
||||||
request_terminate_timeout = {{ request_terminate_timeout }}\
|
{% for name, value in init_vars.items %}
|
||||||
{% endif %}
|
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
|
||||||
{% for name, value in init_vars.items %}\
|
|
||||||
php_admin_value[{{ name | safe }}] = {{ value | safe }}\
|
|
||||||
{% endfor %}
|
|
||||||
"""
|
"""
|
||||||
))
|
))
|
||||||
return fpm_config.render(Context(context))
|
return fpm_config.render(Context(context))
|
||||||
|
|
|
@ -33,6 +33,9 @@ class WordPressBackend(WebAppServiceMixin, ServiceController):
|
||||||
echo "ERROR: execution returned non-zero code: $exit_code. cmd was:\\n$cmd\\n";
|
echo "ERROR: execution returned non-zero code: $exit_code. cmd was:\\n$cmd\\n";
|
||||||
exit($exit_code);
|
exit($exit_code);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function wp_new_blog_notification($blog_title, $blog_url, $user_id, $password){
|
||||||
|
// do nothing
|
||||||
}""")
|
}""")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -113,9 +116,6 @@ class WordPressBackend(WebAppServiceMixin, ServiceController):
|
||||||
$_POST['admin_password'] = "%(password)s";
|
$_POST['admin_password'] = "%(password)s";
|
||||||
$_POST['admin_password2'] = "%(password)s";
|
$_POST['admin_password2'] = "%(password)s";
|
||||||
|
|
||||||
function wp_new_blog_notification($blog_title, $blog_url, $user_id, $password){
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
ob_start();
|
ob_start();
|
||||||
require_once('%(app_path)s/wp-admin/install.php');
|
require_once('%(app_path)s/wp-admin/install.php');
|
||||||
$response = ob_get_contents();
|
$response = ob_get_contents();
|
||||||
|
|
|
@ -35,6 +35,10 @@ WEBAPPS_PHPFPM_POOL_PATH = Setting('WEBAPPS_PHPFPM_POOL_PATH',
|
||||||
validators=[Setting.string_format_validator(_php_names)],
|
validators=[Setting.string_format_validator(_php_names)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
WEBAPPS_PHPFPM_RELOAD_POOL = Setting('WEBAPPS_PHPFPM_RELOAD_POOL',
|
||||||
|
'service php5-fpm reload'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
WEBAPPS_FCGID_WRAPPER_PATH = Setting('WEBAPPS_FCGID_WRAPPER_PATH',
|
WEBAPPS_FCGID_WRAPPER_PATH = Setting('WEBAPPS_FCGID_WRAPPER_PATH',
|
||||||
'/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper',
|
'/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper',
|
||||||
|
|
|
@ -65,10 +65,11 @@ class PHPApp(AppType):
|
||||||
'webapp_id': self.instance.pk,
|
'webapp_id': self.instance.pk,
|
||||||
}
|
}
|
||||||
if merge:
|
if merge:
|
||||||
|
php_version = self.instance.data.get('php_version', self.DEFAULT_PHP_VERSION)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
# webapp__type is not used because wordpress != php != symlink...
|
# webapp__type is not used because wordpress != php != symlink...
|
||||||
'webapp__account': self.instance.account_id,
|
'webapp__account': self.instance.account_id,
|
||||||
'webapp__data__contains': '"php_version":"%s"' % self.instance.data['php_version'],
|
'webapp__data__contains': '"php_version":"%s"' % php_version,
|
||||||
}
|
}
|
||||||
return self.instance.get_options(**kwargs)
|
return self.instance.get_options(**kwargs)
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ class Apache2Backend(ServiceController):
|
||||||
|
|
||||||
def render_virtual_host(self, site, context, ssl=False):
|
def render_virtual_host(self, site, context, ssl=False):
|
||||||
context['port'] = self.HTTPS_PORT if ssl else self.HTTP_PORT
|
context['port'] = self.HTTPS_PORT if ssl else self.HTTP_PORT
|
||||||
|
context['vhost_wrapper_dirs'] = []
|
||||||
extra_conf = self.get_content_directives(site, context)
|
extra_conf = self.get_content_directives(site, context)
|
||||||
directives = site.get_directives()
|
directives = site.get_directives()
|
||||||
if ssl:
|
if ssl:
|
||||||
|
@ -141,10 +142,9 @@ class Apache2Backend(ServiceController):
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
super(Apache2Backend, self).prepare()
|
super(Apache2Backend, self).prepare()
|
||||||
# Coordinate apache restart with php backend in order not to overdo it
|
# Coordinate apache restart with php backend in order not to overdo it
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""
|
||||||
backend="Apache2Backend"
|
backend="Apache2Backend"
|
||||||
echo "$backend" >> /dev/shm/restart.apache2\
|
echo "$backend" >> /dev/shm/restart.apache2""")
|
||||||
""")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
|
@ -187,8 +187,8 @@ class Apache2Backend(ServiceController):
|
||||||
try:
|
try:
|
||||||
method = getattr(self, 'get_%s_directives' % method)
|
method = getattr(self, 'get_%s_directives' % method)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise AttributeError("%s does not has suport for '%s' directive." %
|
context = (self.__class__.__name__, method)
|
||||||
(self.__class__.__name__, method))
|
raise AttributeError("%s does not has suport for '%s' directive." % context)
|
||||||
return method(context, *args)
|
return method(context, *args)
|
||||||
|
|
||||||
def get_content_directives(self, site, context):
|
def get_content_directives(self, site, context):
|
||||||
|
@ -238,10 +238,10 @@ class Apache2Backend(ServiceController):
|
||||||
directives = ''
|
directives = ''
|
||||||
# This Action trick is used instead of FcgidWrapper because we don't want to define
|
# This Action trick is used instead of FcgidWrapper because we don't want to define
|
||||||
# a new fcgid process class each time an app is mounted (num proc limits enforcement).
|
# a new fcgid process class each time an app is mounted (num proc limits enforcement).
|
||||||
if 'wrapper_dir' not in context:
|
context['wrapper_dir'] = os.path.dirname(wrapper_path)
|
||||||
|
if context['wrapper_dir'] not in context['vhost_wrapper_dirs']:
|
||||||
# fcgi-bin only needs to be defined once per vhots
|
# fcgi-bin only needs to be defined once per vhots
|
||||||
# We assume that all account wrapper paths will share the same dir
|
# We assume that all account wrapper paths will share the same dir
|
||||||
context['wrapper_dir'] = os.path.dirname(wrapper_path)
|
|
||||||
directives = textwrap.dedent("""\
|
directives = textwrap.dedent("""\
|
||||||
Alias /fcgi-bin/ %(wrapper_dir)s/
|
Alias /fcgi-bin/ %(wrapper_dir)s/
|
||||||
<Location /fcgi-bin/>
|
<Location /fcgi-bin/>
|
||||||
|
@ -249,6 +249,7 @@ class Apache2Backend(ServiceController):
|
||||||
Options +ExecCGI
|
Options +ExecCGI
|
||||||
</Location>
|
</Location>
|
||||||
""") % context
|
""") % context
|
||||||
|
context['vhost_wrapper_dirs'].append(context['wrapper_dir'])
|
||||||
directives += self.get_location_filesystem_map(context)
|
directives += self.get_location_filesystem_map(context)
|
||||||
directives += textwrap.dedent("""
|
directives += textwrap.dedent("""
|
||||||
ProxyPass %(location)s/ !
|
ProxyPass %(location)s/ !
|
||||||
|
@ -279,26 +280,35 @@ class Apache2Backend(ServiceController):
|
||||||
ca = [settings.WEBSITES_DEFAULT_SSL_CA]
|
ca = [settings.WEBSITES_DEFAULT_SSL_CA]
|
||||||
if not (cert and key):
|
if not (cert and key):
|
||||||
return []
|
return []
|
||||||
config = "SSLEngine on\n"
|
ssl_config = [
|
||||||
config += "SSLCertificateFile %s\n" % cert[0]
|
"SSLEngine on",
|
||||||
config += "SSLCertificateKeyFile %s\n" % key[0]
|
"SSLCertificateFile %s" % cert[0],
|
||||||
|
"SSLCertificateKeyFile %s" % key[0],
|
||||||
|
]
|
||||||
if ca:
|
if ca:
|
||||||
config += "SSLCACertificateFile %s\n" % ca[0]
|
ssl_config.append("SSLCACertificateFile %s" % ca[0])
|
||||||
return [
|
return [
|
||||||
('', config),
|
('', '\n'.join(ssl_config)),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_security(self, directives):
|
def get_security(self, directives):
|
||||||
security = []
|
remove_rules = []
|
||||||
for values in directives.get('sec-rule-remove', []):
|
for values in directives.get('sec-rule-remove', []):
|
||||||
for rule in values.split():
|
for rule in values.split():
|
||||||
sec_rule = "SecRuleRemoveById %i" % int(rule)
|
sec_rule = " SecRuleRemoveById %i" % int(rule)
|
||||||
security.append(('', sec_rule))
|
remove_rules.append(sec_rule)
|
||||||
|
security = []
|
||||||
|
if remove_rules:
|
||||||
|
remove_rules.insert(0, '<IfModule mod_security2.c>')
|
||||||
|
remove_rules.append('</IfModule>')
|
||||||
|
security.append(('', '\n'.join(remove_rules)))
|
||||||
for location in directives.get('sec-engine', []):
|
for location in directives.get('sec-engine', []):
|
||||||
sec_rule = textwrap.dedent("""\
|
sec_rule = textwrap.dedent("""\
|
||||||
<Location %s>
|
<IfModule mod_security2.c>
|
||||||
SecRuleEngine off
|
<Location %s>
|
||||||
</Location>""") % location
|
SecRuleEngine Off
|
||||||
|
</Location>
|
||||||
|
</IfModule>""") % location
|
||||||
security.append((location, sec_rule))
|
security.append((location, sec_rule))
|
||||||
return security
|
return security
|
||||||
|
|
||||||
|
@ -466,9 +476,8 @@ class Apache2Traffic(ServiceMonitor):
|
||||||
self.append('monitor {object_id} "{last_date}" {log_file}'.format(**context))
|
self.append('monitor {object_id} "{last_date}" {log_file}'.format(**context))
|
||||||
|
|
||||||
def get_context(self, site):
|
def get_context(self, site):
|
||||||
context = {
|
return {
|
||||||
'log_file': '%s{,.1}' % site.get_www_access_log_path(),
|
'log_file': '%s{,.1}' % site.get_www_access_log_path(),
|
||||||
'last_date': self.get_last_date(site.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
|
'last_date': self.get_last_date(site.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
|
||||||
'object_id': site.pk,
|
'object_id': site.pk,
|
||||||
}
|
}
|
||||||
return context
|
|
||||||
|
|
|
@ -105,9 +105,9 @@ WEBSITES_TRAFFIC_IGNORE_HOSTS = Setting('WEBSITES_TRAFFIC_IGNORE_HOSTS',
|
||||||
|
|
||||||
WEBSITES_SAAS_DIRECTIVES = Setting('WEBSITES_SAAS_DIRECTIVES',
|
WEBSITES_SAAS_DIRECTIVES = Setting('WEBSITES_SAAS_DIRECTIVES',
|
||||||
{
|
{
|
||||||
'wordpress-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock', '/home/httpd/wordpress-mu/'),
|
'wordpress-saas': ('fpm', '/var/run/fpm/pangea-5.4-fpm.sock', '/home/httpd/wordpress-mu/'),
|
||||||
'drupal-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock','/home/httpd/drupal-mu/'),
|
'drupal-saas': ('fpm', '/var/run/fpm/pangea-5.4-fpm.sock','/home/httpd/drupal-mu/'),
|
||||||
'dokuwiki-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock','/home/httpd/moodle-mu/'),
|
'dokuwiki-saas': ('fpm', '/var/run/fpm/pangea-5.4-fpm.sock','/home/httpd/moodle-mu/'),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue