Continue refactoring webapps and websites

This commit is contained in:
Marc Aymerich 2015-03-10 11:46:48 +00:00
parent 12910bf072
commit 2a8d20910f
27 changed files with 760 additions and 584 deletions

View File

@ -206,3 +206,10 @@ ssh-copy-id root@<server-address>
* symbolicLink webapp (link stuff from other places)
* logs on panle/logs/ ? mkdir ~webapps, backend post save signal?
* transaction abortion on backend.generation, transaction fault tolerant on backend.execute()
* <IfModule security2_module> and other IfModule on backend SecRule
* webalizer backend on webapps and check webapps.websites.all()
* monitor in batches doesnt work!!!

View File

@ -42,7 +42,7 @@ ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', (
('domains.Domain',
'name',
{
'name': '"%s.orchestra.lan" % account.username'
'name': '"%s.orchestra.lan" % account.username.replace("_", "-")',
},
_("Designates whether to creates a related subdomain &lt;username&gt;.orchestra.lan or not."),
),

View File

@ -106,8 +106,7 @@ class ContactInline(admin.StackedInline):
insertattr(AccountAdmin, 'inlines', ContactInline)
search_fields = (
'contacts__short_name', 'contacts__full_name', 'contacts__phone',
'contacts__phone2', 'contacts__email'
'contacts__short_name', 'contacts__full_name',
)
for field in search_fields:
insertattr(AccountAdmin, 'search_fields', field)

View File

@ -33,7 +33,10 @@ class Bind9MasterDomainBackend(ServiceController):
self.append(textwrap.dedent("""\
echo -e '%(zone)s' > %(zone_path)s.tmp
diff -N -I'^\s*;;' %(zone_path)s %(zone_path)s.tmp || UPDATED=1
mv %(zone_path)s.tmp %(zone_path)s""" % context
mv %(zone_path)s.tmp %(zone_path)s
# Because bind realod will not display any fucking error
named-checkzone -k fail -n fail %(name)s %(zone_path)s
""" % context
))
self.update_conf(context)
@ -78,7 +81,7 @@ class Bind9MasterDomainBackend(ServiceController):
def get_servers(self, domain, backend):
""" Get related server IPs from registered backend routes """
from orchestra.apps.orchestration.manager import router
operation = Operation.create(backend_cls=backend, action=Operation.SAVE, instance=domain)
operation = Operation.create(backend, peration.SAVE, domain)
servers = []
for server in router.get_servers(operation):
servers.append(server.get_ip())

View File

@ -160,14 +160,14 @@ class Domain(models.Model):
type=Record.SOA,
value=' '.join(soa)
))
is_a = not types or Record.A in types or Record.AAAA in types
if Record.MX not in types and is_a:
is_host = self.is_top or not types or Record.A in types or Record.AAAA in types
if Record.MX not in types and is_host:
for mx in settings.DOMAINS_DEFAULT_MX:
records.append(AttrDict(
type=Record.MX,
value=mx
))
if (Record.A not in types and Record.AAAA not in types) and is_a:
if (Record.A not in types and Record.AAAA not in types) and is_host:
records.append(AttrDict(
type=Record.A,
value=settings.DOMAINS_DEFAULT_A
@ -250,4 +250,5 @@ class Record(models.Model):
def get_ttl(self):
return self.ttl or settings.DOMAINS_DEFAULT_TTL
services.register(Domain)

View File

@ -34,7 +34,7 @@ DOMAINS_SLAVES_PATH = getattr(settings, 'DOMAINS_SLAVES_PATH', '/etc/bind/named.
DOMAINS_CHECKZONE_BIN_PATH = getattr(settings, 'DOMAINS_CHECKZONE_BIN_PATH',
'/usr/sbin/named-checkzone -i local')
'/usr/sbin/named-checkzone -i local -k fail -n fail')
DOMAINS_CHECKZONE_PATH = getattr(settings, 'DOMAINS_CHECKZONE_PATH', '/dev/shm')

View File

@ -52,7 +52,7 @@ def execute(operations, async=False):
for server in operation.servers:
key = (server, operation.backend)
if key not in scripts:
scripts[key] = (operation.backend, [operation])
scripts[key] = (operation.backend(), [operation])
scripts[key][0].prepare()
else:
scripts[key][1].append(operation)

View File

@ -119,17 +119,17 @@ class BackendOperation(models.Model):
def __hash__(self):
""" set() """
backend_cls = type(self.backend)
return hash(backend_cls) + hash(self.instance) + hash(self.action)
backend = getattr(self, 'backend', self.backend)
return hash(backend) + hash(self.instance) + hash(self.action)
def __eq__(self, operation):
""" set() """
return hash(self) == hash(operation)
@classmethod
def create(cls, backend_cls, instance, action, servers=None):
op = cls(backend=backend_cls.get_name(), instance=instance, action=action)
op.backend = backend_cls()
def create(cls, backend, instance, action, servers=None):
op = cls(backend=backend.get_name(), instance=instance, action=action)
op.backend = backend
# instance should maintain any dynamic attribute until backend execution
# deep copy is prefered over copy otherwise objects will share same atributes (queryset cache)
op.instance = copy.deepcopy(instance)
@ -154,7 +154,7 @@ class BackendOperation(models.Model):
"""
if self.action == self.DELETE:
if hasattr(self.backend, 'get_context'):
self.backend.get_context(op.instance)
self.backend.get_context(self.instance)
def backend_class(self):
return ServiceBackend.get_backend(self.backend)
@ -194,7 +194,7 @@ class Route(models.Model):
def get_servers(cls, operation, **kwargs):
cache = kwargs.get('cache', {})
servers = []
backend_cls = type(operation.backend)
backend_cls = operation.backend
key = (backend_cls.get_name(), operation.action)
try:
routes = cache[key]

View File

@ -42,7 +42,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
'fields': ('shell', ('home', 'directory'), 'groups'),
}),
)
search_fields = ['username']
search_fields = ('username', 'account__username')
readonly_fields = ('account_link',)
change_readonly_fields = ('username',)
filter_horizontal = ('groups',)

View File

@ -26,7 +26,6 @@ class SystemUser(models.Model):
Username max_length determined by LINUX system user lentgh: 32
"""
# TODO max_length
username = models.CharField(_("username"), max_length=32, unique=True,
help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.validate_username])

View File

@ -9,8 +9,9 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.forms.widgets import DynamicHelpTextSelect
from orchestra.plugins.admin import SelectPluginAdminMixin
from . import settings, options
from .applications import App
from . import settings
from .options import AppOption
from .types import AppType
from .models import WebApp, WebAppOption
@ -19,7 +20,7 @@ class WebAppOptionInline(admin.TabularInline):
extra = 1
OPTIONS_HELP_TEXT = {
op.name: str(unicode(op.help_text)) for op in options.get_enabled().values()
op.name: str(unicode(op.help_text)) for op in AppOption.get_plugins()
}
class Media:
@ -35,7 +36,7 @@ class WebAppOptionInline(admin.TabularInline):
plugin = self.parent_object.type_class
else:
request = kwargs['request']
plugin = App.get_plugin(request.GET['type'])
plugin = AppType.get_plugin(request.GET['type'])
kwargs['choices'] = plugin.get_options_choices()
# Help text based on select widget
kwargs['widget'] = DynamicHelpTextSelect(
@ -52,8 +53,8 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
inlines = [WebAppOptionInline]
readonly_fields = ('account_link',)
change_readonly_fields = ('name', 'type')
list_prefetch_related = ('content_set__website',)
plugin = App
list_prefetch_related = ('contents__website',)
plugin = AppType
plugin_field = 'type'
plugin_title = _("Web application type")
@ -63,7 +64,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
def display_websites(self, webapp):
websites = []
for content in webapp.content_set.all():
for content in webapp.contents.all():
website = content.website
url = change_url(website)
name = "%s on %s" % (website.name, content.path)

View File

@ -1,44 +1,15 @@
import pkgutil
import textwrap
from .. import settings
class WebAppServiceMixin(object):
model = 'webapps.WebApp'
directive = None
def valid_directive(self, webapp):
return settings.WEBAPPS_TYPES[webapp.type]['directive'][0] == self.directive
def create_webapp_dir(self, context):
self.append("mkdir -p %(app_path)s" % context)
self.append("chown %(user)s:%(group)s %(app_path)s" % context)
def get_php_init_vars(self, webapp, per_account=False):
"""
process php options for inclusion on php.ini
per_account=True merges all (account, webapp.type) options
"""
init_vars = []
options = webapp.options.all()
if per_account:
options = webapp.account.webapps.filter(webapp_type=webapp.type)
for opt in options:
name = opt.name.replace('PHP-', '')
value = "%s" % opt.value
init_vars.append((name, value))
enabled_functions = []
for value in options.filter(name='php-enabled_functions').values_list('value', flat=True):
enabled_functions += enabled_functions.get().value.split(',')
if enabled_functions:
disabled_functions = []
for function in settings.WEBAPPS_PHP_DISABLED_FUNCTIONS:
if function not in enabled_functions:
disabled_functions.append(function)
init_vars.append(('dissabled_functions', ','.join(disabled_functions)))
return init_vars
def delete_webapp_dir(self, context):
self.append("rm -fr %(app_path)s" % context)

View File

@ -13,12 +13,9 @@ from .. import settings
class PHPFPMBackend(WebAppServiceMixin, ServiceController):
""" Per-webapp php application """
verbose_name = _("PHP-FPM")
directive = 'fpm'
default_route_match = "webapp.type.endswith('-fpm')"
def save(self, webapp):
if not self.valid_directive(webapp):
return
context = self.get_context(webapp)
self.create_webapp_dir(context)
self.append(textwrap.dedent("""\
@ -31,8 +28,6 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController):
))
def delete(self, webapp):
if not self.valid_directive(webapp):
return
context = self.get_context(webapp)
self.append("rm '%(fpm_path)s'" % context)
self.delete_webapp_dir(context)
@ -43,8 +38,8 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController):
super(PHPFPMBackend, self).commit()
self.append(textwrap.dedent("""
[[ $UPDATEDFPM == 1 ]] && {
service php5-fpm start
service php5-fpm reload
service php5-fpm start
}"""))
def get_context(self, webapp):

