Split webapps types into separate files

This commit is contained in:
Marc Aymerich 2015-03-11 16:32:33 +00:00
parent f7aac57a84
commit b36ca7a248
25 changed files with 654 additions and 505 deletions

View file

@ -203,11 +203,12 @@ POST INSTALL
ssh-keygen
ssh-copy-id root@<server-address>
Php binaries should have this format: /usr/bin/php5.2-cgi
* 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()
* logs on panel/logs/ ? mkdir ~webapps, backend post save signal?
* transaction fault tolerant on backend.execute()
* <IfModule security2_module> and other IfModule on backend SecRule

View file

@ -107,7 +107,14 @@ class MailmanBackend(ServiceController):
sed -i -e '/^.*\s%(name)s\(%(address_regex)s\)\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s""") % context
)
self.append("rmlist -a %(name)s" % context)
self.append(textwrap.dedent("""\
# Non-existent list archives produce exit code 1
exit_code=0
rmlist -a %(name)s || exit_code=$?
if [[ $exit_code != 0 && $exit_code != 1 ]]; then
exit $exit_code
fi""") % context
)
def commit(self):
context = self.get_context_files()

View file

@ -38,24 +38,30 @@ def message_user(request, logs):
ids = []
for log in logs:
total += 1
ids.append(log.pk)
if log.state != log.EXCEPTION:
# EXCEPTION logs are not stored on the database
ids.append(log.pk)
if log.state == log.SUCCESS:
successes += 1
errors = total-successes
if total > 1:
if len(ids) == 1:
url = reverse('admin:orchestration_backendlog_change', args=ids)
href = '<a href="{}">backends</a>'.format(url)
elif len(ids) > 1:
url = reverse('admin:orchestration_backendlog_changelist')
url += '?id__in=%s' % ','.join(map(str, ids))
href = '<a href="{}">backends</a>'.format(url)
else:
url = reverse('admin:orchestration_backendlog_change', args=ids)
href = ''
if errors:
msg = ungettext(
_('{errors} out of {total} <a href="{url}">backends</a> has fail to execute.'),
_('{errors} out of {total} <a href="{url}">backends</a> have fail to execute.'),
_('{errors} out of {total} {href} has fail to execute.'),
_('{errors} out of {total} {href} have fail to execute.'),
errors)
messages.error(request, mark_safe(msg.format(errors=errors, total=total, url=url)))
messages.error(request, mark_safe(msg.format(errors=errors, total=total, href=href)))
else:
msg = ungettext(
_('{total} <a href="{url}">backend</a> has been executed.'),
_('{total} <a href="{url}">backends</a> have been executed.'),
_('{total} {href} has been executed.'),
_('{total} {href} have been executed.'),
total)
messages.success(request, mark_safe(msg.format(total=total, url=url)))
messages.success(request, mark_safe(msg.format(total=total, href=href)))

View file

@ -1,12 +1,15 @@
import logging
import threading
import traceback
from django import db
from django.core.mail import mail_admins
from orchestra.utils.python import import_class
from . import settings
from .helpers import send_report
from .models import BackendLog
logger = logging.getLogger(__name__)
@ -29,11 +32,16 @@ def close_connection(execute):
def wrapper(*args, **kwargs):
try:
log = execute(*args, **kwargs)
except:
logger.error('EXCEPTION executing backend %s %s' % (str(args), str(kwargs)))
raise
except Exception as e:
subject = 'EXCEPTION executing backend(s) %s %s' % (str(args), str(kwargs))
message = traceback.format_exc()
logger.error(subject)
logger.error(message)
mail_admins(subject, message)
# We don't propagate the exception further to avoid transaction rollback
else:
# Using the wrapper function as threader messenger for the execute output
# Absense of it will indicate a failure at this stage
wrapper.log = log
finally:
db.connection.close()
@ -78,13 +86,18 @@ def execute(operations, async=False):
logs = []
# collect results
for execution, operations in executions:
for operation in operations:
logger.info("Executed %s" % str(operation))
operation.log = execution.log
operation.save()
stdout = execution.log.stdout.strip()
stdout and logger.debug('STDOUT %s', stdout)
stderr = execution.log.stderr.strip()
stderr and logger.debug('STDERR %s', stderr)
logs.append(execution.log)
# There is no log if an exception has been rised at the very end of the execution
if hasattr(execution, 'log'):
for operation in operations:
logger.info("Executed %s" % str(operation))
operation.log = execution.log
operation.save()
stdout = execution.log.stdout.strip()
stdout and logger.debug('STDOUT %s', stdout)
stderr = execution.log.stderr.strip()
stderr and logger.debug('STDERR %s', stderr)
logs.append(execution.log)
else:
mocked_log = BackendLog(state=BackendLog.EXCEPTION)
logs.append(mocked_log)
return logs

View file

