Added support for multiple php fpm webapps

This commit is contained in:
Marc Aymerich 2015-06-12 12:17:05 +00:00
parent d6bc0daae5
commit 81f5ef5686
11 changed files with 100 additions and 30 deletions

View File

@ -427,3 +427,6 @@ serailzer self.instance on create.
# IF modsecurity... and Merge websites locations # IF modsecurity... and Merge websites locations
# backend email error log with links to instances
# PHP backend multiple FPM directories support

View File

@ -111,7 +111,7 @@ class Bind9MasterDomainBackend(ServiceController):
from orchestra.contrib.orchestration.manager import router from orchestra.contrib.orchestration.manager import router
operation = Operation(backend, domain, Operation.SAVE) operation = Operation(backend, domain, Operation.SAVE)
servers = [] servers = []
for routes in router.get_routes(operation): for route in router.get_routes(operation):
servers.append(route.host.get_ip()) servers.append(route.host.get_ip())
return servers return servers
@ -125,6 +125,7 @@ class Bind9MasterDomainBackend(ServiceController):
ips = [] ips = []
masters_ips = self.get_masters_ips(domain) masters_ips = self.get_masters_ips(domain)
records = domain.get_records() records = domain.get_records()
# Slaves from NS
for record in records.by_type(Record.NS): for record in records.by_type(Record.NS):
hostname = record.value.rstrip('.') hostname = record.value.rstrip('.')
# First try with a DNS query, a more reliable source # First try with a DNS query, a more reliable source
@ -141,6 +142,10 @@ class Bind9MasterDomainBackend(ServiceController):
addr = records.by_type(Record.A)[0].value addr = records.by_type(Record.A)[0].value
if addr not in masters_ips: if addr not in masters_ips:
ips.append(addr) ips.append(addr)
# Slaves from internal networks
if not settings.DOMAINS_MASTERS:
for server in self.get_servers(domain, Bind9SlaveDomainBackend):
ips.append(server)
return OrderedSet(sorted(ips)) return OrderedSet(sorted(ips))
def get_context(self, domain): def get_context(self, domain):

View File

@ -2,11 +2,14 @@ import textwrap
from django.contrib import messages from django.contrib import messages
from django.core.mail import mail_admins from django.core.mail import mail_admins
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.html import escape from django.utils.html import escape
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 _
from orchestra import settings as orchestra_settings
from orchestra.admin.utils import change_url
def get_backends_help_text(backends): def get_backends_help_text(backends):
help_texts = {} help_texts = {}
@ -44,17 +47,29 @@ def get_backends_help_text(backends):
return help_texts return help_texts
def get_instance_url(operation):
try:
url = change_url(operation.instance)
except NoReverseMatch:
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
(escape(operation.content_type), escape(operation.object_id))))
return orchestra_settings.ORCHESTRA_SITE_URL + url
def send_report(method, args, log): def send_report(method, args, log):
server = args[0] server = args[0]
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()])
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,
"[STDOUT]\n%s" % log.stdout, "[STDOUT]\n%s" % log.stdout,
"[SCRIPT]\n%s" % log.script, "[SCRIPT]\n%s" % log.script,
"[TRACEBACK]\n%s" % log.traceback, "[TRACEBACK]\n%s" % log.traceback,
"[OPERATIONS]\n%s" % operations,
]) ])
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,
@ -66,6 +81,8 @@ def send_report(method, args, log):
'<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(log.script), '<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(log.script),
'<h4 style="color:#505050;">Traceback</h4>' '<h4 style="color:#505050;">Traceback</h4>'
'<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>'
'<pre style="margin-left:20px;font-size:11px">%s</pre>' % escape(operations),
]) ])
mail_admins(subject, message, html_message=html_message) mail_admins(subject, message, html_message=html_message)
@ -78,6 +95,7 @@ def get_backend_url(ids):
return url + '?id__in=%s' % ','.join(map(str, ids)) return url + '?id__in=%s' % ','.join(map(str, ids))
return '' return ''
def message_user(request, logs): def message_user(request, logs):
total, successes, async = 0, 0, 0 total, successes, async = 0, 0, 0
ids = [] ids = []