View File

@ -2,6 +2,8 @@ import re
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.signals import pre_save, pre_delete
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
@ -9,7 +11,7 @@ from jsonfield import JSONField
from orchestra.core import validators, services
from orchestra.utils.functional import cached
from . import settings, options
from . import settings
from .types import AppType
@ -53,17 +55,11 @@ class WebApp(models.Model):
opt.name: opt.value for opt in self.options.all()
}
@property
def app_type(self):
return settings.WEBAPPS_TYPES[self.type]
def get_fpm_port(self):
return settings.WEBAPPS_FPM_START_PORT + self.account_id
def get_directive(self):
directive = self.app_type['directive']
args = directive[1:] if len(directive) > 1 else ()
return directive[0], args
return self.type_instance.get_directive(self)
def get_path(self):
context = {
@ -86,8 +82,7 @@ class WebApp(models.Model):
class WebAppOption(models.Model):
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),
related_name='options')
name = models.CharField(_("name"), max_length=128,
choices=((op.name, op.verbose_name) for op in options.get_enabled().values()))
name = models.CharField(_("name"), max_length=128, choices=AppType.get_options_choices())
value = models.CharField(_("value"), max_length=256)
class Meta:
@ -98,18 +93,24 @@ class WebAppOption(models.Model):
def __unicode__(self):
return self.name
@cached_property
def option_class(self):
return SiteDirective.get_plugin(self.name)
@cached_property
def option_instance(self):
""" Per request lived option instance """
return self.option_class()
def clean(self):
option = options.get_enabled()[self.name]
option.validate(self)
self.option_instance.validate(self)
services.register(WebApp)
# Admin bulk deletion doesn't call model.delete(), we use signals instead of model method overriding
from django.db.models.signals import pre_save, pre_delete
from django.dispatch import receiver
# Admin bulk deletion doesn't call model.delete()
# So, signals are used instead of model method overriding
@receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save')
def type_save(sender, *args, **kwargs):

View File