@ -52,6 +52,8 @@ class BackendLog(models.Model):
FAILURE = 'FAILURE'
ERROR = 'ERROR'
REVOKED = 'REVOKED'
# Special state for mocked backendlogs
EXCEPTION = 'EXCEPTION'
STATES = (
(RECEIVED, RECEIVED),

View file

@ -26,9 +26,9 @@ class PaymentMethod(plugins.Plugin):
return plugins
@classmethod
def clean_data(cls, data):
def clean_data(cls):
""" model clean, uses cls.serializer by default """
serializer = cls.serializer(data=data)
serializer = cls.serializer(data=self.instance.data)
if not serializer.is_valid():
serializer.errors.pop('non_field_errors', None)
raise ValidationError(serializer.errors)
@ -43,11 +43,11 @@ class PaymentMethod(plugins.Plugin):
self.serializer.plugin = self
return self.serializer
def get_label(self, data):
return data[self.label_field]
def get_label(self):
return self.instance.data[self.label_field]
def get_number(self, data):
return data[self.number_field]
def get_number(self):
return self.instance.data[self.number_field]
def get_bill_message(self, source):
def get_bill_message(self):
return ''

View file

@ -45,9 +45,9 @@ class SEPADirectDebit(PaymentMethod):
serializer = SEPADirectDebitSerializer
due_delta = datetime.timedelta(days=5)
def get_bill_message(self, source):
def get_bill_message(self):
return _("This bill will been automatically charged to your bank account "
" with IBAN number<br><strong>%s</strong>.") % source.number
" with IBAN number<br><strong>%s</strong>.") % self.instance.number
@classmethod
def process(cls, transactions):

View file

@ -36,27 +36,27 @@ class PaymentSource(models.Model):
@cached_property
def service_instance(self):
""" Per request lived method_instance """
return self.method_class()
return self.method_class(self)
@cached_property
def label(self):
return self.method_instance.get_label(self.data)
return self.method_instance.get_label()
@cached_property
def number(self):
return self.method_instance.get_number(self.data)
return self.method_instance.get_number()
def get_bill_context(self):
method = self.method_instance
return {
'message': method.get_bill_message(self),
'message': method.get_bill_message(),
}
def get_due_delta(self):
return self.method_instance.due_delta
def clean(self):
self.data = self.method_instance.clean_data(self.data)
self.data = self.method_instance.clean_data()
class TransactionQuerySet(models.QuerySet):

View file

@ -38,13 +38,13 @@ class SaaS(models.Model):
@cached_property
def service_instance(self):
""" Per request lived service_instance """
return self.service_class()
return self.service_class(self)
def get_site_name(self):
return self.service_instance.get_site_name(self)
return self.service_instance.get_site_name()
def clean(self):
self.data = self.service_instance.clean_data(self)
self.data = self.service_instance.clean_data()
def set_password(self, password):
self.password = password

View file

@ -84,10 +84,9 @@ class SoftwareService(plugins.Plugin):
plugins.append(import_class(cls))
return plugins
@classmethod
def clean_data(cls, saas):
def clean_data(cls):
""" model clean, uses cls.serizlier by default """
serializer = cls.serializer(data=saas.data)
serializer = cls.serializer(data=self.instance.data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)
return serializer.data
@ -96,8 +95,10 @@ class SoftwareService(plugins.Plugin):
def get_change_readonly_fileds(cls):
return cls.change_readonly_fileds + ('username',)
def get_site_name(self, saas):
return self.site_name or '.'.join((saas.site_name, self.site_name_base_domain))
def get_site_name(self):
return self.site_name or '.'.join(
(self.instance.site_name, self.site_name_base_domain)
)
def get_form(self):
self.form.plugin = self

View file

@ -69,11 +69,12 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
url = change_url(website)
name = "%s on %s" % (website.name, content.path)
websites.append('<a href="%s">%s</a>' % (url, name))
add_url = reverse('admin:websites_website_add')
# TODO support for preselecting related web app on website
add_url += '?account=%s' % webapp.account_id
plus = '<strong style="color:green; font-size:12px">+</strong>'
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))
if not websites:
add_url = reverse('admin:websites_website_add')
# TODO support for preselecting related web app on website
add_url += '?account=%s' % webapp.account_id
plus = '<strong style="color:green; font-size:12px">+</strong>'
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))
return '<br>'.join(websites)
display_websites.short_description = _("web sites")
display_websites.allow_tags = True

View file

@ -9,13 +9,22 @@ class WebAppServiceMixin(object):
directive = None
def create_webapp_dir(self, context):
self.append("[[ ! -e %(app_path)s ]] && CREATED=true" % context)
self.append("mkdir -p %(app_path)s" % context)
self.append("chown %(user)s:%(group)s %(app_path)s" % context)
self.append(textwrap.dedent("""\
CREATED=0
[[ ! -e %(app_path)s ]] && CREATED=1
mkdir -p %(app_path)s
chown %(user)s:%(group)s %(app_path)s
""") % context
)
def set_under_construction(self, context):
if context['under_construction_path']:
self.append("[[ $CREATED ]] && cp -r %(under_construction_path)s %(app_path)s" % context)
self.append(textwrap.dedent("""\
if [[ $CREATED == 1 ]]; then
cp -r %(under_construction_path)s %(app_path)s
chown -R %(user)s:%(group)s %(app_path)s
fi""") % context
)
def delete_webapp_dir(self, context):
self.append("rm -fr %(app_path)s" % context)