View File

@ -27,8 +27,6 @@ def keep_log(execute, log, operations):
log = kwargs['log'] log = kwargs['log']
try: try:
log = execute(*args, **kwargs) log = execute(*args, **kwargs)
if not log.is_success:
send_report(execute, args, log)
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()
log.state = log.EXCEPTION log.state = log.EXCEPTION
@ -44,6 +42,8 @@ def keep_log(execute, log, operations):
for operation in operations: for operation in operations:
logger.info("Executed %s" % str(operation)) logger.info("Executed %s" % str(operation))
operation.store(log) operation.store(log)
if not log.is_success:
send_report(execute, args, log)
stdout = log.stdout.strip() stdout = log.stdout.strip()
stdout and logger.debug('STDOUT %s', stdout) stdout and logger.debug('STDOUT %s', stdout)
stderr = log.stderr.strip() stderr = log.stderr.strip()

View File

@ -10,7 +10,7 @@ from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.forms.widgets import DynamicHelpTextSelect from orchestra.forms.widgets import DynamicHelpTextSelect
from orchestra.plugins.admin import SelectPluginAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin
from .filters import HasWebsiteListFilter from .filters import HasWebsiteListFilter, PHPVersionListFilter
from .models import WebApp, WebAppOption from .models import WebApp, WebAppOption
from .options import AppOption from .options import AppOption
from .types import AppType from .types import AppType
@ -49,7 +49,7 @@ class WebAppOptionInline(admin.TabularInline):
class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link') list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link')
list_filter = ('type', HasWebsiteListFilter) list_filter = ('type', HasWebsiteListFilter, PHPVersionListFilter)
inlines = [WebAppOptionInline] inlines = [WebAppOptionInline]
readonly_fields = ('account_link', ) readonly_fields = ('account_link', )
change_readonly_fields = ('name', 'type', 'display_websites') change_readonly_fields = ('name', 'type', 'display_websites')

View File