@ -1,300 +1,350 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.plugins import Plugin
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class
from . import settings
class AppOption(object):
def __init__(self, name, *args, **kwargs):
self.name = name
self.verbose_name = kwargs.pop('verbose_name', name)
self.help_text = kwargs.pop('help_text', '')
for k,v in kwargs.iteritems():
setattr(self, k, v)
class AppOption(Plugin):
PHP = 'PHP'
PROCESS = 'Process'
FILESYSTEM = 'FileSystem'
def validate(self, webapp):
if self.regex and not re.match(self.regex, webapp.value):
help_text = ""
group = None
@classmethod
@cached
def get_plugins(cls):
plugins = []
for cls in settings.WEBAPPS_ENABLED_OPTIONS:
plugins.append(import_class(cls))
return plugins
@classmethod
@cached
def get_option_groups(cls):
groups = {}
for opt in cls.get_plugins():
try:
groups[opt.group].append(opt)
except KeyError:
groups[opt.group] = [opt]
return groups
def validate(self, option):
if self.regex and not re.match(self.regex, option.value):
raise ValidationError({
'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
params={
'value': webapp.value,
'value': option.value,
'regex': self.regex
}),
})
public_root = AppOption('public-root',
verbose_name=_("Public root"),
help_text=_("Document root relative to webapps/&lt;webapp&gt;/"),
class PublicRoot(AppOption):
name = 'public-root'
verbose_name = _("Public root")
help_text = _("Document root relative to webapps/&lt;webapp&gt;/")
regex = r'[^ ]+'
)
group = AppOption.FILESYSTEM
timeout = AppOption('timeout',
class DirectoryProtection(AppOption):
name = 'directory-protection'
verbose_name = _("Directory protection")
help_text = _("Space separated ...")
regex = r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$'
group = AppOption.FILESYSTEM
class Timeout(AppOption):
name = 'timeout'
# FCGID FcgidIOTimeout
# FPM pm.request_terminate_timeout
# PHP max_execution_time ini
verbose_name=_("Process timeout"),
help_text=_("Maximum time in seconds allowed for a request to complete (a number between 0 and 999)."),
regex=r'^[0-9]{1,3}$',
)
verbose_name = _("Process timeout")
help_text = _("Maximum time in seconds allowed for a request to complete (a number between 0 and 999).")
regex = r'^[0-9]{1,3}$'
group = AppOption.PROCESS
processes = AppOption('processes',
class Processes(AppOption):
name = 'processes'
# FCGID MaxProcesses
# FPM pm.max_children
verbose_name=_("Number of processes"),
help_text=_("Maximum number of children that can be alive at the same time (a number between 0 and 9)."),
regex=r'^[0-9]$',
)
verbose_name=_("Number of processes")
help_text=_("Maximum number of children that can be alive at the same time (a number between 0 and 9).")
regex=r'^[0-9]$'
group = AppOption.PROCESS
php_enabled_functions = AppOption('php-enabled_functions',
verbose_name=_("Enabled functions"),
help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS),
class PHPEnabledFunctions(AppOption):
name = 'enabled_functions'
verbose_name=_("Enabled functions")
help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS)
regex=r'^[\w\.,-]+$'
)
group = AppOption.PHP
php_allow_url_include = AppOption('PHP-allow_url_include',
verbose_name=_("Allow URL include"),
class PHPAllowURLInclude(AppOption):
name = 'allow_url_include'
verbose_name=_("Allow URL include")
help_text=_("Allows the use of URL-aware fopen wrappers with include, include_once, require, "
"require_once (On or Off)."),
"require_once (On or Off).")
regex=r'^(On|Off|on|off)$'
)
group = AppOption.PHP
php_allow_url_fopen = AppOption('PHP-allow_url_fopen',
verbose_name=_("Allow URL fopen"),
help_text=_("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off)."),
class PHPAllowURLFopen(AppOption):
name = 'allow_url_fopen'
verbose_name=_("Allow URL fopen")
help_text=_("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off).")
regex=r'^(On|Off|on|off)$'
)
group = AppOption.PHP
php_auto_append_file = AppOption('PHP-auto_append_file',
verbose_name=_("Auto append file"),
help_text=_("Specifies the name of a file that is automatically parsed after the main file."),
class PHPAutoAppendFile(AppOption):
name = 'auto_append_file'
verbose_name=_("Auto append file")
help_text=_("Specifies the name of a file that is automatically parsed after the main file.")
regex=r'^[\w\.,-/]+$'
)
group = AppOption.PHP
php_auto_prepend_file = AppOption('PHP-auto_prepend_file',
verbose_name=_("Auto prepend file"),
help_text=_("Specifies the name of a file that is automatically parsed before the main file."),
class PHPAutoPrependFile(AppOption):
name = 'auto_prepend_file'
verbose_name=_("Auto prepend file")
help_text=_("Specifies the name of a file that is automatically parsed before the main file.")
regex=r'^[\w\.,-/]+$'
)
group = AppOption.PHP
php_date_timezone = AppOption('PHP-date.timezone',
verbose_name=_("date.timezone"),
help_text=_("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London')."),
class PHPDateTimeZone(AppOption):
name = 'date.timezone'
verbose_name=_("date.timezone")
help_text=_("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London').")
regex=r'^\w+/\w+$'
)
group = AppOption.PHP
php_default_socket_timeout = AppOption('PHP-default_socket_timeout',
verbose_name=_("Default socket timeout"),
help_text=_("Number between 0 and 999."),
class PHPDefaultSocketTimeout(AppOption):
name = 'default_socket_timeout'
verbose_name=_("Default socket timeout")
help_text=_("Number between 0 and 999.")
regex=r'^[0-9]{1,3}$'
)
group = AppOption.PHP
php_display_errors = AppOption('PHP-display_errors',
verbose_name=_("Display errors"),
class PHPDisplayErrors(AppOption):
name = 'display_errors'
verbose_name=_("Display errors")
help_text=_("Determines whether errors should be printed to the screen as part of the output or "
"if they should be hidden from the user (On or Off)."),
"if they should be hidden from the user (On or Off).")
regex=r'^(On|Off|on|off)$'
)
group = AppOption.PHP
php_extension = AppOption('PHP-extension',
verbose_name=_("Extension"),
class PHPExtension(AppOption):
name = 'extension'
verbose_name=_("Extension")
regex=r'^[^ ]+$'
)
group = AppOption.PHP
php_magic_quotes_gpc = AppOption('PHP-magic_quotes_gpc',
verbose_name=_("Magic quotes GPC"),
class PHPMagicQuotesGPC(AppOption):
name = 'magic_quotes_gpc'
verbose_name=_("Magic quotes GPC")
help_text=_("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) "
"<b>DEPRECATED as of PHP 5.3.0</b>."),
regex=r'^(On|Off|on|off)$',
"<b>DEPRECATED as of PHP 5.3.0</b>.")
regex=r'^(On|Off|on|off)$'
deprecated=5.3
)
group = AppOption.PHP
php_magic_quotes_runtime = AppOption('PHP-magic_quotes_runtime',
verbose_name=_("Magic quotes runtime"),
class PHPMagicQuotesRuntime(AppOption):
name = 'magic_quotes_runtime'
verbose_name=_("Magic quotes runtime")
help_text=_("Functions that return data from any sort of external source will have quotes escaped "
"with a backslash (On or Off) <b>DEPRECATED as of PHP 5.3.0</b>."),
regex=r'^(On|Off|on|off)$',
deprecated=5.3
)
php_magic_quotes_sybase = AppOption('PHP-magic_quotes_sybase',
verbose_name=_("Magic quotes sybase"),
help_text=_("Single-quote is escaped with a single-quote instead of a backslash (On or Off)."),
"with a backslash (On or Off) <b>DEPRECATED as of PHP 5.3.0</b>.")
regex=r'^(On|Off|on|off)$'
)
deprecated=5.3
group = AppOption.PHP
php_max_execution_time = AppOption('PHP-max_execution_time',
verbose_name=_("Max execution time"),
class PHPMaginQuotesSybase(AppOption):
name = 'magic_quotes_sybase'
verbose_name=_("Magic quotes sybase")
help_text=_("Single-quote is escaped with a single-quote instead of a backslash (On or Off).")
regex=r'^(On|Off|on|off)$'
group = AppOption.PHP
class PHPMaxExecutonTime(AppOption):
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)."),
"the parser (Integer between 0 and 999).")
regex=r'^[0-9]{1,3}$'
)
group = AppOption.PHP
php_max_input_time = AppOption('PHP-max_input_time',
verbose_name=_("Max input time"),
class PHPMaxInputTime(AppOption):
name = 'max_input_time'
verbose_name=_("Max input time")
help_text=_("Maximum time in seconds a script is allowed to parse input data, like POST and GET "
"(Integer between 0 and 999)."),
"(Integer between 0 and 999).")
regex=r'^[0-9]{1,3}$'
)
group = AppOption.PHP
php_max_input_vars = AppOption('PHP-max_input_vars',
verbose_name=_("Max input vars"),
class PHPMaxInputVars(AppOption):
name = 'max_input_vars'
verbose_name=_("Max input vars")
help_text=_("How many input variables may be accepted (limit is applied to $_GET, $_POST "
"and $_COOKIE superglobal separately) (Integer between 0 and 9999)."),
"and $_COOKIE superglobal separately) (Integer between 0 and 9999).")
regex=r'^[0-9]{1,4}$'
)
group = AppOption.PHP
php_memory_limit = AppOption('PHP-memory_limit',
verbose_name=_("Memory limit"),
class PHPMemoryLimit(AppOption):
name = 'memory_limit'
verbose_name=_("Memory limit")
help_text=_("This sets the maximum amount of memory in bytes that a script is allowed to allocate "
"(Value between 0M and 999M)."),
"(Value between 0M and 999M).")
regex=r'^[0-9]{1,3}M$'
)
group = AppOption.PHP
php_mysql_connect_timeout = AppOption('PHP-mysql.connect_timeout',
verbose_name=_("Mysql connect timeout"),
help_text=_("Number between 0 and 999."),
class PHPMySQLConnectTimeout(AppOption):
name = 'mysql.connect_timeout'
verbose_name=_("Mysql connect timeout")
help_text=_("Number between 0 and 999.")
regex=r'^([0-9]){1,3}$'
)
group = AppOption.PHP
php_output_buffering = AppOption('PHP-output_buffering',
verbose_name=_("Output buffering"),
help_text=_("Turn on output buffering (On or Off)."),
class PHPOutputBuffering(AppOption):
name = 'output_buffering'
verbose_name=_("Output buffering")
help_text=_("Turn on output buffering (On or Off).")
regex=r'^(On|Off|on|off)$'
)
group = AppOption.PHP
php_register_globals = AppOption('PHP-register_globals',
verbose_name=_("Register globals"),
class PHPRegisterGlobals(AppOption):
name = 'register_globals'
verbose_name=_("Register globals")
help_text=_("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) "
"variables as global variables (On or Off)."),
"variables as global variables (On or Off).")
regex=r'^(On|Off|on|off)$'
)
group = AppOption.PHP
php_post_max_size = AppOption('PHP-post_max_size',
verbose_name=_("Post max size"),
help_text=_("Sets max size of post data allowed (Value between 0M and 999M)."),
class PHPPostMaxSize(AppOption):
name = 'post_max_size'
verbose_name=_("Post max size")
help_text=_("Sets max size of post data allowed (Value between 0M and 999M).")
regex=r'^[0-9]{1,3}M$'
)
group = AppOption.PHP
php_sendmail_path = AppOption('PHP-sendmail_path',
verbose_name=_("sendmail_path"),
help_text=_("Where the sendmail program can be found."),
class PHPSendmailPath(AppOption):
name = 'sendmail_path'
verbose_name=_("sendmail_path")
help_text=_("Where the sendmail program can be found.")
regex=r'^[^ ]+$'
)
group = AppOption.PHP
php_session_bug_compat_warn = AppOption('PHP-session.bug_compat_warn',
verbose_name=_("session.bug_compat_warn"),
help_text=_("Enables an PHP bug on session initialization for legacy behaviour (On or Off)."),
class PHPSessionBugCompatWarn(AppOption):
name = 'session.bug_compat_warn'
verbose_name=_("session.bug_compat_warn")
help_text=_("Enables an PHP bug on session initialization for legacy behaviour (On or Off).")
regex=r'^(On|Off|on|off)$'
)
group = AppOption.PHP
php_session_auto_start = AppOption('PHP-session.auto_start',
verbose_name=_("session.auto_start"),
class PHPSessionAutoStart(AppOption):
name = 'session.auto_start',
verbose_name=_("session.auto_start")
help_text=_("Specifies whether the session module starts a session automatically on request "
"startup (On or Off)."),
"startup (On or Off).")
regex=r'^(On|Off|on|off)$'
group = AppOption.PHP
class PHPSafeMode(AppOption):
name = 'safe_mode'
verbose_name=_("Safe mode")
help_text=_("Whether to enable PHP's safe mode (On or Off) <b>DEPRECATED as of PHP 5.3.0</b>")
regex=r'^(On|Off|on|off)$'
)
php_safe_mode = AppOption('PHP-safe_mode',
verbose_name=_("Safe mode"),
help_text=_("Whether to enable PHP's safe mode (On or Off) <b>DEPRECATED as of PHP 5.3.0</b>"),
regex=r'^(On|Off|on|off)$',
deprecated=5.3
)
php_suhosin_post_max_vars = AppOption('PHP-suhosin.post.max_vars',
verbose_name=_("Suhosin POST max vars"),
help_text=_("Number between 0 and 9999."),
group = AppOption.PHP
class PHPSuhosinPostMaxVars(AppOption):
name = 'suhosin.post.max_vars',
verbose_name=_("Suhosin POST max vars")
help_text=_("Number between 0 and 9999.")
regex=r'^[0-9]{1,4}$'
)
php_suhosin_get_max_vars = AppOption('PHP-suhosin.get.max_vars',
verbose_name=_("Suhosin GET max vars"),
help_text=_("Number between 0 and 9999."),
group = AppOption.PHP
class PHPSuhosinGetMaxVars(AppOption):
name = 'suhosin.get.max_vars'
verbose_name=_("Suhosin GET max vars")
help_text=_("Number between 0 and 9999.")
regex=r'^[0-9]{1,4}$'
)
php_suhosin_request_max_vars = AppOption('PHP-suhosin.request.max_vars',
verbose_name=_("Suhosin request max vars"),
help_text=_("Number between 0 and 9999."),
group = AppOption.PHP
class PHPSuhosinRequestMaxVars(AppOption):
name = 'suhosin.request.max_vars'
verbose_name=_("Suhosin request max vars")
help_text=_("Number between 0 and 9999.")
regex=r'^[0-9]{1,4}$'
)
php_suhosin_session_encrypt = AppOption('PHP-suhosin.session.encrypt',
verbose_name=_("suhosin.session.encrypt"),
help_text=_("On or Off"),
group = AppOption.PHP
class PHPSuhosinSessionEncrypt(AppOption):
name = 'suhosin.session.encrypt'
verbose_name=_("suhosin.session.encrypt")
help_text=_("On or Off")
regex=r'^(On|Off|on|off)$'
)
php_suhosin_simulation = AppOption('PHP-suhosin.simulation',
verbose_name=_("Suhosin simulation"),
help_text=_("On or Off"),
group = AppOption.PHP
class PHPSuhosinSimulation(AppOption):
name = 'suhosin.simulation'
verbose_name=_("Suhosin simulation")
help_text=_("On or Off")
regex=r'^(On|Off|on|off)$'
)
php_suhosin_executor_include_whitelist = AppOption('PHP-suhosin.executor.include.whitelist',
verbose_name=_("suhosin.executor.include.whitelist"),
group = AppOption.PHP
class PHPSuhosinExecutorIncludeWhitelist(AppOption):
name = 'suhosin.executor.include.whitelist'
verbose_name=_("suhosin.executor.include.whitelist")
regex=r'.*$'
)
php_upload_max_filesize = AppOption('PHP-upload_max_filesize',
verbose_name=_("upload_max_filesize"),
help_text=_("Value between 0M and 999M."),
group = AppOption.PHP
class PHPUploadMaxFileSize(AppOption):
name = 'upload_max_filesize',
verbose_name=_("upload_max_filesize")
help_text=_("Value between 0M and 999M.")
regex=r'^[0-9]{1,3}M$'
)
php_zend_extension = AppOption('PHP-post_max_size',
verbose_name=_("zend_extension"),
group = AppOption.PHP
class PHPPostMaxSize(AppOption):
name = 'post_max_size'
verbose_name=_("zend_extension")
regex=r'^[^ ]+$'
)
filesystem = [
public_root,
]
process = [
timeout,
processes,
]
php = [
php_enabled_functions,
php_allow_url_include,
php_allow_url_fopen,
php_auto_append_file,
php_auto_prepend_file,
php_date_timezone,
php_default_socket_timeout,
php_display_errors,
php_extension,
php_magic_quotes_gpc,
php_magic_quotes_runtime,
php_magic_quotes_sybase,
php_max_execution_time,
php_max_input_time,
php_max_input_vars,
php_memory_limit,
php_mysql_connect_timeout,
php_output_buffering,
php_register_globals,
php_post_max_size,
php_sendmail_path,
php_session_bug_compat_warn,
php_session_auto_start,
php_safe_mode,
php_suhosin_post_max_vars,
php_suhosin_get_max_vars,
php_suhosin_request_max_vars,
php_suhosin_session_encrypt,
php_suhosin_simulation,
php_suhosin_executor_include_whitelist,
php_upload_max_filesize,
php_zend_extension,
]
_enabled = None
def get_enabled():
global _enabled
if _enabled is None:
from . import settings
_enabled = {}
for op in settings.WEBAPPS_ENABLED_OPTIONS:
op = import_class(op)
_enabled[op.name] = op
return _enabled
group = AppOption.PHP