View file

@ -13,7 +13,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
""" Per-webapp fcgid application """
verbose_name = _("PHP-Fcgid")
directive = 'fcgid'
default_route_match = "webapp.type.endswith('-fcgid')"
default_route_match = "webapp.type_class.php_execution == 'fcgid'"
def save(self, webapp):
context = self.get_context(webapp)
@ -37,6 +37,8 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
echo -e '%(cmd_options)s' > %(cmd_options_path)s; UPDATED_APACHE=1
}""" ) % context
)
else:
self.append("rm -f %(cmd_options_path)s" % context)
def delete(self, webapp):
context = self.get_context(webapp)
@ -50,14 +52,14 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
def get_fcgid_wrapper(self, webapp, context):
opt = webapp.type_instance
# Format PHP init vars
init_vars = opt.get_php_init_vars(webapp)
init_vars = opt.get_php_init_vars()
if init_vars:
init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ]
init_vars = ', '.join(init_vars)
context.update({
'php_binary': opt.php_binary,
'php_rc': opt.php_rc,
'php_binary': opt.get_php_binary_path(),
'php_rc': opt.get_php_rc_path(),
'php_init_vars': init_vars,
})
return textwrap.dedent("""\
@ -82,7 +84,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
def get_context(self, webapp):
context = super(PHPFcgidBackend, self).get_context(webapp)
wrapper_path = settings.WEBAPPS_FCGID_PATH % context
wrapper_path = settings.WEBAPPS_FCGID_WRAPPER_PATH % context
context.update({
'wrapper': self.get_fcgid_wrapper(webapp, context),
'wrapper_path': wrapper_path,

View file

@ -13,7 +13,7 @@ from .. import settings
class PHPFPMBackend(WebAppServiceMixin, ServiceController):
""" Per-webapp php application """
verbose_name = _("PHP-FPM")
default_route_match = "webapp.type.endswith('-fpm')"
default_route_match = "webapp.type_class.php_execution == 'fpm'"
def save(self, webapp):
context = self.get_context(webapp)
@ -45,7 +45,7 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController):
def get_fpm_config(self, webapp, context):
context.update({
'init_vars': webapp.type_instance.get_php_init_vars(webapp),
'init_vars': webapp.type_instance.get_php_init_vars(),
'fpm_port': webapp.get_fpm_port(),
'max_children': webapp.get_options().get('processes', False),
'request_terminate_timeout': webapp.get_options().get('timeout', False),
@ -76,4 +76,3 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController):
'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context,
})
return context

View file

@ -44,12 +44,12 @@ class WebApp(models.Model):
@cached_property
def type_instance(self):
""" Per request lived type_instance """
return self.type_class()
return self.type_class(self)
def clean(self):
apptype = self.type_instance
apptype.validate(self)
self.data = apptype.clean_data(self)
apptype.validate()
self.data = apptype.clean_data()
@cached
def get_options(self):
@ -58,7 +58,7 @@ class WebApp(models.Model):
}
def get_directive(self):
return self.type_instance.get_directive(self)
return self.type_instance.get_directive()
def get_path(self):
context = {
@ -102,10 +102,10 @@ class WebAppOption(models.Model):
@cached_property
def option_instance(self):
""" Per request lived option instance """
return self.option_class()
return self.option_class(self)
def clean(self):
self.option_instance.validate(self)
self.option_instance.validate()
services.register(WebApp)
@ -117,9 +117,9 @@ services.register(WebApp)
@receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save')
def type_save(sender, *args, **kwargs):
instance = kwargs['instance']
instance.type_instance.save(instance)
instance.type_instance.save()
@receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete')
def type_delete(sender, *args, **kwargs):
instance = kwargs['instance']
instance.type_instance.delete(instance)
instance.type_instance.delete()

View file

@ -37,12 +37,12 @@ class AppOption(Plugin):
groups[opt.group] = [opt]
return groups
def validate(self, option):
if self.regex and not re.match(self.regex, option.value):
def validate(self):
if self.regex and not re.match(self.regex, self.instance.value):
raise ValidationError({
'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
params={
'value': option.value,
'value': self.instance.value,
'regex': self.regex
}),
})

View file

