Lots of improvements on webapps and saas

This commit is contained in:
Marc Aymerich 2015-03-25 15:45:04 +00:00
parent 40930a480e
commit dd84217320
29 changed files with 390 additions and 154 deletions

19
TODO.md
View File

@ -241,12 +241,25 @@ require_once(/etc/moodles/.$moodle_host.config.php);``` moodle/drupl
* WPMU blog traffic
* normurlpath '' returns '/'
* normurlpath '' return '/'
* rename webapps.type to something more generic
* initial configuration of multisite sas apps with password stored in DATA
* websites links on webpaps ans saas
* webapps installation complete, passowrd protected
* saas.initial_password autogenerated (ok because its random and not user provided) vs saas.password /change_Form provided + send email with initial_password
* disable saas apps
* more robust backend error handling, continue executing but exit code > 0 if failure, replace exit_code=0; do_sometging || exit_code=1
* saas require unique emails? connect to backend server to find out because they change
* automaitcally set passwords and email users?
* website directives uniquenes validation on serializers
* gitlab store id, username changes
* /var/lib/fcgid/wrappers/ rm write permissions

View File

@ -38,10 +38,11 @@ class MySQLBackend(ServiceController):
if database.type != database.MYSQL:
return
context = self.get_context(database)
self.append("mysql -e 'DROP DATABASE `%(database)s`;'" % context)
self.append("mysql -e 'DROP DATABASE `%(database)s`;' || exit_code=1" % context)
self.append("mysql mysql -e 'DELETE FROM db WHERE db = \"%(database)s\";'" % context)
def commit(self):
super(MySQLBackend, self).commit()
self.append("mysql -e 'FLUSH PRIVILEGES;'")
def get_context(self, database):

View File

@ -36,8 +36,6 @@ 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 -k fail -n fail')
DOMAINS_CHECKZONE_PATH = getattr(settings, 'DOMAINS_CHECKZONE_PATH', '/dev/shm')
DOMAINS_DEFAULT_A = getattr(settings, 'DOMAINS_DEFAULT_A', '10.0.3.13')

View File

@ -108,11 +108,10 @@ def validate_soa_record(value):
def validate_zone(zone):
""" Ultimate zone file validation using named-checkzone """
zone_name = zone.split()[0][:-1]
path = os.path.join(settings.DOMAINS_CHECKZONE_PATH, zone_name)
with open(path, 'wb') as f:
f.write(zone)
checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH
check = run(' '.join([checkzone, zone_name, path]), error_codes=[0,1], display=False)
cmd = ' '.join(["echo -e '%s'" % zone, '|', checkzone, zone_name, '/dev/stdin'])
print cmd
check = run(cmd, error_codes=[0, 1], display=False)
if check.return_code == 1:
errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-1]
raise ValidationError(', '.join(errors))

View File

@ -177,7 +177,8 @@ class ServiceBackend(plugins.Plugin):
"""
self.append(
'set -e\n'
'set -o pipefail'
'set -o pipefail\n'
'exit_code=0;'
)
def commit(self):
@ -187,7 +188,7 @@ class ServiceBackend(plugins.Plugin):
reloading a service is done in a separated method in order to reload
the service once in bulk operations
"""
self.append('exit 0')
self.append('exit $exit_code')
class ServiceController(ServiceBackend):

View File

@ -22,18 +22,10 @@ def as_task(execute):
def wrapper(*args, **kwargs):
""" send report """
# Tasks run on a separate transaction pool (thread), no need to temper with the transaction
try:
log = execute(*args, **kwargs)
if log.state != log.SUCCESS:
send_report(execute, args, log)
return log
return wrapper
def close_connection(execute):
""" Threads have their own connection pool, closing it when finishing """
def wrapper(*args, **kwargs):
try:
log = execute(*args, **kwargs)
except Exception as e:
subject = 'EXCEPTION executing backend(s) %s %s' % (str(args), str(kwargs))
message = traceback.format_exc()
@ -45,6 +37,19 @@ def close_connection(execute):
# Using the wrapper function as threader messenger for the execute output
# Absense of it will indicate a failure at this stage
wrapper.log = log
return log
return wrapper
def close_connection(execute):
""" Threads have their own connection pool, closing it when finishing """
def wrapper(*args, **kwargs):
try:
log = execute(*args, **kwargs)
except:
pass
else:
wrapper.log = log
finally:
db.connection.close()
return wrapper
@ -89,14 +94,14 @@ def execute(operations, async=False):
backend, operations = value
backend.commit()
execute = as_task(backend.execute)
execute = close_connection(execute)
# DEBUG: substitute all thread related stuff for this function
#execute(server, async=async)
logger.debug('%s is going to be executed on %s' % (backend, server))
if block:
# Execute one bakend at a time, no need for threads
execute(server, async=async)
else:
execute = close_connection(execute)
thread = threading.Thread(target=execute, args=(server,), kwargs={'async': async})
thread.start()
if block:
thread.join()
threads.append(thread)
executions.append((execute, operations))
[ thread.join() for thread in threads ]
@ -108,6 +113,8 @@ def execute(operations, async=False):
for operation in operations:
logger.info("Executed %s" % str(operation))
operation.log = execution.log
if operation.object_id:
# Not all backends are call with objects saved on the database
operation.save()
stdout = execution.log.stdout.strip()
stdout and logger.debug('STDOUT %s', stdout)

View File

@ -6,6 +6,7 @@ def compute_resource_usage(data):
resource = data.resource
result = 0
has_result = False
today = datetime.date.today()
for dataset in data.get_monitor_datasets():
if resource.period == resource.MONTHLY_AVG:
last = dataset.latest()

View File

@ -11,18 +11,19 @@ from .services import SoftwareService
class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('username', 'service', 'display_site_name', 'account_link')
list_display = ('name', 'service', 'display_site_domain', 'account_link')
list_filter = ('service',)
change_readonly_fields = ('service',)
plugin = SoftwareService
plugin_field = 'service'
plugin_title = 'Software as a Service'
def display_site_name(self, saas):
site_name = saas.get_site_name()
return '<a href="http://%s">%s</a>' % (site_name, site_name)
display_site_name.short_description = _("Site name")
display_site_name.allow_tags = True
display_site_name.admin_order_field = 'site_name'
def display_site_domain(self, saas):
site_domain = saas.get_site_domain()
return '<a href="http://%s">%s</a>' % (site_domain, site_domain)
display_site_domain.short_description = _("Site domain")
display_site_domain.allow_tags = True
display_site_domain.admin_order_field = 'name'
admin.site.register(SaaS, SaaSAdmin)

View File

@ -0,0 +1,101 @@
import json
import requests
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from .. import settings
class GitLabSaaSBackend(ServiceController):
verbose_name = _("GitLab SaaS")
model = 'saas.SaaS'
default_route_match = "saas.service == 'gitlab'"
block = True
actions = ('save', 'delete', 'validate_creation')
def get_base_url(self):
return 'https://%s/api/v3' % settings.SAAS_GITLAB_DOMAIN
def get_user_url(self, saas):
user_id = saas.data['user_id']
return self.get_base_url() + '/users/%i' % user_id
def validate_response(self, response, status_codes):
if response.status_code not in status_codes:
raise RuntimeError("[%i] %s" % (response.status_code, response.content))
def authenticate(self):
login_url = self.get_base_url() + '/session'
data = {
'login': 'root',
'password': settings.SAAS_GITLAB_ROOT_PASSWORD,
}
response = requests.post(login_url, data=data)
self.validate_response(response, [201])
token = json.loads(response.content)['private_token']
self.headers = {
'PRIVATE-TOKEN': token,
}
def create_user(self, saas, server):
self.authenticate()
user_url = self.get_base_url() + '/users'
data = {
'email': saas.data['email'],
'password': saas.password,
'username': saas.name,
'name': saas.account.get_full_name(),
}
response = requests.post(user_url, data=data, headers=self.headers)
self.validate_response(response, [201])
print response.content
user = json.loads(response.content)
saas.data['user_id'] = user['id']
# Using queryset update to avoid triggering backends with the post_save signal
type(saas).objects.filter(pk=saas.pk).update(data=saas.data)
print json.dumps(user, indent=4)
def change_password(self, saas, server):
self.authenticate()
user_url = self.get_user_url(saas)
data = {
'password': saas.password,
}
response = requests.patch(user_url, data=data, headers=self.headers)
self.validate_response(response, [200])
print json.dumps(json.loads(response.content), indent=4)
def delete_user(self, saas, server):
self.authenticate()
user_url = self.get_user_url(saas)
response = requests.delete(user_url, headers=self.headers)
self.validate_response(response, [200, 404])
print json.dumps(json.loads(response.content), indent=4)
def _validate_creation(self, saas, server):
""" checks if a saas object is valid for creation on the server side """
self.authenticate()
username = saas.name
email = saas.data['email']
users_url = self.get_base_url() + '/users/'
users = json.loads(requests.get(users_url, headers=self.headers).content)
for user in users:
if user['username'] == username:
print 'user-exists'
if user['email'] == email:
print 'email-exists'
def validate_creation(self, saas):
self.append(self._validate_creation, saas)
def save(self, saas):
if hasattr(saas, 'password'):
if saas.data.get('user_id', None):
self.append(self.change_password, saas)
else:
self.append(self.create_user, saas)
def delete(self, saas):
self.append(self.delete_user, saas)

View File

@ -17,7 +17,7 @@ class PhpListSaaSBackend(ServiceController):
def initialize_database(self, saas, server):
base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
admin_link = 'http://%s.%s/admin/' % (saas.get_site_name(), base_domain)
admin_link = 'http://%s/admin/' % saas.get_site_domain()
admin_content = requests.get(admin_link).content
if admin_content.startswith('Cannot connect to Database'):
raise RuntimeError("Database is not yet configured")
@ -28,7 +28,7 @@ class PhpListSaaSBackend(ServiceController):
install = install.groups()[0]
install_link = admin_link + install[1:]
post = {
'adminname': saas.username,
'adminname': saas.name,
'orgname': saas.account.username,
'adminemail': saas.account.username,
'adminpassword': saas.password,

View File

@ -14,10 +14,9 @@ from .services import SoftwareService
class SaaS(models.Model):
service = models.CharField(_("service"), max_length=32,
choices=SoftwareService.get_plugin_choices())
username = models.CharField(_("name"), max_length=64,
name = models.CharField(_("Name"), max_length=64,
help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.validate_username])
# site_name = NullableCharField(_("site name"), max_length=32, null=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='saas')
data = JSONField(_("data"), default={},
@ -27,12 +26,11 @@ class SaaS(models.Model):
verbose_name = "SaaS"
verbose_name_plural = "SaaS"
unique_together = (
('username', 'service'),
# ('site_name', 'service'),
('name', 'service'),
)
def __unicode__(self):
return "%s@%s" % (self.username, self.service)
return "%s@%s" % (self.name, self.service)
@cached_property
def service_class(self):
@ -43,12 +41,12 @@ class SaaS(models.Model):
""" Per request lived service_instance """
return self.service_class(self)
def get_site_name(self):
return self.service_instance.get_site_name()
def clean(self):
self.data = self.service_instance.clean_data()
def get_site_domain(self):
return self.service_instance.get_site_domain()
def set_password(self, password):
self.password = password

View File

@ -11,12 +11,14 @@ from .options import SoftwareService, SoftwareServiceForm
class BSCWForm(SoftwareServiceForm):
email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size':'40'}))
quota = forms.IntegerField(label=_("Quota"), help_text=_("Disk quota in MB."))
quota = forms.IntegerField(label=_("Quota"), initial=settings.SAAS_BSCW_DEFAULT_QUOTA,
help_text=_("Disk quota in MB."))
class BSCWDataSerializer(serializers.Serializer):
email = serializers.EmailField(label=_("Email"))
quota = serializers.IntegerField(label=_("Quota"), help_text=_("Disk quota in MB."))
quota = serializers.IntegerField(label=_("Quota"), default=settings.SAAS_BSCW_DEFAULT_QUOTA,
help_text=_("Disk quota in MB."))
class BSCWService(SoftwareService):
@ -26,5 +28,5 @@ class BSCWService(SoftwareService):
serializer = BSCWDataSerializer
icon = 'orchestra/icons/apps/BSCW.png'
# TODO override from settings
site_name = settings.SAAS_BSCW_DOMAIN
site_domain = settings.SAAS_BSCW_DOMAIN
change_readonly_fileds = ('email',)

View File

@ -1,6 +1,50 @@
from .options import SoftwareService
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.orchestration.models import BackendOperation as Operation
from orchestra.forms import widgets
from .options import SoftwareService, SoftwareServiceForm
from .. import settings
class GitLabForm(SoftwareServiceForm):
email = forms.EmailField(label=_("Email"),
help_text=_("Initial email address, changes on the GitLab server are not reflected here."))
class GitLaChangebForm(GitLabForm):
user_id = forms.IntegerField(label=("User ID"), widget=widgets.ShowTextWidget,
help_text=_("ID of this user on the GitLab server, the only attribute that not changes."))
class GitLabSerializer(serializers.Serializer):
email = serializers.EmailField(label=_("Email"))
user_id = serializers.IntegerField(label=_("User ID"), required=False)
class GitLabService(SoftwareService):
name = 'gitlab'
form = GitLabForm
change_form = GitLaChangebForm
serializer = GitLabSerializer
site_domain = settings.SAAS_GITLAB_DOMAIN
change_readonly_fileds = ('email', 'user_id',)
verbose_name = "GitLab"
icon = 'orchestra/icons/apps/gitlab.png'
def clean_data(self):
data = super(GitLabService, self).clean_data()
if not self.instance.pk:
log = Operation.execute_action(self.instance, 'validate_creation')[0]
errors = {}
if 'user-exists' in log.stdout:
errors['name'] = _("User with this username already exists.")
elif 'email-exists' in log.stdout:
errors['email'] = _("User with this email address already exists.")
if errors:
raise ValidationError(errors)
return data

View File

@ -8,13 +8,13 @@ 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 orchestra.utils.python import import_class, random_ascii
from .. import settings
class SoftwareServiceForm(PluginDataForm):
site_name = forms.CharField(widget=widgets.ShowTextWidget, required=False)
site_url = forms.CharField(label=_("Site URL"), widget=widgets.ShowTextWidget, required=False)
password = forms.CharField(label=_("Password"), required=False,
widget=widgets.ReadOnlyWidget('<strong>Unknown password</strong>'),
help_text=_("Passwords are not stored, so there is no way to see this "
@ -30,25 +30,21 @@ class SoftwareServiceForm(PluginDataForm):
super(SoftwareServiceForm, self).__init__(*args, **kwargs)
self.is_change = bool(self.instance and self.instance.pk)
if self.is_change:
site_name = self.instance.get_site_name()
site_domain = self.instance.get_site_domain()
self.fields['password1'].required = False
self.fields['password1'].widget = forms.HiddenInput()
self.fields['password2'].required = False
self.fields['password2'].widget = forms.HiddenInput()
else:
self.fields['password'].widget = forms.HiddenInput()
site_name = self.plugin.site_name
if site_name:
site_name_link = '<a href="http://%s">%s</a>' % (site_name, site_name)
self.fields['password1'].help_text = _("Suggestion: %s") % random_ascii(10)
site_domain = self.plugin.site_domain
if site_domain:
site_link = '<a href="http://%s">%s</a>' % (site_domain, site_domain)
else:
site_name_link = '&lt;name&gt;.%s' % self.plugin.site_name_base_domain
self.fields['site_name'].initial = site_name_link
## self.fields['site_name'].widget = widgets.ReadOnlyWidget(site_name, mark_safe(link))
## self.fields['site_name'].required = False
# else:
# base_name = self.plugin.site_name_base_domain
# help_text = _("The final URL would be &lt;site_name&gt;.%s") % base_name
# self.fields['site_name'].help_text = help_text
site_link = '&lt;site_name&gt;.%s' % self.plugin.site_base_domain
self.fields['site_url'].initial = site_link
self.fields['name'].label = _("Username")
def clean_password2(self):
if not self.is_change:
@ -59,11 +55,6 @@ class SoftwareServiceForm(PluginDataForm):
raise forms.ValidationError(msg)
return password2
def clean_site_name(self):
if self.plugin.site_name:
return None
return self.cleaned_data['site_name']
def save(self, commit=True):
obj = super(SoftwareServiceForm, self).save(commit=commit)
if not self.is_change:
@ -73,11 +64,10 @@ class SoftwareServiceForm(PluginDataForm):
class SoftwareService(plugins.Plugin):
form = SoftwareServiceForm
site_name = None
site_name_base_domain = 'orchestra.lan'
site_domain = None
site_base_domain = None
has_custom_domain = False
icon = 'orchestra/icons/apps.png'
change_readonly_fileds = ('site_name',)
class_verbose_name = _("Software as a Service")
plugin_field = 'service'
@ -89,14 +79,13 @@ class SoftwareService(plugins.Plugin):
plugins.append(import_class(cls))
return plugins
@classmethod
def get_change_readonly_fileds(cls):
fields = super(SoftwareService, cls).get_change_readonly_fileds()
return fields + ('username',)
return fields + ('name',)
def get_site_name(self):
return self.site_name or '.'.join(
(self.instance.username, self.site_name_base_domain)
def get_site_domain(self):
return self.site_domain or '.'.join(
(self.instance.name, self.site_base_domain)
)
def save(self):

View File

@ -17,23 +17,22 @@ class PHPListForm(SoftwareServiceForm):
def __init__(self, *args, **kwargs):
super(PHPListForm, self).__init__(*args, **kwargs)
self.fields['username'].label = _("Name")
base_domain = self.plugin.site_name_base_domain
help_text = _("Admin URL http://&lt;name&gt;.{}/admin/").format(base_domain)
self.fields['site_name'].help_text = help_text
self.fields['name'].label = _("Site name")
base_domain = self.plugin.site_base_domain
help_text = _("Admin URL http://&lt;site_name&gt;.{}/admin/").format(base_domain)
self.fields['site_url'].help_text = help_text
class PHPListChangeForm(PHPListForm):
# site_name = forms.CharField(widget=widgets.ShowTextWidget, required=False)
db_name = forms.CharField(label=_("Database name"),
help_text=_("Database used for this webapp."))
def __init__(self, *args, **kwargs):
super(PHPListChangeForm, self).__init__(*args, **kwargs)
site_name = self.instance.get_site_name()
admin_url = "http://%s/admin/" % site_name
site_domain = self.instance.get_site_domain()
admin_url = "http://%s/admin/" % site_domain
help_text = _("Admin URL <a href={0}>{0}</a>").format(admin_url)
self.fields['site_name'].help_text = help_text
self.fields['site_url'].help_text = help_text
class PHPListSerializer(serializers.Serializer):
@ -48,21 +47,25 @@ class PHPListService(SoftwareService):
change_readonly_fileds = ('db_name',)
serializer = PHPListSerializer
icon = 'orchestra/icons/apps/Phplist.png'
site_name_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
site_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
def get_db_name(self):
db_name = 'phplist_mu_%s' % self.instance.username
db_name = 'phplist_mu_%s' % self.instance.name
# Limit for mysql database names
return db_name[:65]
def get_db_user(self):
return settings.SAAS_PHPLIST_DB_NAME
def get_account(self):
return type(self.instance.account).get_main()
def validate(self):
super(PHPListService, self).validate()
create = not self.instance.pk
if create:
db = Database(name=self.get_db_name(), account=self.instance.account)
account = self.get_account()
db = Database(name=self.get_db_name(), account=account)
try:
db.full_clean()
except ValidationError as e:
@ -73,7 +76,8 @@ class PHPListService(SoftwareService):
def save(self):
db_name = self.get_db_name()
db_user = self.get_db_user()
db, db_created = Database.objects.get_or_create(name=db_name, account=self.instance.account)
account = self.get_account()
db, db_created = account.databases.get_or_create(name=db_name)
user = DatabaseUser.objects.get(username=db_user)
db.users.add(user)
self.instance.data = {
@ -90,9 +94,10 @@ class PHPListService(SoftwareService):
def get_related(self):
related = []
account = self.instance.account
account = self.get_account()
db_name = self.instance.data.get('db_name')
try:
db = account.databases.get(name=self.instance.data.get('db_name'))
db = account.databases.get(name=db_name)
except Database.DoesNotExist:
pass
else:

View File

@ -47,3 +47,16 @@ SAAS_BSCW_DOMAIN = getattr(settings, 'SAAS_BSCW_DOMAIN',
)
SAAS_BSCW_DEFAULT_QUOTA = getattr(settings, 'SAAS_BSCW_DEFAULT_QUOTA',
50
)
SAAS_GITLAB_ROOT_PASSWORD = getattr(settings, 'SAAS_GITLAB_ROOT_PASSWORD',
'secret'
)
SAAS_GITLAB_DOMAIN = getattr(settings, 'SAAS_GITLAB_DOMAIN',
'gitlab.orchestra.lan'
)

View File

@ -28,10 +28,11 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
self.create_webapp_dir(context)
self.set_under_construction(context)
self.append(textwrap.dedent("""\
fpm_config='%(fpm_config)s'
{
echo -e '%(fpm_config)s' | diff -N -I'^\s*;;' %(fpm_path)s -
echo -e "${fpm_config}" | diff -N -I'^\s*;;' %(fpm_path)s -
} || {
echo -e '%(fpm_config)s' > %(fpm_path)s
echo -e "${fpm_config}" > %(fpm_path)s
UPDATEDFPM=1
}""") % context
)
@ -41,20 +42,23 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
self.set_under_construction(context)
self.append("mkdir -p %(wrapper_dir)s" % context)
self.append(textwrap.dedent("""\
wrapper='%(wrapper)s'
{
echo -e '%(wrapper)s' | diff -N -I'^\s*#' %(wrapper_path)s -
echo -e "${wrapper}" | diff -N -I'^\s*#' %(wrapper_path)s -
} || {
echo -e '%(wrapper)s' > %(wrapper_path)s; UPDATED_APACHE=1
echo -e "${wrapper}" > %(wrapper_path)s; UPDATED_APACHE=1
}""") % context
)
self.append("chmod +x %(wrapper_path)s" % context)
self.append("chmod 550 %(wrapper_dir)s" % context)
self.append("chmod 550 %(wrapper_path)s" % context)
self.append("chown -R %(user)s:%(group)s %(wrapper_dir)s" % context)
if context['cmd_options']:
self.append(textwrap.dedent("""
cmd_options='%(cmd_options)s'
{
echo -e '%(cmd_options)s' | diff -N -I'^\s*#' %(cmd_options_path)s -
echo -e "${cmd_options}" | diff -N -I'^\s*#' %(cmd_options_path)s -
} || {
echo -e '%(cmd_options)s' > %(cmd_options_path)s; UPDATED_APACHE=1
echo -e "${cmd_options}" > %(cmd_options_path)s; UPDATED_APACHE=1
}""" ) % context
)
else:

View File

@ -16,6 +16,7 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH',
WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH',
# Inside SuExec Document root
'/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper')

View File

@ -89,5 +89,6 @@ class AppType(plugins.Plugin):
'app_id': self.instance.id,
'app_name': self.instance.name,
'user': self.instance.account.username,
'home': self.instance.account.main_systemuser.get_home(),
}

View File

@ -33,8 +33,8 @@ class WebalizerApp(AppType):
icon = 'orchestra/icons/apps/Stats.png'
option_groups = ()
def get_directive(self, webapp):
webalizer_path = os.path.join(webapp.get_path(), '%(site_name)s')
def get_directive(self):
webalizer_path = os.path.join(self.instance.get_path(), '%(site_name)s')
webalizer_path = os.path.normpath(webalizer_path)
return ('static', webalizer_path)

View File

@ -57,15 +57,6 @@ class PHPApp(AppType):
def get_detail(self):
return self.instance.data.get('php_version', '')
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, merge=False):
"""
process php options for inclusion on php.ini
@ -77,17 +68,17 @@ class PHPApp(AppType):
# Get options from the same account and php_version webapps
options = []
php_version = self.get_php_version()
webapps = self.instance.account.webapps.filter(webapp_type=self.instance.type)
webapps = self.instance.account.webapps.filter(type=self.instance.type)
for webapp in webapps:
if webapp.type_instance.get_php_version == php_version:
options += list(webapp.options.all())
php_options = [option.name for option in type(self).get_php_options()]
enabled_functions = set()
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(',')
elif opt.name == 'enabled_functions':
enabled_functions.union(set(opt.value.split(',')))
if enabled_functions:
disabled_functions = []
for function in self.PHP_DISABLED_FUNCTIONS:
@ -95,11 +86,18 @@ class PHPApp(AppType):
disabled_functions.append(function)
init_vars['dissabled_functions'] = ','.join(disabled_functions)
if self.PHP_ERROR_LOG_PATH and 'error_log' not in init_vars:
context = self.get_context()
context = self.get_directive_context()
error_log_path = os.path.normpath(self.PHP_ERROR_LOG_PATH % context)
init_vars['error_log'] = error_log_path
return init_vars
def get_directive_context(self):
context = super(PHPApp, self).get_directive_context()
context.update({
'php_version': self.get_php_version(),
})
return context
def get_directive(self):
context = self.get_directive_context()
if self.is_fpm:

View File

@ -12,12 +12,13 @@ from orchestra.forms.widgets import DynamicHelpTextSelect
from . import settings
from .directives import SiteDirective
from .forms import WebsiteAdminForm
from .forms import WebsiteAdminForm, WebsiteDirectiveInlineFormSet
from .models import Content, Website, WebsiteDirective
class WebsiteDirectiveInline(admin.TabularInline):
model = WebsiteDirective
formset = WebsiteDirectiveInlineFormSet
extra = 1
DIRECTIVES_HELP_TEXT = {

View File

@ -31,6 +31,7 @@ class Apache2Backend(ServiceController):
extra_conf += self.get_security(directives)
extra_conf += self.get_redirects(directives)
extra_conf += self.get_proxies(directives)
extra_conf += self.get_saas(directives)
# Order extra conf directives based on directives (longer first)
extra_conf = sorted(extra_conf, key=lambda a: len(a[0]), reverse=True)
context['extra_conf'] = '\n'.join([conf for location, conf in extra_conf])
@ -46,7 +47,7 @@ class Apache2Backend(ServiceController):
SuexecUserGroup {{ user }} {{ group }}\
{% for line in extra_conf.splitlines %}
{{ line | safe }}{% endfor %}
#IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f]
IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f]
</VirtualHost>
""")
).render(Context(context))
@ -80,10 +81,11 @@ class Apache2Backend(ServiceController):
apache_conf += self.render_redirect_https(context)
context['apache_conf'] = apache_conf
self.append(textwrap.dedent("""\
apache_conf='%(apache_conf)s'
{
echo -e '%(apache_conf)s' | diff -N -I'^\s*#' %(sites_available)s -
echo -e "${apache_conf}" | diff -N -I'^\s*#' %(sites_available)s -
} || {
echo -e '%(apache_conf)s' > %(sites_available)s
echo -e "${apache_conf}" > %(sites_available)s
UPDATED=1
}""") % context
)
@ -116,7 +118,7 @@ class Apache2Backend(ServiceController):
return directives
def get_static_directives(self, context, app_path):
context['app_path'] = app_path % context
context['app_path'] = os.path.normpath(app_path % context)
location = "%(location)s/" % context
directive = "Alias %(location)s/ %(app_path)s/" % context
return [(location, directive)]
@ -128,10 +130,10 @@ class Apache2Backend(ServiceController):
else:
# UNIX socket
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/'
if context['location'] != '/':
if context['location']:
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1'
context.update({
'app_path': app_path,
'app_path': os.path.normpath(app_path),
'socket': socket,
})
location = "%(location)s/" % context
@ -143,7 +145,7 @@ class Apache2Backend(ServiceController):
def get_fcgid_directives(self, context, app_path, wrapper_path):
context.update({
'app_path': app_path,
'app_path': os.path.normpath(app_path),
'wrapper_path': wrapper_path,
})
location = "%(location)s/" % context
@ -158,16 +160,20 @@ class Apache2Backend(ServiceController):
return [(location, directives)]
def get_ssl(self, directives):
config = ''
cert = directives.get('ssl_cert')
key = directives.get('ssl_key')
ca = directives.get('ssl_ca')
if not (cert and key):
cert = [settings.WEBSITES_DEFAULT_SSL_CERT]
key = [settings.WEBSITES_DEFAULT_SSL_KEY]
ca = [settings.WEBSITES_DEFAULT_SSL_CA]
if not (cert and key):
return []
config = 'SSLEngine on\n'
config += "SSLCertificateFile %s\n" % cert[0]
config += "SSLCertificateKeyFile %s\n" % key[0]
if ca:
config += "SSLCACertificateFile %s\n" % ca[0]
cert = directives.get('ssl_cert')
if cert:
config += "SSLCertificateFile %\n" % cert[0]
key = directives.get('ssl_key')
if key:
config += "SSLCertificateKeyFile %s\n" % key[0]
return [('', config)]
def get_security(self, directives):
@ -210,13 +216,14 @@ class Apache2Backend(ServiceController):
def get_saas(self, directives):
saas = []
for name, value in directives.iteritems():
for name, values in directives.iteritems():
if name.endswith('-saas'):
for value in values:
context = {
'location': normurlpath(value),
}
directive = settings.WEBSITES_SAAS_DIRECTIVES[name]
saas += self.get_directive(context, directive)
saas += self.get_directives(directive, context)
return saas
# def get_protections(self, site):
# protections = ''
@ -280,7 +287,8 @@ class Apache2Backend(ServiceController):
'site_unique_name': site.unique_name,
'user': self.get_username(site),
'group': self.get_groupname(site),
'sites_enabled': "%s.conf" % os.path.join(sites_enabled, site.unique_name),
# TODO remove '0-'
'sites_enabled': "%s.conf" % os.path.join(sites_enabled, '0-'+site.unique_name),
'sites_available': "%s.conf" % os.path.join(sites_available, site.unique_name),
'access_log': site.get_www_access_log_path(),
'error_log': site.get_www_error_log_path(),

View File

@ -18,7 +18,8 @@ class SiteDirective(Plugin):
SAAS = 'SaaS'
help_text = ""
unique = True
unique_name = False
unique_value = False
@classmethod
@cached
@ -67,6 +68,7 @@ class Redirect(SiteDirective):
help_text = _("<tt>&lt;website path&gt; &lt;destination URL&gt;</tt>")
regex = r'^[^ ]+\s[^ ]+$'
group = SiteDirective.HTTPD
unique_value = True
class Proxy(SiteDirective):
@ -75,6 +77,7 @@ class Proxy(SiteDirective):
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
unique_value = True
class ErrorDocument(SiteDirective):
@ -87,6 +90,7 @@ class ErrorDocument(SiteDirective):
"<tt>&nbsp;403 \"Sorry can't allow you access today\"</tt>")
regex = r'[45]0[0-9]\s.*'
group = SiteDirective.HTTPD
unique_value = True
class SSLCA(SiteDirective):
@ -95,6 +99,7 @@ class SSLCA(SiteDirective):
help_text = _("Filesystem path of the CA certificate file.")
regex = r'^[^ ]+$'
group = SiteDirective.SSL
unique_name = True
class SSLCert(SiteDirective):
@ -103,6 +108,7 @@ class SSLCert(SiteDirective):
help_text = _("Filesystem path of the certificate file.")
regex = r'^[^ ]+$'
group = SiteDirective.SSL
unique_name = True
class SSLKey(SiteDirective):
@ -111,6 +117,7 @@ class SSLKey(SiteDirective):
help_text = _("Filesystem path of the key file.")
regex = r'^[^ ]+$'
group = SiteDirective.SSL
unique_name = True
class SecRuleRemove(SiteDirective):
@ -123,34 +130,38 @@ class SecRuleRemove(SiteDirective):
class SecEngine(SiteDirective):
name = 'sec_engine'
verbose_name = _("Modsecurity engine")
help_text = _("URL location for disabling modsecurity engine.")
verbose_name = _("SecRuleEngine Off")
help_text = _("URL path with disabled modsecurity engine.")
regex = r'^/[^ ]*$'
group = SiteDirective.SEC
unique_value = True
class WordPressSaaS(SiteDirective):
name = 'wordpress-saas'
verbose_name = "WordPress"
help_text = _("URL location for mounting wordpress multisite.")
verbose_name = "WordPress SaaS"
help_text = _("URL path for mounting wordpress multisite.")
# fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN
group = SiteDirective.SAAS
regex = r'^/[^ ]*$'
unique_value = True
class DokuWikiSaaS(SiteDirective):
name = 'dokuwiki-saas'
verbose_name = "DokuWiki"
help_text = _("URL location for mounting wordpress multisite.")
verbose_name = "DokuWiki SaaS"
help_text = _("URL path for mounting wordpress multisite.")
# fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN
group = SiteDirective.SAAS
regex = r'^/[^ ]*$'
unique_value = True
class DrupalSaaS(SiteDirective):
name = 'drupal-saas'
verbose_name = "Drupdal"
help_text = _("URL location for mounting wordpress multisite.")
verbose_name = "Drupdal SaaS"
help_text = _("URL path for mounting wordpress multisite.")
# fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN
group = SiteDirective.SAAS
regex = r'^/[^ ]*$'
unique_value = True

View File

@ -19,3 +19,26 @@ class WebsiteAdminForm(forms.ModelForm):
self.add_error(None, e)
return self.cleaned_data
class WebsiteDirectiveInlineFormSet(forms.models.BaseInlineFormSet):
""" Validate uniqueness """
def clean(self):
values = {}
for form in self.forms:
name = form.cleaned_data.get('name', None)
if name is not None:
directive = form.instance.directive_class
if directive.unique_name and name in values:
form.add_error(None, ValidationError(
_("Only one %s can be defined.") % directive.get_verbose_name()
))
value = form.cleaned_data.get('value', None)
if value is not None:
if directive.unique_value and value in values.get(name, []):
form.add_error('value', ValidationError(
_("This value is already used by other %s.") % unicode(directive.get_verbose_name())
))
try:
values[name].append(value)
except KeyError:
values[name] = [value]

View File

@ -76,13 +76,21 @@ WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS
# '')
WEBAPPS_SAAS_DIRECTIVES = getattr(settings, 'WEBAPPS_SAAS_DIRECTIVES', {
'wordpress-saas': ('fpm', '/home/httpd/wordpress-mu/', '/opt/php/5.4/socks/wordpress-mu.sock'),
'drupal-saas': ('fpm', '/home/httpd/drupal-mu/', '/opt/php/5.4/socks/drupal-mu.sock'),
'dokuwiki-saas': ('fpm', '/home/httpd/moodle-mu/', '/opt/php/5.4/socks/moodle-mu.sock'),
# 'moodle-saas': ('fpm', '/home/httpd/moodle-mu/', '/opt/php/5.4/socks/moodle-mu.sock'),
WEBSITES_SAAS_DIRECTIVES = getattr(settings, 'WEBSITES_SAAS_DIRECTIVES', {
'wordpress-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock', '/home/httpd/wordpress-mu/'),
'drupal-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock','/home/httpd/drupal-mu/'),
'dokuwiki-saas': ('fpm', '/opt/php/5.4/socks/pangea.sock','/home/httpd/moodle-mu/'),
})
WEBSITES_DEFAULT_SSL_CERT = getattr(settings, 'WEBSITES_DEFAULT_SSL_CERT',
''
)
WEBSITES_DEFAULT_SSL_KEY = getattr(settings, 'WEBSITES_DEFAULT_SSL_KEY',
''
)
WEBSITES_DEFAULT_SSL_CA = getattr(settings, 'WEBSITES_DEFAULT_SSL_CA',
''
)

View File

@ -2,6 +2,8 @@ from django import forms
from django.contrib.auth import forms as auth_forms
from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.utils.python import random_ascii
from ..core.validators import validate_password
@ -20,6 +22,10 @@ class UserCreationForm(forms.ModelForm):
widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification."))
def __init__(self, *args, **kwargs):
super(UserCreationForm, self).__init__(*args, **kwargs)
self.fields['password1'].help_text = _("Suggestion: %s") % random_ascii(10)
def clean_password2(self):
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')

View File

@ -1,3 +1,5 @@
from django.core.exceptions import ValidationError
from orchestra.utils.functional import cached
@ -53,7 +55,7 @@ class Plugin(object):
@classmethod
def get_change_readonly_fileds(cls):
return (cls.plugin_field,) + cls.change_readonly_fileds
return cls.change_readonly_fileds
def clean_data(self):
""" model clean, uses cls.serizlier by default """

View File

@ -13,7 +13,7 @@ def import_class(cls):
def random_ascii(length):
return ''.join([random.choice(string.hexdigits) for i in range(0, length)]).lower()
return ''.join([random.SystemRandom().choice(string.hexdigits) for i in range(0, length)]).lower()
class OrderedSet(collections.MutableSet):