View File

@ -2,29 +2,30 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '%(home)s/webapps/%(app_name)s/')
WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '{home}/webapps/{app_name}/')
WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN',
# '/var/run/%(user)s-%(app_name)s.sock')
'127.0.0.1:%(fpm_port)s')
# '127.0.0.1:9{app_id:03d}
'/opt/php/5.4/socks/{user}-{app_name}.sock'
)
WEBAPPS_FPM_START_PORT = getattr(settings, 'WEBAPPS_FPM_START_PORT', 10000)
WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH',
'/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf')
'/etc/php5/fpm/pool.d/{user}-{app_name}.conf')
WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH',
'/home/httpd/fcgid/%(user)s/%(app_name)s-wrapper')
'/home/httpd/fcgid/{user}/{app_name}-wrapper')
WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', (
'orchestra.apps.webapps.types.Php55App',
'orchestra.apps.webapps.types.Php52App',
'orchestra.apps.webapps.types.Php4App',
'orchestra.apps.webapps.types.PHP54App',
'orchestra.apps.webapps.types.PHP52App',
'orchestra.apps.webapps.types.PHP4App',
'orchestra.apps.webapps.types.StaticApp',
'orchestra.apps.webapps.types.WebalizerApp',
'orchestra.apps.webapps.types.WordPressMuApp',
@ -35,6 +36,7 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', (
))
#WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {})
#for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems():
# if value is None:
@ -76,41 +78,42 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO
WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', (
'orchestra.apps.webapps.options.public_root',
'orchestra.apps.webapps.options.timeout',
'orchestra.apps.webapps.options.processes',
'orchestra.apps.webapps.options.php_enabled_functions',
'orchestra.apps.webapps.options.php_allow_url_include',
'orchestra.apps.webapps.options.php_allow_url_fopen',
'orchestra.apps.webapps.options.php_auto_append_file',
'orchestra.apps.webapps.options.php_auto_prepend_file',
'orchestra.apps.webapps.options.php_date_timezone',
'orchestra.apps.webapps.options.php_default_socket_timeout',
'orchestra.apps.webapps.options.php_display_errors',
'orchestra.apps.webapps.options.php_extension',
'orchestra.apps.webapps.options.php_magic_quotes_gpc',
'orchestra.apps.webapps.options.php_magic_quotes_runtime',
'orchestra.apps.webapps.options.php_magic_quotes_sybase',
'orchestra.apps.webapps.options.php_max_execution_time',
'orchestra.apps.webapps.options.php_max_input_time',
'orchestra.apps.webapps.options.php_max_input_vars',
'orchestra.apps.webapps.options.php_memory_limit',
'orchestra.apps.webapps.options.php_mysql_connect_timeout',
'orchestra.apps.webapps.options.php_output_buffering',
'orchestra.apps.webapps.options.php_register_globals',
'orchestra.apps.webapps.options.php_post_max_size',
'orchestra.apps.webapps.options.php_sendmail_path',
'orchestra.apps.webapps.options.php_session_bug_compat_warn',
'orchestra.apps.webapps.options.php_session_auto_start',
'orchestra.apps.webapps.options.php_safe_mode',
'orchestra.apps.webapps.options.php_suhosin_post_max_vars',
'orchestra.apps.webapps.options.php_suhosin_get_max_vars',
'orchestra.apps.webapps.options.php_suhosin_request_max_vars',
'orchestra.apps.webapps.options.php_suhosin_session_encrypt',
'orchestra.apps.webapps.options.php_suhosin_simulation',
'orchestra.apps.webapps.options.php_suhosin_executor_include_whitelist',
'orchestra.apps.webapps.options.php_upload_max_filesize',
'orchestra.apps.webapps.options.php_zend_extension',
'orchestra.apps.webapps.options.PublicRoot',
'orchestra.apps.webapps.options.DirectoryProtection',
'orchestra.apps.webapps.options.Timeout',
'orchestra.apps.webapps.options.Processes',
'orchestra.apps.webapps.options.PHPEnabledFunctions',
'orchestra.apps.webapps.options.PHPAllowURLInclude',
'orchestra.apps.webapps.options.PHPAllowURLFopen',
'orchestra.apps.webapps.options.PHPAutoAppendFile',
'orchestra.apps.webapps.options.PHPAutoPrependFile',
'orchestra.apps.webapps.options.PHPDateTimeZone',
'orchestra.apps.webapps.options.PHPDefaultSocketTimeout',
'orchestra.apps.webapps.options.PHPDisplayErrors',
'orchestra.apps.webapps.options.PHPExtension',
'orchestra.apps.webapps.options.PHPMagicQuotesGPC',
'orchestra.apps.webapps.options.PHPMagicQuotesRuntime',
'orchestra.apps.webapps.options.PHPMaginQuotesSybase',
'orchestra.apps.webapps.options.PHPMaxExecutonTime',
'orchestra.apps.webapps.options.PHPMaxInputTime',
'orchestra.apps.webapps.options.PHPMaxInputVars',
'orchestra.apps.webapps.options.PHPMemoryLimit',
'orchestra.apps.webapps.options.PHPMySQLConnectTimeout',
'orchestra.apps.webapps.options.PHPOutputBuffering',
'orchestra.apps.webapps.options.PHPRegisterGlobals',
'orchestra.apps.webapps.options.PHPPostMaxSize',
'orchestra.apps.webapps.options.PHPSendmailPath',
'orchestra.apps.webapps.options.PHPSessionBugCompatWarn',
'orchestra.apps.webapps.options.PHPSessionAutoStart',
'orchestra.apps.webapps.options.PHPSafeMode',
'orchestra.apps.webapps.options.PHPSuhosinPostMaxVars',
'orchestra.apps.webapps.options.PHPSuhosinGetMaxVars',
'orchestra.apps.webapps.options.PHPSuhosinRequestMaxVars',
'orchestra.apps.webapps.options.PHPSuhosinSessionEncrypt',
'orchestra.apps.webapps.options.PHPSuhosinSimulation',
'orchestra.apps.webapps.options.PHPSuhosinExecutorIncludeWhitelist',
'orchestra.apps.webapps.options.PHPUploadMaxFileSize',
'orchestra.apps.webapps.options.PHPPostMaxSize',
))
@ -120,6 +123,10 @@ WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'WEBAPPS_WORDPRESSMU_ADMI
WEBAPPS_WORDPRESSMU_BASE_URL = getattr(settings, 'WEBAPPS_WORDPRESSMU_BASE_URL',
'http://blogs.orchestra.lan/')
WEBAPPS_WORDPRESSMU_LISTEN = getattr(settings, 'WEBAPPS_WORDPRESSMU_LISTEN',
'/opt/php/5.4/socks/wordpress-mu.sock'
)
WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH',
'/home/httpd/htdocs/wikifarm/template.tar.gz')
@ -127,9 +134,22 @@ WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_TEMPLAT
WEBAPPS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_FARM_PATH',
'/home/httpd/htdocs/wikifarm/farm')
WEBAPPS_DOKUWIKIMU_LISTEN = getattr(settings, 'WEBAPPS_DOKUWIKIMU_LISTEN',
'/opt/php/5.4/socks/dokuwiki-mu.sock'
)
WEBAPPS_DRUPAL_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPAL_SITES_PATH',
'/home/httpd/htdocs/drupal-mu/sites/%(site_name)s')
WEBAPPS_DRUPALMU_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPALMU_SITES_PATH',
'/home/httpd/htdocs/drupal-mu/sites/{site_name}')
WEBAPPS_DRUPALMU_LISTEN = getattr(settings, 'WEBAPPS_DRUPALMU_LISTEN',
'/opt/php/5.4/socks/drupal-mu.sock'
)
WEBAPPS_MOODLEMU_LISTEN = getattr(settings, 'WEBAPPS_MOODLEMU_LISTEN',
'/opt/php/5.4/socks/moodle-mu.sock'
)
WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST',