@ -2,7 +2,9 @@ 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)s/webapps/%(app_name)s/')
WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN',
# '127.0.0.1:9%(app_id)03d
@ -13,11 +15,12 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH',
'/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf')
WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH',
WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH',
'/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper')
WEBAPPS_FCGID_CMD_OPTIONS_PATH = getattr(settings, 'WEBAPPS_FCGID_CMD_OPTIONS_PATH',
# Loaded by Apache
'/etc/apache2/fcgid-conf/%(user)s-%(app_name)s.conf')
@ -25,19 +28,50 @@ WEBAPPS_PHP_ERROR_LOG_PATH = getattr(settings, 'WEBAPPS_PHP_ERROR_LOG_PATH',
'')
WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', (
'orchestra.apps.webapps.types.PHP54App',
'orchestra.apps.webapps.types.PHP53App',
'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',
'orchestra.apps.webapps.types.DokuWikiMuApp',
'orchestra.apps.webapps.types.DrupalMuApp',
'orchestra.apps.webapps.types.SymbolicLinkApp',
'orchestra.apps.webapps.types.WordPressApp',
'orchestra.apps.webapps.types.php.PHPFPMApp',
'orchestra.apps.webapps.types.php.PHPFCGIDApp',
'orchestra.apps.webapps.types.misc.StaticApp',
'orchestra.apps.webapps.types.misc.WebalizerApp',
'orchestra.apps.webapps.types.saas.WordPressMuApp',
'orchestra.apps.webapps.types.saas.DokuWikiMuApp',
'orchestra.apps.webapps.types.saas.DrupalMuApp',
'orchestra.apps.webapps.types.misc.SymbolicLinkApp',
'orchestra.apps.webapps.types.wordpress.WordPressFPMApp',
'orchestra.apps.webapps.types.wordpress.WordPressFCGIDApp',
))
WEBAPPS_PHP_FCGID_VERSIONS = getattr(settings, 'WEBAPPS_PHP_FCGID_VERSIONS', (
('5.4', '5.4'),
('5.3', '5.3'),
('5.2', '5.2'),
('4', '4'),
))
WEBAPPS_PHP_FCGID_DEFAULT_VERSION = getattr(settings, 'WEBAPPS_PHP_FCGID_DEFAULT_VERSION',
'5.4')
WEBAPPS_PHP_CGI_BINARY_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_BINARY_PATH',
# Path of the cgi binary used by fcgid
'/usr/bin/php%(php_version)s-cgi')
WEBAPPS_PHP_CGI_RC_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_RC_PATH',
# Path to php.ini
'/etc/php%(php_version)s/cgi/')
WEBAPPS_PHP_FPM_VERSIONS = getattr(settings, 'WEBAPPS_PHP_FPM_VERSIONS', (
('5.4', '5.4'),
))
WEBAPPS_PHP_FPM_DEFAULT_VERSION = getattr(settings, 'WEBAPPS_PHP_DEFAULT_VERSION',
'5.4')
WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_PATH',
# Server-side path where a under construction stock page is
# '/var/www/undercontruction/index.html',
@ -51,14 +85,6 @@ WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_
# WEBAPPS_TYPES[webapp_type] = value
WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5')
WEBAPPS_DEFAULT_HTTPS_CERT = getattr(settings, 'WEBAPPS_DEFAULT_HTTPS_CERT',
('/etc/apache2/cert', '/etc/apache2/cert.key')
)
WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTION', [
'exec',
'passthru',

View file