@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController from orchestra.contrib.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
from .. import settings from .. import settings, utils
class PHPBackend(WebAppServiceMixin, ServiceController): class PHPBackend(WebAppServiceMixin, ServiceController):
@ -36,10 +36,13 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
self.create_webapp_dir(context) self.create_webapp_dir(context)
if webapp.type_instance.is_fpm: if webapp.type_instance.is_fpm:
self.save_fpm(webapp, context) self.save_fpm(webapp, context)
self.delete_fcgid(webapp, context)
elif webapp.type_instance.is_fcgid: elif webapp.type_instance.is_fcgid:
self.save_fcgid(webapp, context) self.save_fcgid(webapp, context)
self.delete_fpm(webapp, context) else:
raise TypeError("Unknown PHP execution type")
# Clean php fcgid/fpm apps in order to effectively support change of php-version
self.delete_fcgid(webapp, context, preserve=True)
self.delete_fpm(webapp, context, preserve=True)
self.set_under_construction(context) self.set_under_construction(context)
def save_fpm(self, webapp, context): def save_fpm(self, webapp, context):
@ -103,16 +106,39 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
self.delete_fcgid(webapp, context) self.delete_fcgid(webapp, context)
self.delete_webapp_dir(context) self.delete_webapp_dir(context)
def delete_fpm(self, webapp, context): def has_sibilings(self, webapp, context):
# Better not delete a pool used by other apps return type(webapp).objects.filter(
if not self.MERGE: account=webapp.account_id,
self.append("rm -f %(fpm_path)s" % context) data__contains='"php_version":"%s"' % context['php_version'],
).exclude(id=webapp.pk).exists()
def delete_fcgid(self, webapp, context): def delete_fpm(self, webapp, context, preserve=False):
# Better not delete a wrapper used by other apps """ delete all pools in order to efectively support changing php-fpm version """
if not self.MERGE: context_copy = dict(context)
self.append("rm -f %(wrapper_path)s" % context) for php_version, verbose in settings.WEBAPPS_PHP_VERSIONS:
self.append("rm -f %(cmd_options_path)s" % context) if preserve and php_version == context['php_version']:
continue
php_version_number = utils.extract_version_number(php_version)
context_copy['php_version_number'] = php_version_number
if not self.MERGE or not self.has_sibilings(webapp, context_copy):
context_copy['fpm_path'] = settings.WEBAPPS_PHPFPM_POOL_PATH % context_copy
self.append("rm -f %(fpm_path)s" % context_copy)
def delete_fcgid(self, webapp, context, preserve=False):
""" delete all pools in order to efectively support changing php-fcgid version """
context_copy = dict(context)
for php_version, verbose in settings.WEBAPPS_PHP_VERSIONS:
if preserve and php_version == context['php_version']:
continue
php_version_number = utils.extract_version_number(php_version)
context_copy['php_version_number'] = php_version_number
if not self.MERGE or not self.has_sibilings(webapp, context_copy):
context_copy.update({
'wrapper_path': settings.WEBAPPS_FCGID_WRAPPER_PATH % context_copy,
'cmd_options_path': settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context_copy,
})
self.append("rm -f %(wrapper_path)s" % context_copy)
self.append("rm -f %(cmd_options_path)s" % context_copy)
def prepare(self): def prepare(self):
super(PHPBackend, self).prepare() super(PHPBackend, self).prepare()
@ -237,7 +263,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
return ' \\\n '.join(cmd_options) return ' \\\n '.join(cmd_options)
def update_fcgid_context(self, webapp, context): def update_fcgid_context(self, webapp, context):
wrapper_path = webapp.type_instance.FCGID_WRAPPER_PATH % context wrapper_path = settings.WEBAPPS_FCGID_WRAPPER_PATH % context
context.update({ context.update({
'wrapper': self.get_fcgid_wrapper(webapp, context), 'wrapper': self.get_fcgid_wrapper(webapp, context),
'wrapper_path': wrapper_path, 'wrapper_path': wrapper_path,

View File

@ -1,6 +1,8 @@
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from . import settings
class HasWebsiteListFilter(SimpleListFilter): class HasWebsiteListFilter(SimpleListFilter):
title = _("Has website") title = _("Has website")
@ -20,3 +22,15 @@ class HasWebsiteListFilter(SimpleListFilter):
return queryset return queryset
class PHPVersionListFilter(SimpleListFilter):
title = _("PHP version")
parameter_name = 'php_version'
def lookups(self, request, model_admin):
return settings.WEBAPPS_PHP_VERSIONS
def queryset(self, request, queryset):
value = self.value()
if value:
return queryset.filter(data__contains='"php_version":"%s"' % value)
return queryset

View File

@ -159,6 +159,12 @@ class PHPExtension(PHPAppOption):
regex = r'^[^ ]+$' regex = r'^[^ ]+$'
class PHPIncludePath(PHPAppOption):
name = 'include_path'
verbose_name = _("Include path")
regex = r'^[^ ]+$'
class PHPMagicQuotesGPC(PHPAppOption): class PHPMagicQuotesGPC(PHPAppOption):
name = 'magic_quotes_gpc' name = 'magic_quotes_gpc'
verbose_name = _("Magic quotes GPC") verbose_name = _("Magic quotes GPC")

View File

@ -30,7 +30,7 @@ WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = Setting('WEBAPPS_FPM_DEFAULT_MAX_CHILDREN',
WEBAPPS_PHPFPM_POOL_PATH = Setting('WEBAPPS_PHPFPM_POOL_PATH', WEBAPPS_PHPFPM_POOL_PATH = Setting('WEBAPPS_PHPFPM_POOL_PATH',
'/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf', '/etc/php%(php_version_number)s/fpm/pool.d/%(user)s-%(app_name)s.conf',
help_text="Available fromat names: <tt>%s</tt>" % ', '.join(_php_names), help_text="Available fromat names: <tt>%s</tt>" % ', '.join(_php_names),
validators=[Setting.string_format_validator(_php_names)], validators=[Setting.string_format_validator(_php_names)],
) )
@ -84,6 +84,8 @@ WEBAPPS_TYPES = Setting('WEBAPPS_TYPES', (
WEBAPPS_PHP_VERSIONS = Setting('WEBAPPS_PHP_VERSIONS', ( WEBAPPS_PHP_VERSIONS = Setting('WEBAPPS_PHP_VERSIONS', (
('5.6-fpm', 'PHP 5.6 FPM'),
('5.6-cgi', 'PHP 5.6 FCGID'),
('5.4-fpm', 'PHP 5.4 FPM'), ('5.4-fpm', 'PHP 5.4 FPM'),
('5.4-cgi', 'PHP 5.4 FCGID'), ('5.4-cgi', 'PHP 5.4 FCGID'),
('5.3-cgi', 'PHP 5.3 FCGID'), ('5.3-cgi', 'PHP 5.3 FCGID'),
@ -96,7 +98,7 @@ WEBAPPS_PHP_VERSIONS = Setting('WEBAPPS_PHP_VERSIONS', (
WEBAPPS_DEFAULT_PHP_VERSION = Setting('WEBAPPS_DEFAULT_PHP_VERSION', WEBAPPS_DEFAULT_PHP_VERSION = Setting('WEBAPPS_DEFAULT_PHP_VERSION',
'5.4-cgi', '5.6-fpm',
choices=WEBAPPS_PHP_VERSIONS choices=WEBAPPS_PHP_VERSIONS
) )
@ -223,6 +225,7 @@ WEBAPPS_ENABLED_OPTIONS = Setting('WEBAPPS_ENABLED_OPTIONS', (
'orchestra.contrib.webapps.options.PHPDefaultSocketTimeout', 'orchestra.contrib.webapps.options.PHPDefaultSocketTimeout',
'orchestra.contrib.webapps.options.PHPDisplayErrors', 'orchestra.contrib.webapps.options.PHPDisplayErrors',
'orchestra.contrib.webapps.options.PHPExtension', 'orchestra.contrib.webapps.options.PHPExtension',
'orchestra.contrib.webapps.options.PHPIncludePath',
'orchestra.contrib.webapps.options.PHPMagicQuotesGPC', 'orchestra.contrib.webapps.options.PHPMagicQuotesGPC',
'orchestra.contrib.webapps.options.PHPMagicQuotesRuntime', 'orchestra.contrib.webapps.options.PHPMagicQuotesRuntime',
'orchestra.contrib.webapps.options.PHPMaginQuotesSybase', 'orchestra.contrib.webapps.options.PHPMaginQuotesSybase',

View File

@ -1,5 +1,4 @@
import os import os
import re
from collections import OrderedDict from collections import OrderedDict
from django import forms from django import forms
@ -9,7 +8,7 @@ from rest_framework import serializers
from orchestra.plugins.forms import PluginDataForm from orchestra.plugins.forms import PluginDataForm
from orchestra.utils.functional import cached from orchestra.utils.functional import cached
from .. import settings from .. import settings, utils
from ..options import AppOption from ..options import AppOption
from . import AppType from . import AppType
@ -146,9 +145,5 @@ class PHPApp(AppType):
def get_php_version_number(self): def get_php_version_number(self):
php_version = self.get_php_version() php_version = self.get_php_version()
number = re.findall(r'[0-9]+\.?[0-9]?', php_version) return utils.extract_version_number(php_version)
if not number:
raise ValueError("No version number matches for '%s'" % php_version)
if len(number) > 1:
raise ValueError("Multiple version number matches for '%s'" % php_version)
return number[0]

View File

@ -152,7 +152,7 @@ class Command(BaseCommand):
'project_dir': paths.get_project_dir(), 'project_dir': paths.get_project_dir(),
'site_dir': paths.get_site_dir(), 'site_dir': paths.get_site_dir(),
'static_root': settings.STATIC_ROOT, 'static_root': settings.STATIC_ROOT,
'static_url': (settings.STATIC_URL or '/static').rstrip('/') 'static_url': (settings.STATIC_URL or '/static').rstrip('/'),
'user': user, 'user': user,
'group': options.get('group') or user, 'group': options.get('group') or user,
'home': expanduser("~%s" % options.get('user')), 'home': expanduser("~%s" % options.get('user')),