View File

@ -11,6 +11,7 @@ from orchestra.utils.functional import cached
from orchestra.utils.python import import_class
from . import options, settings
from .options import AppOption
class AppType(plugins.Plugin):
@ -22,11 +23,7 @@ class AppType(plugins.Plugin):
serializer = None
icon = 'orchestra/icons/apps.png'
unique_name = False
options = (
('Process', options.process),
('PHP', options.php),
('File system', options.filesystem),
)
option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP)
@classmethod
@cached
@ -36,16 +33,18 @@ class AppType(plugins.Plugin):
plugins.append(import_class(cls))
return plugins
@classmethod
def clean_data(cls, webapp):
def clean_data(self, webapp):
""" model clean, uses cls.serizlier by default """
if cls.serializer:
serializer = cls.serializer(data=webapp.data)
if self.serializer:
serializer = self.serializer(data=webapp.data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)
return serializer.data
return {}
def get_directive(self, webapp):
return ('static', webapp.get_path())
def get_form(self):
self.form.plugin = self
self.form.plugin_field = 'type'
@ -69,19 +68,40 @@ class AppType(plugins.Plugin):
'name': _("A WordPress blog with this name already exists."),
})
def get_options(self):
pass
@classmethod
@cached
def get_php_options(cls):
php_version = getattr(cls, 'php_version', 1)
php_options = AppOption.get_option_groups()[AppOption.PHP]
return [op for op in php_options if getattr(cls, 'deprecated', 99) > php_version]
@classmethod
@cached
def get_options(cls):
""" Get enabled options based on cls.option_groups """
groups = AppOption.get_option_groups()
options = []
for group in cls.option_groups:
group_options = groups[group]
if group == AppOption.PHP:
group_options = cls.get_php_options()
if group is None:
options.insert(0, (group, group_options))
else:
options.append((group, group_options))
return options
@classmethod
def get_options_choices(cls):
enabled = options.get_enabled().values()
""" Generates grouped choices ready to use in Field.choices """
# generators can not be @cached
yield (None, '-------')
for option in cls.options:
if hasattr(option, '__iter__'):
yield (option[0], [(op.name, op.verbose_name) for op in option[1] if op in enabled])
elif option in enabled:
for group, options in cls.get_options():
if group is None:
for option in options:
yield (option.name, option.verbose_name)
else:
yield (group, [(op.name, op.verbose_name) for op in options])
def save(self, instance):
pass
@ -92,35 +112,83 @@ class AppType(plugins.Plugin):
def get_related_objects(self, instance):
pass
def get_directive_context(self, webapp):
return {
'app_id': webapp.id,
'app_name': webapp.name,
'user': webapp.account.username,
}
class Php55App(AppType):
name = 'php5.5-fpm'
verbose_name = "PHP 5.5 FPM"
# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',),
directive = ('fpm', 'fcgi://{}%(app_path)s'.format(settings.WEBAPPS_FPM_LISTEN))
class PHPAppType(AppType):
php_version = 5.4
fpm_listen = settings.WEBAPPS_FPM_LISTEN
def get_directive(self, webapp):
context = self.get_directive_context(webapp)
socket_type = 'unix'
if ':' in self.fpm_listen:
socket_type = 'tcp'
socket = self.fpm_listen.format(context)
return ('fpm', socket_type, socket, webapp.get_path())
def get_php_init_vars(self, webapp, per_account=False):
"""
process php options for inclusion on php.ini
per_account=True merges all (account, webapp.type) options
"""
init_vars = []
php_options = type(self).get_php_options()
options = webapp.options.all()
if per_account:
options = webapp.account.webapps.filter(webapp_type=webapp.type)
php_options = [option.name for option in php_options]
for opt in options:
if opt.option_class in php_options:
init_vars.append(
(opt.name, opt.value)
)
enabled_functions = []
for value in options.filter(name='enabled_functions').values_list('value', flat=True):
enabled_functions += enabled_functions.get().value.split(',')
if enabled_functions:
disabled_functions = []
for function in settings.WEBAPPS_PHP_DISABLED_FUNCTIONS:
if function not in enabled_functions:
disabled_functions.append(function)
init_vars.append(
('dissabled_functions', ','.join(disabled_functions))
)
return init_vars
class PHP54App(PHPAppType):
name = 'php5.4-fpm'
php_version = 5.4
verbose_name = "PHP 5.4 FPM"
help_text = _("This creates a PHP5.5 application under ~/webapps/&lt;app_name&gt;<br>"
"PHP-FPM will be used to execute PHP files.")
options = (
('Process', options.process),
('PHP', [op for op in options.php if getattr(op, 'deprecated', 99) > 5.5]),
('File system', options.filesystem),
)
icon = 'orchestra/icons/apps/PHPFPM.png'
class Php52App(AppType):
name = 'php5.2-fcgi'
verbose_name = "PHP 5.2 FCGI"
directive = ('fcgi', settings.WEBAPPS_FCGID_PATH)
class PHP52App(PHPAppType):
name = 'php5.2-fcgid'
php_version = 5.2
verbose_name = "PHP 5.2 FCGID"
help_text = _("This creates a PHP5.2 application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.")
icon = 'orchestra/icons/apps/PHPFCGI.png'
def get_directive(self, webapp):
context = self.get_directive_context(webapp)
wrapper_path = settings.WEBAPPS_FCGID_PATH.format(context)
return ('fcgi', webapp.get_path(), wrapper_path)
class Php4App(AppType):
name = 'php4-fcgi'
verbose_name = "PHP 4 FCGI"
directive = ('fcgi', settings.WEBAPPS_FCGID_PATH)
class PHP4App(PHP52App):
name = 'php4-fcgid'
php_version = 4
verbose_name = "PHP 4 FCGID"
help_text = _("This creates a PHP4 application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.")
icon = 'orchestra/icons/apps/PHPFCGI.png'
@ -129,13 +197,11 @@ class Php4App(AppType):
class StaticApp(AppType):
name = 'static'
verbose_name = "Static"
directive = ('static',)
help_text = _("This creates a Static application under ~/webapps/&lt;app_name&gt;<br>"
"Apache2 will be used to serve static content and execute CGI files.")
icon = 'orchestra/icons/apps/Static.png'
options = (
('File system', options.filesystem),
)
option_groups = (AppOption.FILESYSTEM,)
class WebalizerApp(AppType):
name = 'webalizer'
@ -144,10 +210,13 @@ class WebalizerApp(AppType):
help_text = _("This creates a Webalizer application under "
"~/webapps/&lt;app_name&gt;-&lt;site_name&gt;")
icon = 'orchestra/icons/apps/Stats.png'
options = ()
option_groups = ()
def get_directive(self, webapp):
return ('static', webapp.get_path())
class WordPressMuApp(AppType):
class WordPressMuApp(PHPAppType):
name = 'wordpress-mu'
verbose_name = "WordPress (SaaS)"
directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/')
@ -155,10 +224,11 @@ class WordPressMuApp(AppType):
"By default this blog is accessible via &lt;app_name&gt;.blogs.orchestra.lan")
icon = 'orchestra/icons/apps/WordPressMu.png'
unique_name = True
options = ()
option_groups = ()
fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN
class DokuWikiMuApp(AppType):
class DokuWikiMuApp(PHPAppType):
name = 'dokuwiki-mu'
verbose_name = "DokuWiki (SaaS)"
directive = ('alias', '/home/httpd/wikifarm/farm/')
@ -166,10 +236,11 @@ class DokuWikiMuApp(AppType):
"By default this wiki is accessible via &lt;app_name&gt;.wikis.orchestra.lan")
icon = 'orchestra/icons/apps/DokuWikiMu.png'
unique_name = True
options = ()
option_groups = ()
fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN
class MoodleMuApp(AppType):
class MoodleMuApp(PHPAppType):
name = 'moodle-mu'
verbose_name = "Moodle (SaaS)"
directive = ('alias', '/home/httpd/wikifarm/farm/')
@ -177,10 +248,11 @@ class MoodleMuApp(AppType):
"By default this wiki is accessible via &lt;app_name&gt;.moodle.orchestra.lan")
icon = 'orchestra/icons/apps/MoodleMu.png'
unique_name = True
options = ()
option_groups = ()
fpm_listen = settings.WEBAPPS_MOODLEMU_LISTEN
class DrupalMuApp(AppType):
class DrupalMuApp(PHPAppType):
name = 'drupal-mu'
verbose_name = "Drupdal (SaaS)"
directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/')
@ -190,7 +262,8 @@ class DrupalMuApp(AppType):
"By default this site will be accessible via &lt;app_name&gt;.drupal.orchestra.lan")
icon = 'orchestra/icons/apps/DrupalMu.png'
unique_name = True
options = ()
option_groups = ()
fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN
from rest_framework import serializers
@ -204,7 +277,7 @@ class SymbolicLinkSerializer(serializers.Serializer):
path = serializers.CharField(label=_("Path"))
class SymbolicLinkApp(AppType):
class SymbolicLinkApp(PHPAppType):
name = 'symbolic-link'
verbose_name = "Symbolic link"
form = SymbolicLinkForm
@ -231,7 +304,7 @@ from orchestra.apps.databases.models import Database, DatabaseUser
from orchestra.utils.python import random_ascii
class WordPressApp(AppType):
class WordPressApp(PHPAppType):
name = 'wordpress'
verbose_name = "WordPress"
icon = 'orchestra/icons/apps/WordPress.png'