@ -1,408 +0,0 @@
import os
from django import forms
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra import plugins
from orchestra.plugins.forms import PluginDataForm
from orchestra.core import validators
from orchestra.forms import widgets
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):
name = None
verbose_name = ""
help_text= ""
form = PluginDataForm
change_form = None
serializer = None
icon = 'orchestra/icons/apps.png'
unique_name = False
option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP)
@classmethod
@cached
def get_plugins(cls):
plugins = []
for cls in settings.WEBAPPS_TYPES:
plugins.append(import_class(cls))
return plugins
def clean_data(self, webapp):
""" model clean, uses cls.serizlier by default """
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'
return self.form
def get_change_form(self):
form = self.change_form or self.form
form.plugin = self
form.plugin_field = 'type'
return form
def get_serializer(self):
self.serializer.plugin = self
return self.serializer
def validate(self, instance):
""" Unique name validation """
if self.unique_name:
if not instance.pk and Webapp.objects.filter(name=instance.name, type=instance.type).exists():
raise ValidationError({
'name': _("A WordPress blog with this name already exists."),
})
@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):
""" Generates grouped choices ready to use in Field.choices """
# generators can not be @cached
yield (None, '-------')
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
def delete(self, instance):
pass
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 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 % context
return ('fpm', socket_type, socket, webapp.get_path())
def get_context(self, webapp):
""" context used to format settings """
return {
'home': webapp.account.main_systemuser.get_home(),
'account': webapp.account.username,
'user': webapp.account.username,
'app_name': webapp.name,
}
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)
php_options = [option.name for option in type(self).get_php_options()]
for opt in options:
if opt.name in php_options:
init_vars[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['dissabled_functions'] = ','.join(disabled_functions)
if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars:
context = self.get_context(webapp)
error_log_path = os.path.normpath(settings.WEBAPPS_PHP_ERROR_LOG_PATH % context)
init_vars['error_log'] = error_log_path
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.")
icon = 'orchestra/icons/apps/PHPFPM.png'
class PHP53App(PHPAppType):
name = 'php5.3-fcgid'
php_version = 5.3
php_binary = '/usr/bin/php5-cgi'
php_rc = '/etc/php5/cgi/'
verbose_name = "PHP 5.3 FCGID"
help_text = _("This creates a PHP5.3 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 = os.path.normpath(settings.WEBAPPS_FCGID_PATH % context)
return ('fcgid', webapp.get_path(), wrapper_path)
class PHP52App(PHP53App):
name = 'php5.2-fcgid'
php_version = 5.2
php_binary = '/usr/bin/php5.2-cgi'
php_rc = '/etc/php5.2/cgi/'
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'
class PHP4App(PHP53App):
name = 'php4-fcgid'
php_version = 4
php_binary = '/usr/bin/php4-cgi'
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'
class StaticApp(AppType):
name = 'static'
verbose_name = "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'
option_groups = (AppOption.FILESYSTEM,)
class WebalizerApp(AppType):
name = 'webalizer'
verbose_name = "Webalizer"
directive = ('static', '%(app_path)s%(site_name)s')
help_text = _("This creates a Webalizer application under "
"~/webapps/&lt;app_name&gt;-&lt;site_name&gt;")
icon = 'orchestra/icons/apps/Stats.png'
option_groups = ()
def get_directive(self, webapp):
webalizer_path = os.path.join(webapp.get_path(), '%(site_name)s')
webalizer_path = os.path.normpath(webalizer_path)
return ('static', webalizer_path)
class WordPressMuApp(PHPAppType):
name = 'wordpress-mu'
verbose_name = "WordPress (SaaS)"
directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/')
help_text = _("This creates a WordPress site on a multi-tenant WordPress server.<br>"
"By default this blog is accessible via &lt;app_name&gt;.blogs.orchestra.lan")
icon = 'orchestra/icons/apps/WordPressMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN
class DokuWikiMuApp(PHPAppType):
name = 'dokuwiki-mu'
verbose_name = "DokuWiki (SaaS)"
directive = ('alias', '/home/httpd/wikifarm/farm/')
help_text = _("This create a DokuWiki wiki into a shared DokuWiki server.<br>"
"By default this wiki is accessible via &lt;app_name&gt;.wikis.orchestra.lan")
icon = 'orchestra/icons/apps/DokuWikiMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN
class MoodleMuApp(PHPAppType):
name = 'moodle-mu'
verbose_name = "Moodle (SaaS)"
directive = ('alias', '/home/httpd/wikifarm/farm/')
help_text = _("This create a Moodle site into a shared Moodle server.<br>"
"By default this wiki is accessible via &lt;app_name&gt;.moodle.orchestra.lan")
icon = 'orchestra/icons/apps/MoodleMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_MOODLEMU_LISTEN
class DrupalMuApp(PHPAppType):
name = 'drupal-mu'
verbose_name = "Drupdal (SaaS)"
directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/')
help_text = _("This creates a Drupal site into a multi-tenant Drupal server.<br>"
"The installation will be completed after visiting "
"http://&lt;app_name&gt;.drupal.orchestra.lan/install.php?profile=standard<br>"
"By default this site will be accessible via &lt;app_name&gt;.drupal.orchestra.lan")
icon = 'orchestra/icons/apps/DrupalMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN
from rest_framework import serializers
from orchestra.forms import widgets
class SymbolicLinkForm(PluginDataForm):
path = forms.CharField(label=_("Path"), widget=forms.TextInput(attrs={'size':'100'}),
help_text=_("Path for the origin of the symbolic link."))
class SymbolicLinkSerializer(serializers.Serializer):
path = serializers.CharField(label=_("Path"))
class SymbolicLinkApp(PHPAppType):
name = 'symbolic-link'
verbose_name = "Symbolic link"
form = SymbolicLinkForm
serializer = SymbolicLinkSerializer
icon = 'orchestra/icons/apps/SymbolicLink.png'
change_readonly_fileds = ('path',)
class WordPressForm(PluginDataForm):
db_name = forms.CharField(label=_("Database name"),
help_text=_("Database used for this webapp."))
db_user = forms.CharField(label=_("Database user"),)
db_pass = forms.CharField(label=_("Database user password"),
help_text=_("Initial database password."))
class WordPressSerializer(serializers.Serializer):
db_name = serializers.CharField(label=_("Database name"), required=False)
db_user = serializers.CharField(label=_("Database user"), required=False)
db_pass = serializers.CharField(label=_("Database user password"), required=False)
from orchestra.apps.databases.models import Database, DatabaseUser
from orchestra.utils.python import random_ascii
class WordPressApp(PHPAppType):
name = 'wordpress'
verbose_name = "WordPress"
icon = 'orchestra/icons/apps/WordPress.png'
change_form = WordPressForm
serializer = WordPressSerializer
change_readonly_fileds = ('db_name', 'db_user', 'db_pass',)
help_text = _("Visit http://&lt;domain.lan&gt;/wp-admin/install.php to finish the installation.")
def get_db_name(self, webapp):
db_name = 'wp_%s_%s' % (webapp.name, webapp.account)
# Limit for mysql database names
return db_name[:65]
def get_db_user(self, webapp):
db_name = self.get_db_name(webapp)
# Limit for mysql user names
return db_name[:17]
def get_db_pass(self):
return random_ascii(10)
def validate(self, webapp):
create = not webapp.pk
if create:
db = Database(name=self.get_db_name(webapp), account=webapp.account)
user = DatabaseUser(username=self.get_db_user(webapp), password=self.get_db_pass(),
account=webapp.account)
for obj in (db, user):
try:
obj.full_clean()
except ValidationError, e:
raise ValidationError({
'name': e.messages,
})
def save(self, webapp):
create = not webapp.pk
if create:
db_name = self.get_db_name(webapp)
db_user = self.get_db_user(webapp)
db_pass = self.get_db_pass()
db = Database.objects.create(name=db_name, account=webapp.account)
user = DatabaseUser(username=db_user, account=webapp.account)
user.set_password(db_pass)
user.save()
db.users.add(user)
webapp.data = {
'db_name': db_name,
'db_user': db_user,
'db_pass': db_pass,
}
else:
# Trigger related backends
for related in self.get_related(webapp):
related.save()
def delete(self, webapp):
for related in self.get_related(webapp):
related.delete()
def get_related(self, webapp):
related = []
try:
db_user = DatabaseUser.objects.get(username=webapp.data.get('db_user'))
except DatabaseUser.DoesNotExist:
pass
else:
related.append(db_user)
try:
db = Database.objects.get(name=webapp.data.get('db_name'))
except Database.DoesNotExist:
pass
else:
related.append(db)
return related

View file

@ -0,0 +1,119 @@
from django.core.exceptions import ValidationError
from orchestra import plugins
from orchestra.plugins.forms import PluginDataForm
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class
from .. import settings
from ..options import AppOption
class AppType(plugins.Plugin):
name = None
verbose_name = ""
help_text= ""
form = PluginDataForm
change_form = None
serializer = None
icon = 'orchestra/icons/apps.png'
unique_name = False
option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP)
# TODO generic name like 'execution' ?
php_execution = None
@classmethod
@cached
def get_plugins(cls):
plugins = []
for cls in settings.WEBAPPS_TYPES:
plugins.append(import_class(cls))
return plugins
def clean_data(self):
""" model clean, uses cls.serizlier by default """
if self.serializer:
serializer = self.serializer(data=self.instance.data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)
return serializer.data
return {}
def get_directive(self):
raise NotImplementedError
def get_form(self):
self.form.plugin = self
self.form.plugin_field = 'type'
return self.form
def get_change_form(self):
form = self.change_form or self.form
form.plugin = self
form.plugin_field = 'type'
return form
def get_serializer(self):
self.serializer.plugin = self
return self.serializer
def validate(self):
""" Unique name validation """
if self.unique_name:
if not self.instance.pk and Webapp.objects.filter(name=self.instance.name, type=self.instance.type).exists():
raise ValidationError({
'name': _("A WordPress blog with this name already exists."),
})
@classmethod
@cached
def get_php_options(cls):
# TODO validate php options once a php version has been selected (deprecated directives)
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):
""" Generates grouped choices ready to use in Field.choices """
# generators can not be @cached
yield (None, '-------')
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):
pass
def delete(self):
pass
def get_related_objects(self):
pass
def get_directive_context(self):
return {
'app_id': self.instance.id,
'app_name': self.instance.name,
'user': self.instance.account.username,
}

