django-orchestra/orchestra/contrib/webapps/backends/php.py

320 lines
13 KiB
Python
Raw Normal View History

2015-03-12 14:05:23 +00:00
import os
import textwrap
2015-05-06 10:51:12 +00:00
from collections import OrderedDict
2015-03-12 14:05:23 +00:00
from django.template import Template
2015-03-12 14:05:23 +00:00
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController
2015-03-12 14:05:23 +00:00
from . import WebAppServiceMixin
from .. import settings, utils
2015-03-12 14:05:23 +00:00
2016-03-08 10:16:49 +00:00
class PHPController(WebAppServiceMixin, ServiceController):
2015-04-23 19:46:23 +00:00
"""
PHP support for apache-mod-fcgid and php-fpm.
It handles switching between these two PHP process management systemes.
"""
2015-04-24 11:39:20 +00:00
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS
2015-03-12 14:05:23 +00:00
verbose_name = _("PHP FPM/FCGID")
2015-03-27 19:50:54 +00:00
default_route_match = "webapp.type.endswith('php')"
2015-04-24 11:39:20 +00:00
doc_settings = (settings, (
'WEBAPPS_MERGE_PHP_WEBAPPS',
'WEBAPPS_FPM_DEFAULT_MAX_CHILDREN',
'WEBAPPS_PHP_CGI_BINARY_PATH',
'WEBAPPS_PHP_CGI_RC_DIR',
'WEBAPPS_PHP_CGI_INI_SCAN_DIR',
'WEBAPPS_FCGID_CMD_OPTIONS_PATH',
'WEBAPPS_PHPFPM_POOL_PATH',
'WEBAPPS_PHP_MAX_REQUESTS',
))
2015-03-12 14:05:23 +00:00
def save(self, webapp):
self.delete_old_config(webapp)
2015-03-12 14:05:23 +00:00
context = self.get_context(webapp)
2015-04-09 14:32:10 +00:00
self.create_webapp_dir(context)
2015-03-12 14:05:23 +00:00
if webapp.type_instance.is_fpm:
self.save_fpm(webapp, context)
elif webapp.type_instance.is_fcgid:
self.save_fcgid(webapp, context)
else:
raise TypeError("Unknown PHP execution type")
# LEGACY CLEANUP FUNCTIONS. TODO REMOVE WHEN SURE NOT NEEDED.
# self.delete_fcgid(webapp, context, preserve=True)
# self.delete_fpm(webapp, context, preserve=True)
2015-05-06 14:39:25 +00:00
self.set_under_construction(context)
def delete_config(self,webapp):
context = self.get_context(webapp)
to_delete = []
if webapp.type_instance.is_fpm:
to_delete.append(settings.WEBAPPS_PHPFPM_POOL_PATH % context)
to_delete.append(settings.WEBAPPS_FPM_LISTEN % context)
elif webapp.type_instance.is_fcgid:
to_delete.append(settings.WEBAPPS_FCGID_WRAPPER_PATH % context)
to_delete.append(settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context)
for item in to_delete:
self.append('rm -f "{}"'.format(item))
def delete_old_config(self,webapp):
# Check if we loaded the old version of the webapp. If so, we're updating
# rather than creating, so we must make sure the old config files are removed.
if hasattr(webapp, '_old_self'):
self.append("# Clean old configuration files")
self.delete_config(webapp._old_self)
else:
self.append("# No old config files to delete")
2015-03-12 14:05:23 +00:00
def save_fpm(self, webapp, context):
self.append(textwrap.dedent("""
# Generate FPM configuration
read -r -d '' fpm_config << 'EOF' || true
%(fpm_config)s
EOF
2015-03-12 14:05:23 +00:00
{
echo -e "${fpm_config}" | diff -N -I'^\s*;;' %(fpm_path)s -
2015-03-12 14:05:23 +00:00
} || {
echo -e "${fpm_config}" > %(fpm_path)s
UPDATED_FPM=1
2015-03-27 19:50:54 +00:00
}
""") % context
2015-03-12 14:05:23 +00:00
)
2015-03-12 14:05:23 +00:00
def save_fcgid(self, webapp, context):
self.append("mkdir -p %(wrapper_dir)s" % context)
self.append(textwrap.dedent("""
# Generate FCGID configuration
read -r -d '' wrapper << 'EOF' || true
%(wrapper)s
EOF
2015-03-12 14:05:23 +00:00
{
echo -e "${wrapper}" | diff -N -I'^\s*#' %(wrapper_path)s -
2015-03-12 14:05:23 +00:00
} || {
2015-03-27 19:50:54 +00:00
echo -e "${wrapper}" > %(wrapper_path)s
if [[ %(is_mounted)i -eq 1 ]]; then
2015-06-16 12:39:52 +00:00
# Reload fcgid wrapper (All PHP versions, because of version changing support)
2015-06-16 10:20:37 +00:00
pkill -SIGHUP -U %(user)s "^php[0-9\.]+-cgi$" || true
fi
2015-03-27 19:50:54 +00:00
}
2015-06-16 12:39:52 +00:00
chmod 550 %(wrapper_dir)s
chmod 550 %(wrapper_path)s
chown -R %(user)s:%(group)s %(wrapper_dir)s""") % context
2015-03-12 14:05:23 +00:00
)
if context['cmd_options']:
self.append(textwrap.dedent("""\
# FCGID options
read -r -d '' cmd_options << 'EOF' || true
%(cmd_options)s
EOF
2015-03-12 14:05:23 +00:00
{
echo -e "${cmd_options}" | diff -N -I'^\s*#' %(cmd_options_path)s -
2015-03-12 14:05:23 +00:00
} || {
2015-03-27 19:50:54 +00:00
echo -e "${cmd_options}" > %(cmd_options_path)s
[[ ${UPDATED_APACHE} -eq 0 ]] && UPDATED_APACHE=%(is_mounted)i
}
""" ) % context
2015-03-12 14:05:23 +00:00
)
else:
2015-06-16 12:39:52 +00:00
self.append("rm -f %(cmd_options_path)s\n" % context)
2015-03-12 14:05:23 +00:00
def delete(self, webapp):
context = self.get_context(webapp)
self.delete_old_config(webapp)
# if webapp.type_instance.is_fpm:
# self.delete_fpm(webapp, context)
# elif webapp.type_instance.is_fcgid:
# self.delete_fcgid(webapp, context)
2015-03-12 14:05:23 +00:00
self.delete_webapp_dir(context)
def has_sibilings(self, webapp, context):
return type(webapp).objects.filter(
account=webapp.account_id,
data__contains='"php_version":"%s"' % context['php_version'],
).exclude(id=webapp.pk).exists()
2015-06-16 10:20:37 +00:00
def all_versions_to_delete(self, webapp, context, preserve=False):
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)
2015-06-16 10:20:37 +00:00
context_copy['php_version'] = php_version
context_copy['php_version_number'] = php_version_number
if not self.MERGE or not self.has_sibilings(webapp, context_copy):
2015-06-16 10:20:37 +00:00
yield context_copy
2015-06-16 10:20:37 +00:00
def delete_fpm(self, webapp, context, preserve=False):
""" delete all pools in order to efectively support changing php-fpm version """
for context_copy in self.all_versions_to_delete(webapp, context, preserve):
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 """
2015-06-16 10:20:37 +00:00
for context_copy in self.all_versions_to_delete(webapp, context, preserve):
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)
2015-04-10 15:03:38 +00:00
def prepare(self):
2016-03-08 10:16:49 +00:00
super(PHPController, self).prepare()
2015-06-22 14:14:16 +00:00
self.append(textwrap.dedent("""
2016-03-08 10:16:49 +00:00
BACKEND="PHPController"
2016-03-04 09:46:39 +00:00
echo "$BACKEND" >> /dev/shm/reload.apache2
2016-03-04 09:46:39 +00:00
function coordinate_apache_reload () {
2016-03-08 10:16:49 +00:00
# Coordinate Apache reload with other concurrent backends (e.g. Apache2Controller)
2016-03-04 09:46:39 +00:00
is_last=0
counter=0
while ! mv /dev/shm/reload.apache2 /dev/shm/reload.apache2.locked; do
sleep 0.1;
if [[ $counter -gt 4 ]]; then
echo "[ERROR]: Apache reload synchronization deadlocked!" >&2
exit 10
fi
counter=$(($counter+1))
done
2016-04-27 08:35:13 +00:00
state="$(grep -v -E "^$BACKEND($|\s)" /dev/shm/reload.apache2.locked)" || is_last=1
2016-03-04 09:46:39 +00:00
[[ $is_last -eq 0 ]] && {
echo "$state" | grep -v ' RELOAD$' || is_last=1
}
if [[ $is_last -eq 1 ]]; then
echo "[DEBUG]: Last backend to run, update: $UPDATED_APACHE, state: '$state'"
if [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RELOAD$ ]]; then
if service apache2 status > /dev/null; then
service apache2 reload
else
service apache2 start
fi
fi
rm /dev/shm/reload.apache2.locked
else
2016-04-27 08:35:13 +00:00
echo "$state" > /dev/shm/reload.apache2.locked
2016-03-04 09:46:39 +00:00
if [[ $UPDATED_APACHE -eq 1 ]]; then
echo -e "[DEBUG]: Apache will be reloaded by another backend:\\n${state}"
echo "$BACKEND RELOAD" >> /dev/shm/reload.apache2.locked
fi
mv /dev/shm/reload.apache2.locked /dev/shm/reload.apache2
fi
}""")
2015-05-06 14:39:25 +00:00
)
2015-03-12 14:05:23 +00:00
def commit(self):
2015-06-22 14:14:16 +00:00
context = {
'reload_pool': settings.WEBAPPS_PHPFPM_RELOAD_POOL,
}
2015-04-10 15:03:38 +00:00
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_FPM -eq 1 ]]; then
2015-06-22 14:14:16 +00:00
%(reload_pool)s
2015-04-10 15:03:38 +00:00
fi
2016-03-04 09:46:39 +00:00
coordinate_apache_reload
2015-06-22 14:14:16 +00:00
""") % context
2015-04-10 15:03:38 +00:00
)
2016-03-08 10:16:49 +00:00
super(PHPController, self).commit()
2015-03-12 14:05:23 +00:00
def get_fpm_config(self, webapp, context):
2015-06-05 09:57:06 +00:00
options = webapp.type_instance.get_options()
2015-03-12 14:05:23 +00:00
context.update({
2015-03-23 15:36:51 +00:00
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
2015-04-14 14:29:22 +00:00
'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
'request_terminate_timeout': options.get('timeout', False),
2015-03-12 14:05:23 +00:00
})
context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context
fpm_config = Template(textwrap.dedent("""\
;; {{ banner }}
[{{ user }}-{{app_name}}]
2015-03-12 14:05:23 +00:00
user = {{ user }}
group = {{ group }}
2015-03-12 14:05:23 +00:00
listen = {{ fpm_listen | safe }}
listen.owner = {{ user }}
listen.group = {{ group }}
2015-03-12 14:05:23 +00:00
pm = ondemand
2015-04-02 16:14:55 +00:00
pm.max_requests = {{ max_requests }}
2015-04-09 14:32:10 +00:00
pm.max_children = {{ max_children }}
2015-06-22 14:14:16 +00:00
{% if request_terminate_timeout %}
request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
{% for name, value in init_vars.items %}
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
2015-03-12 14:05:23 +00:00
"""
))
return fpm_config.render(context)
2015-03-12 14:05:23 +00:00
def get_fcgid_wrapper(self, webapp, context):
opt = webapp.type_instance
# Format PHP init vars
2015-03-23 15:36:51 +00:00
init_vars = opt.get_php_init_vars(merge=self.MERGE)
2015-03-12 14:05:23 +00:00
if init_vars:
2015-05-22 13:15:06 +00:00
init_vars = [ " \\\n -d %s='%s'" % (k, v.replace("'", '"')) for k,v in init_vars.items() ]
init_vars = ''.join(init_vars)
2015-03-12 14:05:23 +00:00
context.update({
'php_binary_path': os.path.normpath(settings.WEBAPPS_PHP_CGI_BINARY_PATH % context),
2015-03-12 14:05:23 +00:00
'php_rc': os.path.normpath(settings.WEBAPPS_PHP_CGI_RC_DIR % context),
'php_ini_scan': os.path.normpath(settings.WEBAPPS_PHP_CGI_INI_SCAN_DIR % context),
'php_init_vars': init_vars,
})
context['php_binary'] = os.path.basename(context['php_binary_path'])
2015-03-12 14:05:23 +00:00
return textwrap.dedent("""\
#!/bin/sh
# %(banner)s
export PHPRC=%(php_rc)s
export PHP_INI_SCAN_DIR=%(php_ini_scan)s
2015-04-02 16:14:55 +00:00
export PHP_FCGI_MAX_REQUESTS=%(max_requests)s
2015-05-22 13:15:06 +00:00
exec %(php_binary_path)s%(php_init_vars)s""") % context
2015-03-12 14:05:23 +00:00
def get_fcgid_cmd_options(self, webapp, context):
2015-06-05 09:57:06 +00:00
options = webapp.type_instance.get_options()
2015-05-06 10:51:12 +00:00
maps = OrderedDict(
MaxProcesses=options.get('processes', None),
IOTimeout=options.get('timeout', None),
)
2015-03-12 14:05:23 +00:00
cmd_options = []
2015-04-02 16:14:55 +00:00
for directive, value in maps.items():
2015-03-12 14:05:23 +00:00
if value:
2015-04-05 18:02:36 +00:00
cmd_options.append(
"%s %s" % (directive, value.replace("'", '"'))
)
2015-03-12 14:05:23 +00:00
if cmd_options:
2015-03-27 19:50:54 +00:00
head = (
'# %(banner)s\n'
'FcgidCmdOptions %(wrapper_path)s'
) % context
2015-03-12 14:05:23 +00:00
cmd_options.insert(0, head)
return ' \\\n '.join(cmd_options)
2015-03-12 14:05:23 +00:00
def update_fcgid_context(self, webapp, context):
wrapper_path = settings.WEBAPPS_FCGID_WRAPPER_PATH % context
2015-03-12 14:05:23 +00:00
context.update({
'wrapper': self.get_fcgid_wrapper(webapp, context),
'wrapper_path': wrapper_path,
'wrapper_dir': os.path.dirname(wrapper_path),
})
context.update({
'cmd_options': self.get_fcgid_cmd_options(webapp, context),
'cmd_options_path': settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context,
})
2015-04-08 14:41:09 +00:00
return context
2015-03-12 14:05:23 +00:00
def update_fpm_context(self, webapp, context):
context.update({
'fpm_config': self.get_fpm_config(webapp, context),
'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context,
})
return context
2015-03-12 14:05:23 +00:00
def get_context(self, webapp):
context = super().get_context(webapp)
2015-03-12 14:05:23 +00:00
context.update({
2015-04-02 16:14:55 +00:00
'max_requests': settings.WEBAPPS_PHP_MAX_REQUESTS,
2015-03-12 14:05:23 +00:00
})
self.update_fpm_context(webapp, context)
2015-04-05 18:02:36 +00:00
self.update_fcgid_context(webapp, context)
2015-03-12 14:05:23 +00:00
return context