View File

@ -10,17 +10,18 @@ from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from orchestra.forms.widgets import DynamicHelpTextSelect
from . import settings, options
from . import settings
from .directives import SiteDirective
from .forms import WebsiteAdminForm
from .models import Content, Website, WebsiteOption
from .models import Content, Website, Directive
class WebsiteOptionInline(admin.TabularInline):
model = WebsiteOption
class DirectiveInline(admin.TabularInline):
model = Directive
extra = 1
OPTIONS_HELP_TEXT = {
op.name: str(unicode(op.help_text)) for op in options.get_enabled().values()
DIRECTIVES_HELP_TEXT = {
op.name: str(unicode(op.help_text)) for op in SiteDirective.get_plugins()
}
# class Media:
@ -34,9 +35,9 @@ class WebsiteOptionInline(admin.TabularInline):
if db_field.name == 'name':
# Help text based on select widget
kwargs['widget'] = DynamicHelpTextSelect(
'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT
'this.id.replace("name", "value")', self.DIECTIVES_HELP_TEXT
)
return super(WebsiteOptionInline, self).formfield_for_dbfield(db_field, **kwargs)
return super(DirectiveInline, self).formfield_for_dbfield(db_field, **kwargs)
class ContentInline(AccountAdminMixin, admin.TabularInline):
@ -60,7 +61,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'display_domains', 'display_webapps', 'account_link')
list_filter = ('port', 'is_active')
change_readonly_fields = ('name',)
inlines = [ContentInline, WebsiteOptionInline]
inlines = [ContentInline, DirectiveInline]
filter_horizontal = ['domains']
fieldsets = (
(None, {
@ -70,7 +71,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
)
form = WebsiteAdminForm
filter_by_account_fields = ['domains']
list_prefetch_related = ('domains', 'content_set__webapp')
list_prefetch_related = ('domains', 'contents__webapp')
search_fields = ('name', 'account__username', 'domains__name')
def display_domains(self, website):
@ -85,7 +86,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def display_webapps(self, website):
webapps = []
for content in website.content_set.all():
for content in website.contents.all():
webapp = content.webapp
url = change_url(webapp)
name = "%s on %s" % (webapp.get_type_display(), content.path)

View File

@ -68,33 +68,51 @@ class Apache2Backend(ServiceController):
def get_content_directives(self, site):
directives = ''
for content in site.content_set.all().order_by('-path'):
method, args = content.webapp.get_directive()
for content in site.contents.all().order_by('-path'):
directive = content.webapp.get_directive()
method, agrs = directive[0], directive[1:]
method = getattr(self, 'get_%s_directives' % method)
directives += method(content, *args)
return directives
def get_static_directives(self, content, *args):
def get_static_directives(self, content, app_path):
context = self.get_content_context(content)
context['path'] = args[0] % context if args else content.webapp.get_path()
context['app_path'] = app_path
return "Alias %(location)s %(path)s\n" % context
def get_fpm_directives(self, content, *args):
def get_fpm_directives(self, content, socket_type, socket, app_path):
if socket_type == 'unix':
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/'
if content.path != '/':
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1'
elif socket_type == 'tcp':
target = 'fcgi://%(socket)s%(app_path)s/$1'
else:
raise TypeError("%s socket not supported." % socket_type)
context = self.get_content_context(content)
context['fcgi_path'] = args[0] % context
directive = "ProxyPassMatch ^%(location)s(.*\.php(/.*)?)$ %(fcgi_path)s$1\n"
return directive % context
context.update({
'app_path': app_path,
'socket': socket,
})
return textwrap.dedent("""\
ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target}
Alias %(location)s/ %(app_path)s/
""".format(target=target) % context
)
def get_fcgi_directives(self, content, fcgid_path):
def get_fcgi_directives(self, content, app_path, wrapper_path):
context = self.get_content_context(content)
context['fcgid_path'] = fcgid_path % context
fcgid = self.get_static_directives(content)
fcgid += textwrap.dedent("""\
context.update({
'app_path': app_path,
'wrapper_path': wrapper_path,
})
fcgid = textwrap.dedent("""\
Alias %(location)s %(app_path)s
ProxyPass %(location)s !
<Directory %(app_path)s>
Options +ExecCGI
AddHandler fcgid-script .php
FcgidWrapper %(fcgid_path)s\
FcgidWrapper %(wrapper_path)s\
""") % context
for option in content.webapp.options.filter(name__startswith='Fcgid'):
fcgid += " %s %s\n" % (option.name, option.value)

View File

@ -24,17 +24,21 @@ class WebalizerBackend(ServiceController):
))
def delete(self, content):
pass
# TODO delete has to be done on webapp deleteion, not content deletion
# context = self.get_context(content)
# self.append("rm -fr %(webalizer_path)s" % context)
# self.append("rm %(webalizer_conf_path)s" % context)
context = self.get_context(content)
delete_webapp = not content.webapp.pk
# TODO remove when confirmed that it works, otherwise create a second WebalizerBackend for WebApps
if delete_webapp:
self.append("mv %(webapp_path)s %(webapp_path)s.deleted" % context)
if delete_webapp or not content.webapp.contents.filter(website=content.website).exists():
self.append("mv %(webalizer_path)s %(webalizer_path)s.deleted" % context)
self.append("rm %(webalizer_conf_path)s" % context)
def get_context(self, content):
conf_file = "%s.conf" % content.website.unique_name
context = {
'site_logs': content.website.get_www_access_log_path(),
'site_name': content.website.name,
'webapp_path': content.webapp.get_path(),
'webalizer_path': os.path.join(content.webapp.get_path(), content.website.name),
'webalizer_conf_path': os.path.join(settings.WEBSITES_WEBALIZER_PATH, conf_file),
'user': content.webapp.account.username,

View File

@ -0,0 +1,151 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.plugins import Plugin
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class
from . import settings
# TODO multiple and unique validation support in the formset
class SiteDirective(Plugin):
HTTPD = 'httpd'
SEC = 'sec'
SSL = 'ssl'
help_text = ""
unique = True
@classmethod
@cached
def get_plugins(cls):
plugins = []
for cls in settings.WEBSITES_ENABLED_DIRECTIVES:
plugins.append(import_class(cls))
return plugins
@classmethod
@cached
def get_option_groups(cls):
groups = {}
for opt in cls.get_plugins():
try:
groups[opt.group].append(opt)
except KeyError:
groups[opt.group] = [opt]
return groups
@classmethod
def get_plugin_choices(cls):
""" Generates grouped choices ready to use in Field.choices """
# generators can not be @cached
yield (None, '-------')
options = cls.get_option_groups()
for option in options.pop(None, ()):
yield (option.name, option.verbose_name)
for group, options in options.iteritems():
yield (group, [(op.name, op.verbose_name) for op in options])
def validate(self, website):
if self.regex and not re.match(self.regex, website.value):
raise ValidationError({
'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
params={
'value': website.value,
'regex': self.regex
}),
})
class Redirect(SiteDirective):
name = 'redirect'
verbose_name=_("Redirection")
help_text = _("<tt>&lt;website path&gt; &lt;destination URL&gt;</tt>")
regex = r'^[^ ]+\s[^ ]+$'
group = SiteDirective.HTTPD
class Proxy(SiteDirective):
name = 'proxy'
verbose_name=_("Proxy")
help_text = _("<tt>&lt;website path&gt; &lt;target URL&gt;</tt>")
regex = r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$'
group = SiteDirective.HTTPD
class UserGroup(SiteDirective):
name = 'user_group'
verbose_name=_("SuexecUserGroup")
help_text = _("<tt>user [group]</tt>, username and optional groupname.")
regex = r'^[\w/_]+(\s[\w/_]+)*$'
group = SiteDirective.HTTPD
def validate(self, directive):
super(UserGroupDirective, self).validate(directive)
options = directive.split()
syetmusers = [options[0]]
if len(options) > 1:
systemusers.append(options[1])
# TODO not sure about this dependency maybe make it part of pangea only
from orchestra.apps.users.models import SystemUser
errors = []
for user in systemusers:
if not SystemUser.objects.filter(username=user).exists():
erros.append("")
if errors:
raise ValidationError({
'value': errors
})
class ErrorDocument(SiteDirective):
name = 'error_document'
verbose_name=_("ErrorDocumentRoot")
help_text = _("&lt;error code&gt; &lt;URL/path/message&gt;<br>"
"<tt>&nbsp;500 http://foo.example.com/cgi-bin/tester</tt><br>"
"<tt>&nbsp;404 /cgi-bin/bad_urls.pl</tt><br>"
"<tt>&nbsp;401 /subscription_info.html</tt><br>"
"<tt>&nbsp;403 \"Sorry can't allow you access today\"</tt>")
regex = r'[45]0[0-9]\s.*'
group = SiteDirective.HTTPD
class SSLCA(SiteDirective):
name = 'ssl_ca'
verbose_name=_("SSL CA")
help_text = _("Filesystem path of the CA certificate file.")
regex = r'^[^ ]+$'
group = SiteDirective.SSL
class SSLCert(SiteDirective):
name = 'ssl_cert'
verbose_name=_("SSL cert")
help_text = _("Filesystem path of the certificate file.")
regex = r'^[^ ]+$'
group = SiteDirective.SSL
class SSLKey(SiteDirective):
name = 'ssl_key'
verbose_name=_("SSL key")
help_text = _("Filesystem path of the key file.")
regex = r'^[^ ]+$'
group = SiteDirective.SSL
class SecRuleRemove(SiteDirective):
name = 'sec_rule_remove'
verbose_name=_("SecRuleRemoveById")
help_text = _("Space separated ModSecurity rule IDs.")
regex = r'^[0-9\s]+$'
group = SiteDirective.SEC
class SecEngine(SiteDirective):
name = 'sec_engine'
verbose_name=_("Modsecurity engine")
help_text = _("<tt>On</tt> or <tt>Off</tt>, defaults to On")
regex = r'^(On|Off)$'
group = SiteDirective.SEC

View File

@ -2,12 +2,14 @@ import re
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.core import validators, services
from orchestra.utils.functional import cached
from . import settings, options
from . import settings
from .directives import SiteDirective
class Website(models.Model):
@ -16,6 +18,7 @@ class Website(models.Model):
validators=[validators.validate_name])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='websites')
# TODO protocol
port = models.PositiveIntegerField(_("port"),
choices=settings.WEBSITES_PORT_CHOICES,
default=settings.WEBSITES_DEFAULT_PORT)
@ -49,9 +52,9 @@ class Website(models.Model):
raise TypeError('No protocol for port "%s"' % self.port)
@cached
def get_options(self):
def get_directives(self):
return {
opt.name: opt.value for opt in self.options.all()
opt.name: opt.value for opt in self.directives.all()
}
def get_absolute_url(self):
@ -78,29 +81,34 @@ class Website(models.Model):
return path.replace('//', '/')
class WebsiteOption(models.Model):
class Directive(models.Model):
website = models.ForeignKey(Website, verbose_name=_("web site"),
related_name='options')
related_name='directives')
name = models.CharField(_("name"), max_length=128,
choices=((op.name, op.verbose_name) for op in options.get_enabled().values()))
choices=SiteDirective.get_plugin_choices())
value = models.CharField(_("value"), max_length=256)
class Meta:
# unique_together = ('website', 'name')
verbose_name = _("option")
verbose_name_plural = _("options")
def __unicode__(self):
return self.name
@cached_property
def directive_class(self):
return SiteDirective.get_plugin(self.name)
@cached_property
def directive_instance(self):
""" Per request lived directive instance """
return self.directive_class()
def clean(self):
option = options.get_enabled()[self.name]
option.validate(self)
self.directive_instance.validate(self)
class Content(models.Model):
webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application"))
website = models.ForeignKey('websites.Website', verbose_name=_("web site"))
webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application"),
related_name='contents')
website = models.ForeignKey('websites.Website', verbose_name=_("web site"),
related_name='contents')
path = models.CharField(_("path"), max_length=256, blank=True,
validators=[validators.validate_url_path])

View File

@ -1,127 +0,0 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.utils.python import import_class
from . import settings
# TODO multiple and unique validation support in the formset
class SiteOption(object):
unique = True
def __init__(self, name, *args, **kwargs):
self.name = name
self.verbose_name = kwargs.pop('verbose_name', name)
self.help_text = kwargs.pop('help_text', '')
for k,v in kwargs.iteritems():
setattr(self, k, v)
def validate(self, website):
if self.regex and not re.match(self.regex, website.value):
raise ValidationError({
'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
params={
'value': website.value,
'regex': self.regex
}),
})
directory_protection = SiteOption('directory_protection',
verbose_name=_("Directory protection"),
help_text=_("Space separated ..."),
regex=r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$',
)
redirect = SiteOption('redirect',
verbose_name=_("Redirection"),
help_text=_("<tt>&lt;website path&gt; &lt;destination URL&gt;</tt>"),
regex=r'^[^ ]+\s[^ ]+$',
)
proxy = SiteOption('proxy',
verbose_name=_("Proxy"),
help_text=_("<tt>&lt;website path&gt; &lt;target URL&gt;</tt>"),
regex=r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$',
)
ssl_ca = SiteOption('ssl_ca',
verbose_name=_("SSL CA"),
help_text=_("Filesystem path of the CA certificate file."),
regex=r'^[^ ]+$'
)
ssl_cert = SiteOption('ssl_cert',
verbose_name=_("SSL cert"),
help_text=_("Filesystem path of the certificate file."),
regex=r'^[^ ]+$',
)
ssl_key = SiteOption('ssl_key',
verbose_name=_("SSL key"),
help_text=_("Filesystem path of the key file."),
regex=r'^[^ ]+$',
)
sec_rule_remove = SiteOption('sec_rule_remove',
verbose_name=_("SecRuleRemoveById"),
help_text=_("Space separated ModSecurity rule IDs."),
regex=r'^[0-9\s]+$',
)
sec_engine = SiteOption('sec_engine',
verbose_name=_("Modsecurity engine"),
help_text=_("<tt>On</tt> or <tt>Off</tt>, defaults to On"),
regex=r'^(On|Off)$',
)
user_group = SiteOption('user_group',
verbose_name=_("SuexecUserGroup"),
help_text=_("<tt>user [group]</tt>, username and optional groupname."),
# TODO validate existing user/group
regex=r'^[\w/_]+(\s[\w/_]+)*$',
)
error_document = SiteOption('error_document',
verbose_name=_("ErrorDocumentRoot"),
help_text=_("&lt;error code&gt; &lt;URL/path/message&gt;<br>"
"<tt>&nbsp;500 http://foo.example.com/cgi-bin/tester</tt><br>"
"<tt>&nbsp;404 /cgi-bin/bad_urls.pl</tt><br>"
"<tt>&nbsp;401 /subscription_info.html</tt><br>"
"<tt>&nbsp;403 \"Sorry can't allow you access today\"</tt>"),
regex=r'[45]0[0-9]\s.*',
)
ssl = [
ssl_ca,
ssl_cert,
ssl_key,
]
sec = [
sec_rule_remove,
sec_engine,
]
httpd = [
directory_protection,
redirect,
proxy,
user_group,
error_document,
]
_enabled = None
def get_enabled():
global _enabled
if _enabled is None:
from . import settings
_enabled = {}
for op in settings.WEBSITES_ENABLED_OPTIONS:
op = import_class(op)
_enabled[op.name] = op
return _enabled

View File

@ -43,7 +43,7 @@ class ContentSerializer(serializers.HyperlinkedModelSerializer):
class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
domains = RelatedDomainSerializer(many=True, allow_add_remove=True, required=False)
contents = ContentSerializer(required=False, many=True, allow_add_remove=True,
source='content_set')
source='contents')
options = OptionField(required=False)
class Meta:

View File

@ -14,10 +14,10 @@ WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', (
WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', (
('http', 'HTTP'),
('https', 'HTTPS'),
('http-https', 'HTTP and HTTPS),
('https-only', 'HTTPS only'),
('http', "HTTP"),
('https', "HTTPS"),
('http-https', _("HTTP and HTTPS")),
('https-only', _("HTTPS only")),
))
WEBSITES_DEFAULT_PORT = getattr(settings, 'WEBSITES_DEFAULT_PORT', 80)
@ -29,17 +29,16 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*')
WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain')
WEBSITES_ENABLED_OPTIONS = getattr(settings, 'WEBSITES_ENABLED_OPTIONS', (
'orchestra.apps.websites.options.directory_protection',
'orchestra.apps.websites.options.redirect',
'orchestra.apps.websites.options.proxy',
'orchestra.apps.websites.options.ssl_ca',
'orchestra.apps.websites.options.ssl_cert',
'orchestra.apps.websites.options.ssl_key',
'orchestra.apps.websites.options.sec_rule_remove',
'orchestra.apps.websites.options.sec_engine',
'orchestra.apps.websites.options.user_group',
'orchestra.apps.websites.options.error_document',
WEBSITES_ENABLED_DIRECTIVES = getattr(settings, 'WEBSITES_ENABLED_DIRECTIVES', (
'orchestra.apps.websites.directives.Redirect',
'orchestra.apps.websites.directives.Proxy',
'orchestra.apps.websites.directives.UserGroup',
'orchestra.apps.websites.directives.ErrorDocument',
'orchestra.apps.websites.directives.SSLCA',
'orchestra.apps.websites.directives.SSLCert',
'orchestra.apps.websites.directives.SSLKey',
'orchestra.apps.websites.directives.SecRuleRemove',
'orchestra.apps.websites.directives.SecEngine',
))

View File

@ -17,12 +17,12 @@ class Plugin(object):
return cls.plugins
@classmethod
@cached
def get_plugin(cls, name):
for plugin in cls.get_plugins():
if plugin.get_name() == name:
return plugin
raise KeyError('This plugin is not registered')
if not hasattr(cls, '_registry'):
cls._registry = {
plugin.get_name(): plugin for plugin in cls.get_plugins()
}
return cls._registry[name]
@classmethod
def get_verbose_name(cls):
@ -38,7 +38,9 @@ class Plugin(object):
choices = []
for plugin in cls.get_plugins():
verbose = plugin.get_verbose_name()
choices.append((plugin.get_name(), verbose))
choices.append(
(plugin.get_name(), verbose)
)
return sorted(choices, key=lambda e: e[1])
@classmethod

View File

@ -1,7 +1,8 @@
def cached(func):
""" caches func return value """
def cached_func(self, *args, **kwargs):
attr = '_cached_' + func.__name__
# id(self) prevents sharing within subclasses
attr = '_cached_%s_%i' % (func.__name__, id(self))
key = (args, tuple(kwargs.items()))
try:
return getattr(self, attr)[key]
@ -13,4 +14,3 @@ def cached(func):
setattr(self, attr, {key: value})
return value
return cached_func