View file

@ -0,0 +1,59 @@
import os
from django import forms
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orchestra.plugins.forms import PluginDataForm
from ..options import AppOption
from . import AppType
from .php import PHPAppType
class StaticApp(AppType):
name = 'static'
verbose_name = "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'
option_groups = (AppOption.FILESYSTEM,)
def get_directive(self):
return ('static', self.instance.get_path())
class WebalizerApp(AppType):
name = 'webalizer'
verbose_name = "Webalizer"
directive = ('static', '%(app_path)s%(site_name)s')
help_text = _("This creates a Webalizer application under "
"~/webapps/&lt;app_name&gt;-&lt;site_name&gt;")
icon = 'orchestra/icons/apps/Stats.png'
option_groups = ()
def get_directive(self, webapp):
webalizer_path = os.path.join(webapp.get_path(), '%(site_name)s')
webalizer_path = os.path.normpath(webalizer_path)
return ('static', webalizer_path)
class SymbolicLinkForm(PluginDataForm):
path = forms.CharField(label=_("Path"), widget=forms.TextInput(attrs={'size':'100'}),
help_text=_("Path for the origin of the symbolic link."))
class SymbolicLinkSerializer(serializers.Serializer):
path = serializers.CharField(label=_("Path"))
class SymbolicLinkApp(PHPAppType):
name = 'symbolic-link'
verbose_name = "Symbolic link"
form = SymbolicLinkForm
serializer = SymbolicLinkSerializer
icon = 'orchestra/icons/apps/SymbolicLink.png'
change_readonly_fileds = ('path',)

View file

@ -0,0 +1,131 @@
import os
from django import forms
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orchestra.forms import widgets
from orchestra.plugins.forms import PluginDataForm
from .. import settings
from . import AppType
class PHPAppType(AppType):
FPM = 'fpm'
FCGID = 'fcgid'
php_version = 5.4
fpm_listen = settings.WEBAPPS_FPM_LISTEN
def get_context(self):
""" context used to format settings """
return {
'home': self.instance.account.main_systemuser.get_home(),
'account': self.instance.account.username,
'user': self.instance.account.username,
'app_name': self.instance.name,
}
def get_php_init_vars(self, per_account=False):
"""
process php options for inclusion on php.ini
per_account=True merges all (account, webapp.type) options
"""
init_vars = {}
options = self.instance.options.all()
if per_account:
options = self.instance.account.webapps.filter(webapp_type=self.instance.type)
php_options = [option.name for option in type(self).get_php_options()]
for opt in options:
if opt.name in php_options:
init_vars[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['dissabled_functions'] = ','.join(disabled_functions)
if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars:
context = self.get_context()
error_log_path = os.path.normpath(settings.WEBAPPS_PHP_ERROR_LOG_PATH % context)
init_vars['error_log'] = error_log_path
return init_vars
class PHPFPMAppForm(PluginDataForm):
php_version = forms.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FPM_VERSIONS,
initial=settings.WEBAPPS_PHP_FPM_DEFAULT_VERSION)
class PHPFPMAppSerializer(serializers.Serializer):
php_version = serializers.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FPM_VERSIONS,
default=settings.WEBAPPS_PHP_FPM_DEFAULT_VERSION)
class PHPFPMApp(PHPAppType):
name = 'php-fpm'
php_execution = PHPAppType.FPM
verbose_name = "PHP FPM"
help_text = _("This creates a PHP application under ~/webapps/&lt;app_name&gt;<br>"
"PHP-FPM will be used to execute PHP files.")
icon = 'orchestra/icons/apps/PHPFPM.png'
form = PHPFPMAppForm
serializer = PHPFPMAppSerializer
def get_directive(self):
context = self.get_directive_context()
socket_type = 'unix'
if ':' in self.fpm_listen:
socket_type = 'tcp'
socket = self.fpm_listen % context
return ('fpm', socket_type, socket, self.instance.get_path())
class PHPFCGIDAppForm(PluginDataForm):
php_version = forms.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FCGID_VERSIONS,
initial=settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION)
class PHPFCGIDAppSerializer(serializers.Serializer):
php_version = serializers.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FCGID_VERSIONS,
default=settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION)
class PHPFCGIDApp(PHPAppType):
name = 'php-fcgid'
php_execution = PHPAppType.FCGID
verbose_name = "PHP FCGID"
help_text = _("This creates a PHP application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.")
icon = 'orchestra/icons/apps/PHPFCGI.png'
form = PHPFCGIDAppForm
serializer = PHPFCGIDAppSerializer
def get_directive(self):
context = self.get_directive_context()
wrapper_path = os.path.normpath(settings.WEBAPPS_FCGID_PATH % context)
return ('fcgid', self.instance.get_path(), wrapper_path)
def get_php_binary_path(self):
default_version = settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION
context = {
'php_version': self.instance.data.get('php_version', default_version)
}
return os.path.normpath(settings.WEBAPPS_PHP_CGI_BINARY_PATH % context)
def get_php_rc_path(self):
default_version = settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION
context = {
'php_version': self.instance.data.get('php_version', default_version)
}
return os.path.normpath(settings.WEBAPPS_PHP_CGI_RC_PATH % context)

View file

@ -0,0 +1,54 @@
from django.utils.translation import ugettext_lazy as _
from . import AppType
from .. import settings
class WordPressMuApp(AppType):
name = 'wordpress-mu'
verbose_name = "WordPress (SaaS)"
directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/')
help_text = _("This creates a WordPress site on a multi-tenant WordPress server.<br>"
"By default this blog is accessible via &lt;app_name&gt;.blogs.orchestra.lan")
icon = 'orchestra/icons/apps/WordPressMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN
class DokuWikiMuApp(AppType):
name = 'dokuwiki-mu'
verbose_name = "DokuWiki (SaaS)"
directive = ('alias', '/home/httpd/wikifarm/farm/')
help_text = _("This create a DokuWiki wiki into a shared DokuWiki server.<br>"
"By default this wiki is accessible via &lt;app_name&gt;.wikis.orchestra.lan")
icon = 'orchestra/icons/apps/DokuWikiMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN
class MoodleMuApp(AppType):
name = 'moodle-mu'
verbose_name = "Moodle (SaaS)"
directive = ('alias', '/home/httpd/wikifarm/farm/')
help_text = _("This create a Moodle site into a shared Moodle server.<br>"
"By default this wiki is accessible via &lt;app_name&gt;.moodle.orchestra.lan")
icon = 'orchestra/icons/apps/MoodleMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_MOODLEMU_LISTEN
class DrupalMuApp(AppType):
name = 'drupal-mu'
verbose_name = "Drupdal (SaaS)"
directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/')
help_text = _("This creates a Drupal site into a multi-tenant Drupal server.<br>"
"The installation will be completed after visiting "
"http://&lt;app_name&gt;.drupal.orchestra.lan/install.php?profile=standard<br>"
"By default this site will be accessible via &lt;app_name&gt;.drupal.orchestra.lan")
icon = 'orchestra/icons/apps/DrupalMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN

View file

@ -0,0 +1,123 @@
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orchestra.apps.databases.models import Database, DatabaseUser
from orchestra.plugins.forms import PluginDataForm
from orchestra.utils.python import random_ascii
from .. import settings
from .php import (PHPAppType, PHPFCGIDApp, PHPFPMApp, PHPFCGIDAppForm, PHPFCGIDAppSerializer,
PHPFPMAppForm, PHPFPMAppSerializer)
class WordPressAbstractAppForm(PluginDataForm):
db_name = forms.CharField(label=_("Database name"),
help_text=_("Database used for this webapp."))
db_user = forms.CharField(label=_("Database user"),)
db_pass = forms.CharField(label=_("Database user password"),
help_text=_("Initial database password."))
class WordPressAbstractAppSerializer(serializers.Serializer):
db_name = serializers.CharField(label=_("Database name"), required=False)
db_user = serializers.CharField(label=_("Database user"), required=False)
db_pass = serializers.CharField(label=_("Database user password"), required=False)
class WordPressAbstractApp(object):
icon = 'orchestra/icons/apps/WordPress.png'
change_readonly_fileds = ('db_name', 'db_user', 'db_pass',)
help_text = _("Visit http://&lt;domain.lan&gt;/wp-admin/install.php to finish the installation.")
def get_db_name(self):
db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account)
# Limit for mysql database names
return db_name[:65]
def get_db_user(self):
db_name = self.get_db_name()
# Limit for mysql user names
return db_name[:16]
def get_db_pass(self):
return random_ascii(10)
def validate(self):
super(WordPressAbstractApp, self).validate()
create = not self.instance.pk
if create:
db = Database(name=self.get_db_name(), account=self.instance.account)
user = DatabaseUser(username=self.get_db_user(), password=self.get_db_pass(),
account=self.instance.account)
for obj in (db, user):
try:
obj.full_clean()
except ValidationError as e:
raise ValidationError({
'name': e.messages,
})
def save(self):
create = not self.instance.pk
if create:
db_name = self.get_db_name()
db_user = self.get_db_user()
db_pass = self.get_db_pass()
db = Database.objects.create(name=db_name, account=self.instance.account)
user = DatabaseUser(username=db_user, account=self.instance.account)
user.set_password(db_pass)
user.save()
db.users.add(user)
self.instance.data = {
'db_name': db_name,
'db_user': db_user,
'db_pass': db_pass,
}
else:
# Trigger related backends
for related in self.get_related():
related.save()
def delete(self):
for related in self.get_related():
related.delete()
def get_related(self):
related = []
account = self.instance.account
try:
db_user = account.databaseusers.get(username=self.instance.data.get('db_user'))
except DatabaseUser.DoesNotExist:
pass
else:
related.append(db_user)
try:
db = account.databases.get(name=self.instance.data.get('db_name'))
except Database.DoesNotExist:
pass
else:
related.append(db)
return related
class WordPressFPMApp(WordPressAbstractApp, PHPFPMApp):
name = 'wordpress-fpm'
php_execution = PHPAppType.FPM
verbose_name = "WordPress (FPM)"
serializer = type('WordPressFPMSerializer',
(WordPressAbstractAppSerializer, PHPFPMAppSerializer), {})
change_form = type('WordPressFPMForm',
(WordPressAbstractAppForm, PHPFPMAppForm), {})
class WordPressFCGIDApp(WordPressAbstractApp, PHPFCGIDApp):
name = 'wordpress-fcgid'
php_execution = PHPAppType.FCGID
verbose_name = "WordPress (FCGID)"
serializer = type('WordPressFCGIDSerializer',
(WordPressAbstractAppSerializer, PHPFCGIDAppSerializer), {})
change_form = type('WordPressFCGIDForm',
(WordPressAbstractAppForm, PHPFCGIDAppForm), {})

View file

@ -1,6 +1,6 @@
import textwrap
import os
import re
import textwrap
from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _

View file

@ -8,6 +8,10 @@ class Plugin(object):
icon = None
change_readonly_fileds = ()
def __init__(self, instance=None):
# Related model instance of this plugin
self.instance = instance
@classmethod
def get_name(cls):
return getattr(cls, 'name', cls.__name__)