Added create link systemuser action

This commit is contained in:
Marc Aymerich 2016-02-09 12:17:42 +00:00
parent 2b41593783
commit acfa74d9ae
25 changed files with 457 additions and 154 deletions

124
TODO.md
View file

@ -455,8 +455,130 @@ mkhomedir_helper or create ssh homes with bash.rc and such
* setuppostgres use porject_name for db name and user instead of orchestra
* show all available choices (plugins) on admin settings value i.e. payment methods
# POSTFIX web traffic monitor '": uid=" from=<%(user)s>'
### Quick start
0. Install orchestra following any of these methods:
1. [PIP-only, Fast deployment setup (demo)](README.md#fast-deployment-setup)
2. [Docker container (development)](INSTALLDEV.md)
3. [Install on current system (production)](INSTALL.md)
1. Add the servers that you want to manage on `/admin/orchestration/servers` and copy orchestra's SSH pubkey to them
`orchestra@panel:~ ssh-copy-id root@server.address`
2. Now configure service by service (domains, databases, webapps, websites...):
1. Add the route through `/admin/orchestration/route/`
2. Check and configure related settings on `/admin/settings/setting/`
3. Configure related resources if needed `/resources/resource/`, like Account Disc limit and traffic.
3. Test that everything works by creating and deleting services
4. Do the same for the other services
3. Configure billing by adding services `/admin/services/service/add/` and plans `/admin/plans/plan/`
1. Once a service is created hit the *Update orders* button
### Architecture
Orchestration
Orders
### Creating new services
1. Think about if the service can fit into one of the existing models like: SaaS or WebApps, refere to the related documentation if that is the case.
2. Create a new django app using startapp management command. For ilustrational purposes we will create a crontab services that will allow orchestra to manage user-based crontabs.
`python3 manage.py startapp crontabs`
3. Add the new *crontabs* app to the `INSTALLED_APPS` in your project's `settings.py`
3. Create a `models.py` file with the data your service needs to keep in order to be managed by orchestra
```python
from django.db import models
class CrontabSchedule(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"))
minute = models.CharField(_("minute"), max_length=64, default='*')
hour = models.CharField(_("hour"), max_length=64, default='*')
day_of_week = models.CharField(_("day of week"), max_length=64, default='*')
day_of_month = models.CharField(_("day of month"), max_length=64, default='*')
month_of_year = models.CharField(_("month of year"), max_length=64, default='*')
class Meta:
ordering = ('month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute')
def __str__(self):
rfield = lambda f: f and str(f).replace(' ', '') or '*'
return "{0} {1} {2} {3} {4} (m/h/d/dM/MY)".format(
rfield(self.minute), rfield(self.hour), rfield(self.day_of_week),
rfield(self.day_of_month), rfield(self.month_of_year),
)
class Crontab(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"))
schedule = models.ForeignKey(CrontabSchedule, verbose_name=_("schedule"))
description = models.CharField(_("description"), max_length=256, blank=True)
command = models.TextField(_("content"))
def __str__(self):
return (self.description or self.command)[:32]
```
4. Create a `admin.py` to enable the admin interface
```python
from django.contrib import admin
from .models import CrontabSchedule, Crontab
class CrontabScheduleAdmin(admin.ModelAdmin):
pass
class CrontabAdmin(admin.ModelAdmin):
pass
admin.site.register(CrontabSchedule, CrontabScheduleAdmin)
admin.site.register(Crontab, CrontabAdmin)
5. Create a `api.py` to enable the REST API.
6. Create a `backends.py` fiel with the needed backends for service orchestration and monitoring
```python
import os
import textwrap
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController, replace
from orchestra.contrib.resources import ServiceMonitor
class UNIXCronBackend(ServiceController):
"""
Basic UNIX cron support.
"""
verbose_name = _("UNIX cron")
model = 'crons.CronTab'
def prepare(self):
super(UNIXCronBackend, self).prepare()
self.accounts = set()
def save(self, crontab):
self.accounts.add(crontab.account)
def delete(self, crontab):
self.accounts.add(crontab.account)
def commit(self):
for account in self.accounts:
crontab = None
self.append("echo '' > %(crontab_path)s" % context)
chown
for crontab in account.crontabs.all():
self.append("
# if crontab is None:
# self.append("rm -f %(crontab_path)s" % context)
```
7. Configure the routing

View file

@ -15,7 +15,8 @@ class Mailbox(models.Model):
CUSTOM = 'CUSTOM'
name = models.CharField(_("name"), max_length=64, unique=True,
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
help_text=_("Required. %s characters or fewer. Letters, digits and ./-/_ only.") %
settings.MAILBOXES_NAME_MAX_LENGTH,
validators=[
RegexValidator(r'^[\w.-]+$', _("Enter a valid mailbox name.")),
])

View file

@ -21,8 +21,8 @@ MAILBOXES_DOMAIN_MODEL = Setting('MAILBOXES_DOMAIN_MODEL', 'domains.Domain',
MAILBOXES_NAME_MAX_LENGTH = Setting('MAILBOXES_NAME_MAX_LENGTH',
64,
help_text=_("Limit for system user based mailbox on Linux should be 32.")
32,
help_text=_("Limit for system user based mailbox on Linux is 32.")
)

View file

@ -37,7 +37,7 @@ class MiscServiceAdmin(ExtendedModelAdmin):
""" return num slivers as a link to slivers changelist view """
num = misc.instances__count
url = reverse('admin:miscellaneous_miscellaneous_changelist')
url += '?service={}'.format(misc.pk)
url += '?service__name={}'.format(misc.name)
return mark_safe('<a href="{0}">{1}</a>'.format(url, num))
num_instances.short_description = _("Instances")
num_instances.admin_order_field = 'instances__count'

View file

@ -1,4 +1,6 @@
import importlib
import logging
import os
from dateutil import relativedelta
from functools import lru_cache
@ -8,7 +10,7 @@ from orchestra.utils.python import import_class
from .. import settings
class PaymentMethod(plugins.Plugin):
class PaymentMethod(plugins.Plugin, metaclass=plugins.PluginMount):
label_field = 'label'
number_field = 'number'
process_credit = False
@ -18,10 +20,16 @@ class PaymentMethod(plugins.Plugin):
@classmethod
@lru_cache()
def get_plugins(cls):
plugins = []
for cls in settings.PAYMENTS_ENABLED_METHODS:
plugins.append(import_class(cls))
def get_plugins(cls, all=False):
if all:
for module in os.listdir(os.path.dirname(__file__)):
if module not in ('options.py', '__init__.py') and module[-3:] == '.py':
importlib.import_module('.'+module[:-3], __package__)
plugins = super().get_plugins()
else:
plugins = []
for cls in settings.PAYMENTS_ENABLED_METHODS:
plugins.append(import_class(cls))
return plugins
def get_label(self):

View file

@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
try:
import lxml
except ImportError:
logger.error('Error loading lxml, module not install')
logger.error('Error loading lxml, module not installed.')
class SEPADirectDebitForm(PluginDataForm):

View file

@ -41,6 +41,6 @@ PAYMENTS_ENABLED_METHODS = Setting('PAYMENTS_ENABLED_METHODS',
'orchestra.contrib.payments.methods.creditcard.CreditCard',
),
# lazy loading
choices=lambda : ((m.get_class_path(), m.get_class_path()) for m in payments.methods.PaymentMethod.get_plugins()),
choices=lambda : ((m.get_class_path(), m.get_class_path()) for m in payments.methods.PaymentMethod.get_plugins(all=True)),
multiple=True,
)

View file

@ -1,7 +1,10 @@
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import insertattr
from orchestra.admin.utils import insertattr, admin_link
from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.contrib.services.models import Service
@ -15,7 +18,9 @@ class RateInline(admin.TabularInline):
class PlanAdmin(ExtendedModelAdmin):
list_display = ('name', 'is_default', 'is_combinable', 'allow_multiple', 'is_active')
list_display = (
'name', 'is_default', 'is_combinable', 'allow_multiple', 'is_active', 'num_contracts',
)
list_filter = ('is_default', 'is_combinable', 'allow_multiple', 'is_active')
fields = ('verbose_name', 'name', 'is_default', 'is_combinable', 'allow_multiple')
prepopulated_fields = {
@ -24,14 +29,29 @@ class PlanAdmin(ExtendedModelAdmin):
change_readonly_fields = ('name',)
inlines = [RateInline]
def num_contracts(self, plan):
num = plan.contracts__count
url = reverse('admin:plans_contractedplan_changelist')
url += '?plan__name={}'.format(plan.name)
return '<a href="{0}">{1}</a>'.format(url, num)
num_contracts.short_description = _("Contracts")
num_contracts.admin_order_field = 'contracts__count'
num_contracts.allow_tags = True
def get_queryset(self, request):
qs = super(PlanAdmin, self).get_queryset(request)
return qs.annotate(models.Count('contracts', distinct=True))
class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('plan', 'account_link')
list_display = ('id', 'plan_link', 'account_link')
list_filter = ('plan__name',)
list_select_related = ('plan', 'account')
search_fields = ('account__username', 'plan__name', 'id')
actions = (list_accounts,)
plan_link = admin_link('plan')
admin.site.register(Plan, PlanAdmin)
admin.site.register(ContractedPlan, ContractedPlanAdmin)

View file

@ -1,3 +1,5 @@
import importlib
import os
from functools import lru_cache
from urllib.parse import urlparse
@ -17,7 +19,7 @@ from .. import settings
from ..forms import SaaSPasswordForm
class SoftwareService(plugins.Plugin):
class SoftwareService(plugins.Plugin, metaclass=plugins.PluginMount):
PROTOCOL_MAP = {
'http': (Website.HTTP, (Website.HTTP, Website.HTTP_AND_HTTPS)),
'https': (Website.HTTPS_ONLY, (Website.HTTPS, Website.HTTP_AND_HTTPS, Website.HTTPS_ONLY)),
@ -35,10 +37,16 @@ class SoftwareService(plugins.Plugin):
@classmethod
@lru_cache()
def get_plugins(cls):
plugins = []
for cls in settings.SAAS_ENABLED_SERVICES:
plugins.append(import_class(cls))
def get_plugins(cls, all=False):
if all:
for module in os.listdir(os.path.dirname(__file__)):
if module not in ('options.py', '__init__.py') and module[-3:] == '.py':
importlib.import_module('.'+module[:-3], __package__)
plugins = super().get_plugins()
else:
plugins = []
for cls in settings.SAAS_ENABLED_SERVICES:
plugins.append(import_class(cls))
return plugins
def get_change_readonly_fileds(cls):
@ -143,6 +151,7 @@ class SoftwareService(plugins.Plugin):
class DBSoftwareService(SoftwareService):
db_name = None
db_user = None
abstract = True
def get_db_name(self):
context = {

View file

@ -21,7 +21,7 @@ SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES',
# 'orchestra.contrib.saas.services.seafile.SeaFileService',
),
# lazy loading
choices=lambda: ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins()),
choices=lambda: ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins(all=True)),
multiple=True,
)

View file

@ -66,7 +66,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
def num_orders(self, service):
num = service.orders__count
url = reverse('admin:orders_order_changelist')
url += '?service=%i&is_active=True' % service.pk
url += '?service__id__exact=%i&is_active=True' % service.pk
return '<a href="%s">%d</a>' % (url, num)
num_orders.short_description = _("Orders")
num_orders.admin_order_field = 'orders__count'

View file

@ -7,7 +7,7 @@ from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.contrib.orchestration import Operation, helpers
from .forms import PermissionForm
from .forms import PermissionForm, LinkForm
def get_verbose_choice(choices, value):
@ -21,11 +21,12 @@ def set_permission(modeladmin, request, queryset):
for user in queryset:
account_id = account_id or user.account_id
if user.account_id != account_id:
messages.error("Users from the same account should be selected.")
messages.error(request, "Users from the same account should be selected.")
return
user = queryset[0]
form = PermissionForm(user)
action_value = 'set_permission'
if request.POST.get('action') == action_value:
if request.POST.get('post') == 'generic_confirmation':
form = PermissionForm(user, request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
@ -35,7 +36,10 @@ def set_permission(modeladmin, request, queryset):
extension = cleaned_data['home_extension']
action = cleaned_data['set_action']
perms = cleaned_data['permissions']
user.set_permission(base_home, extension, action=action, perms=perms)
user.set_perm_action = action
user.set_perm_base_home = base_home
user.set_perm_home_extension = extension
user.set_perm_perms = perms
operations.extend(Operation.create_for_action(user, 'set_permission'))
verbose_action = get_verbose_choice(form.fields['set_action'].choices,
user.set_perm_action)
@ -48,8 +52,11 @@ def set_permission(modeladmin, request, queryset):
}
msg = _("%(action)s %(perms)s permission to %(to)s") % context
modeladmin.log_change(request, user, msg)
logs = Operation.execute(operations)
helpers.message_user(request, logs)
if not operations:
messages.error(request, "No backend operation has been executed.")
else:
logs = Operation.execute(operations)
helpers.message_user(request, logs)
return
opts = modeladmin.model._meta
app_label = opts.app_label
@ -70,6 +77,60 @@ set_permission.url_name = 'set-permission'
set_permission.tool_description = _("Set permission")
def create_link(modeladmin, request, queryset):
account_id = None
for user in queryset:
account_id = account_id or user.account_id
if user.account_id != account_id:
messages.error(request, "Users from the same account should be selected.")
return
user = queryset[0]
form = LinkForm(user)
action_value = 'create_link'
if request.POST.get('post') == 'generic_confirmation':
form = LinkForm(user, request.POST, queryset=queryset)
if form.is_valid():
cleaned_data = form.cleaned_data
operations = []
for user in queryset:
base_home = cleaned_data['base_home']
extension = cleaned_data['home_extension']
target = os.path.join(base_home, extension)
link_name = cleaned_data['link_name'] or os.path.join(user.home, os.path.basename(target))
user.create_link_target = target
user.create_link_name = link_name
operations.extend(Operation.create_for_action(user, 'create_link'))
context = {
'target': target,
'link_name': link_name,
}
msg = _("Created link from %(target)s to %(link_name)s") % context
modeladmin.log_change(request, request.user, msg)
logs = Operation.execute(operations)
if logs:
helpers.message_user(request, logs)
else:
messages.error(request, "No backend operation has been executed.")
return
opts = modeladmin.model._meta
app_label = opts.app_label
context = {
'title': _("Create link"),
'action_name': _("Create link"),
'action_value': action_value,
'queryset': queryset,
'opts': opts,
'obj': user,
'app_label': app_label,
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
'form': form,
}
return TemplateResponse(request, 'admin/systemusers/systemuser/create_link.html',
context, current_app=modeladmin.admin_site.name)
create_link.url_name = 'create-link'
create_link.tool_description = _("Create link")
def delete_selected(modeladmin, request, queryset):
""" wrapper arround admin.actions.delete_selected to prevent main system users deletion """
opts = modeladmin.model._meta

View file

@ -6,7 +6,7 @@ from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter
from .actions import set_permission, delete_selected
from .actions import set_permission, create_link, delete_selected
from .filters import IsMainListFilter
from .forms import SystemUserCreationForm, SystemUserChangeForm
from .models import SystemUser
@ -41,7 +41,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
add_form = SystemUserCreationForm
form = SystemUserChangeForm
ordering = ('-id',)
change_view_actions = (set_permission,)
change_view_actions = (set_permission, create_link)
actions = (delete_selected, list_accounts) + change_view_actions
def display_main(self, user):

View file

@ -17,7 +17,7 @@ class UNIXUserBackend(ServiceController):
"""
verbose_name = _("UNIX user")
model = 'systemusers.SystemUser'
actions = ('save', 'delete', 'set_permission', 'validate_path_exists')
actions = ('save', 'delete', 'set_permission', 'validate_path_exists', 'create_link')
doc_settings = (settings, (
'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
'SYSTEMUSERS_MOVE_ON_DELETE_PATH',
@ -189,6 +189,24 @@ class UNIXUserBackend(ServiceController):
else:
raise NotImplementedError()
def create_link(self, user):
context = self.get_context(user)
context.update({
'link_target': user.create_link_target,
'link_name': user.create_link_name,
})
self.append(textwrap.dedent("""\
# Create link
su %(user)s --shell /bin/bash << 'EOF' || exit_code=1
if [[ ! -e %(link_name)s ]]; then
ln -s %(link_target)s %(link_name)s
else
echo "%(link_name)s already exists, doing nothing." >&2
exit 1
fi
EOF""") % context
)
def validate_path_exists(self, user):
context = {
'path': user.path_to_validate,

View file

@ -9,7 +9,7 @@ from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings
from .models import SystemUser
from .validators import validate_home, validate_path_exists
from .validators import validate_home, validate_paths_exist
class SystemUserFormMixin(object):
@ -89,7 +89,56 @@ class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm):
pass
class PermissionForm(forms.Form):
class LinkForm(forms.Form):
base_home = forms.ChoiceField(label=_("Target path"), choices=(),
help_text=_("Target link will be under this directory."))
home_extension = forms.CharField(label=_("Home extension"), required=False, initial='',
widget=forms.TextInput(attrs={'size':'70'}), help_text=_("Relative to chosen home."))
link_name = forms.CharField(label=_("Link name"), required=False, initial='',
widget=forms.TextInput(attrs={'size':'70'}),
help_text=_("If left blank or relative path: link will be created in each user home."))
def __init__(self, *args, **kwargs):
self.instance = args[0]
self.queryset = kwargs.pop('queryset', [])
super_args = []
if len(args) > 1:
super_args.append(args[1])
super(LinkForm, self).__init__(*super_args, **kwargs)
related_users = type(self.instance).objects.filter(account=self.instance.account_id)
self.fields['base_home'].choices = (
(user.get_base_home(), user.get_base_home()) for user in related_users
)
def clean_home_extension(self):
home_extension = self.cleaned_data['home_extension']
return home_extension.lstrip('/')
def clean_link_name(self):
link_name = self.cleaned_data['link_name']
if link_name:
if link_name.startswith('/'):
if len(self.queryset) > 1:
raise ValidationError(_("Link name can not be a full path when multiple users."))
link_names = [os.path.dirname(link_name)]
else:
link_names = [os.path.join(user.home, os.path.dirname(link_names)) for user in self.queryset]
validate_paths_exist(self.instance, link_names)
return link_name
def clean(self):
cleaned_data = super(LinkForm, self).clean()
path = os.path.join(cleaned_data['base_home'], cleaned_data['home_extension'])
try:
validate_paths_exist(self.instance, [path])
except ValidationError as err:
raise ValidationError({
'home_extension': err,
})
return cleaned_data
class PermissionForm(LinkForm):
set_action = forms.ChoiceField(label=_("Action"), initial='grant',
choices=(
('grant', _("Grant")),
@ -105,29 +154,3 @@ class PermissionForm(forms.Form):
('r', _("Read only")),
('w', _("Write only"))
))
def __init__(self, *args, **kwargs):
self.instance = args[0]
super_args = []
if len(args) > 1:
super_args.append(args[1])
super(PermissionForm, self).__init__(*super_args, **kwargs)
related_users = type(self.instance).objects.filter(account=self.instance.account_id)
self.fields['base_home'].choices = (
(user.get_base_home(), user.get_base_home()) for user in related_users
)
def clean_home_extension(self):
home_extension = self.cleaned_data['home_extension']
return home_extension.lstrip('/')
def clean(self):
cleaned_data = super(PermissionForm, self).clean()
path = os.path.join(cleaned_data['base_home'], cleaned_data['home_extension'])
try:
validate_path_exists(self.instance, path)
except ValidationError as err:
raise ValidationError({
'home_extension': err,
})
return cleaned_data

View file

@ -123,12 +123,6 @@ class SystemUser(models.Model):
def set_password(self, raw_password):
self.password = make_password(raw_password)
def set_permission(self, base_home, extension, perms='rw', action='grant'):
self.set_perm_action = action
self.set_perm_base_home = base_home
self.set_perm_home_extension = extension
self.set_perm_perms = perms
def get_base_home(self):
context = {
'user': self.username,

View file

@ -0,0 +1,74 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if obj %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
&rsaquo; {{ action_name }}
{% elif add %}
&rsaquo; <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
&rsaquo; {{ action_name }}
{% else %}
&rsaquo; {{ action_name }} multiple objects
{% endif %}
</div>
{% endblock %}
{% block content %}
<div>
<div style="margin:20px;">
{% block introduction %}
Create link for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %}.
{% endblock %}
<ul>{{ display_objects | unordered_list }}</ul>
<form action="" method="post">{% csrf_token %}
<fieldset class="module aligned wide">
{{ form.non_field_errors }}
{% block prefields %}
{% endblock %}
<div class="form-row ">
<div class="field-box field-base_home">
{{ form.base_home.errors }}
<label for="{{ form.base_home.id_for_label }}"><b>{{ form.base_home.label }}</b>:</label>
{{ form.base_home }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.base_home.help_text|safe }}</p>
</div>
<div class="field-box field-user_extension">
{{ form.home_extension.errors }}
<label for="{{ form.home_extension.id_for_label }}"></label>
{{ form.home_extension }}
<p class="help">{{ form.home_extension.help_text|safe }}</p>
</div>
</div>
{% block postfields %}
<div class="form-row ">
{{ form.link_name.errors }}
<label for="{{ form.link_name.id_for_label }}">{{ form.link_name.label }}:</label>
{{ form.link_name }}
<p class="help">{{ form.link_name.help_text|safe }}</p>
</div>
{% endblock %}
</fieldset>
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="{{ action_value }}" />
<input type="hidden" name="post" value="{{ post_value|default:'generic_confirmation' }}" />
<input type="submit" value="{{ submit_value|default:_("Save") }}" />
</div>
</form>
{% endblock %}

View file

@ -1,74 +1,27 @@
{% extends "admin/base_site.html" %}
{% extends "admin/systemusers/systemuser/create_link.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
{% block introduction %}
Set permissions for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %} system user(s).
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if obj %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
&rsaquo; {{ action_name }}
{% elif add %}
&rsaquo; <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
&rsaquo; {{ action_name }}
{% else %}
&rsaquo; {{ action_name }} multiple objects
{% endif %}
{% block prefields %}
<div class="form-row ">
{{ form.set_action.errors }}
<label for="{{ form.set_action.id_for_label }}">{{ form.set_action.label }}:</label>
{{ form.set_action }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.set_action.help_text|safe }}</p>
</div>
{% endblock %}
{% block content %}
<div>
<div style="margin:20px;">
Set permissions for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %} system user(s).
<ul>{{ display_objects | unordered_list }}</ul>
<form action="" method="post">{% csrf_token %}
<fieldset class="module aligned wide">
{{ form.non_field_errors }}
<div class="form-row ">
{{ form.set_action.errors }}
<label for="{{ form.set_action.id_for_label }}">{{ form.set_action.label }}:</label>
{{ form.set_action }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.set_action.help_text|safe }}</p>
</div>
<div class="form-row ">
<div class="field-box field-base_home">
{{ form.base_home.errors }}
<label for="{{ form.base_home.id_for_label }}">{{ form.base_home.label }}:</label>
{{ form.base_home }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.base_home.help_text|safe }}</p>
</div>
<div class="field-box field-user_extension">
{{ form.home_extension.errors }}
<label for="{{ form.home_extension.id_for_label }}"></label>
{{ form.home_extension }}
<p class="help">{{ form.home_extension.help_text|safe }}</p>
</div>
</div>
<div class="form-row ">
{{ form.permissions.errors }}
<label for="{{ form.base_path.id_for_label }}">{{ form.permissions.label }}:</label>
{{ form.permissions }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.permissions.help_text|safe }}</p>
</div>
</fieldset>
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="{{ action_value }}" />
<input type="hidden" name="post" value="{{ post_value|default:'generic_confirmation' }}" />
<input type="submit" value="{{ submit_value|default:_("Save") }}" />
</div>
</form>
{% block postfields %}
<div class="form-row ">
{{ form.permissions.errors }}
<label for="{{ form.base_path.id_for_label }}">{{ form.permissions.label }}:</label>
{{ form.permissions }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.permissions.help_text|safe }}</p>
</div>
{% endblock %}

View file

@ -6,11 +6,15 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import Operation
def validate_path_exists(user, path):
user.path_to_validate = path
log = Operation.execute_action(user, 'validate_path_exists')[0]
if 'path does not exists' in log.stderr:
raise ValidationError(log.stderr)
def validate_paths_exist(user, paths):
operations = []
for path in paths:
user.path_to_validate = path
operations.extend(Operation.create_for_action(user, 'validate_path_exists'))
logs = Operation.execute(operations)
stderr = '\n'.join([log.stderr for log in logs])
if 'path does not exists' in stderr:
raise ValidationError(stderr)
def validate_home(user, data, account):

View file

@ -4,13 +4,13 @@ from functools import lru_cache
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.plugins import Plugin
from orchestra import plugins
from orchestra.utils.python import import_class
from . import settings
class AppOption(Plugin):
class AppOption(plugins.Plugin, metaclass=plugins.PluginMount):
PHP = 'PHP'
PROCESS = 'Process'
FILESYSTEM = 'FileSystem'
@ -21,10 +21,13 @@ class AppOption(Plugin):
@classmethod
@lru_cache()
def get_plugins(cls):
plugins = []
for cls in settings.WEBAPPS_ENABLED_OPTIONS:
plugins.append(import_class(cls))
def get_plugins(cls, all=False):
if all:
plugins = super().get_plugins()
else:
plugins = []
for cls in settings.WEBAPPS_ENABLED_OPTIONS:
plugins.append(import_class(cls))
return plugins
@classmethod
@ -52,6 +55,7 @@ class AppOption(Plugin):
class PHPAppOption(AppOption):
deprecated = None
group = AppOption.PHP
abstract = True
def validate(self):
super(PHPAppOption, self).validate()

View file

@ -82,7 +82,7 @@ WEBAPPS_TYPES = Setting('WEBAPPS_TYPES', (
'orchestra.contrib.webapps.types.python.PythonApp',
),
# lazy loading
choices=lambda : ((t.get_class_path(), t.get_class_path()) for t in webapps.types.AppType.get_plugins()),
choices=lambda : ((t.get_class_path(), t.get_class_path()) for t in webapps.types.AppType.get_plugins(all=True)),
multiple=True,
)
@ -255,7 +255,7 @@ WEBAPPS_ENABLED_OPTIONS = Setting('WEBAPPS_ENABLED_OPTIONS', (
'orchestra.contrib.webapps.options.PHPZendExtension',
),
# lazy loading
choices=lambda : ((o.get_class_path(), o.get_class_path()) for o in webapps.options.AppOption.get_plugins()),
choices=lambda : ((o.get_class_path(), o.get_class_path()) for o in webapps.options.AppOption.get_plugins(all=True)),
multiple=True,
)

View file

@ -1,3 +1,5 @@
import importlib
import os
from functools import lru_cache
from django.core.exceptions import ValidationError
@ -11,7 +13,7 @@ from .. import settings
from ..options import AppOption
class AppType(plugins.Plugin):
class AppType(plugins.Plugin, metaclass=plugins.PluginMount):
name = None
verbose_name = ""
help_text= ""
@ -24,10 +26,16 @@ class AppType(plugins.Plugin):
@classmethod
@lru_cache()
def get_plugins(cls):
plugins = []
for cls in settings.WEBAPPS_TYPES:
plugins.append(import_class(cls))
def get_plugins(cls, all=False):
if all:
for module in os.listdir(os.path.dirname(__file__)):
if module != '__init__.py' and module[-3:] == '.py':
importlib.import_module('.'+module[:-3], __package__)
plugins = super().get_plugins()
else:
plugins = []
for cls in settings.WEBAPPS_TYPES:
plugins.append(import_class(cls))
return plugins
def validate(self):

View file

@ -53,6 +53,7 @@ class CMSApp(PHPApp):
change_form = CMSAppForm
change_readonly_fileds = ('db_name', 'db_user', 'password',)
db_type = Database.MYSQL
abstract = True
def get_db_name(self):
db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account)

View file

@ -5,14 +5,14 @@ from functools import lru_cache
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.plugins import Plugin
from orchestra import plugins
from orchestra.utils.python import import_class
from . import settings
from .utils import normurlpath
class SiteDirective(Plugin):
class SiteDirective(plugins.Plugin, metaclass=plugins.PluginMount):
HTTPD = 'HTTPD'
SEC = 'ModSecurity'
SSL = 'SSL'
@ -25,10 +25,13 @@ class SiteDirective(Plugin):
@classmethod
@lru_cache()
def get_plugins(cls):
plugins = []
for cls in settings.WEBSITES_ENABLED_DIRECTIVES:
plugins.append(import_class(cls))
def get_plugins(cls, all=False):
if all:
plugins = super().get_plugins()
else:
plugins = []
for cls in settings.WEBSITES_ENABLED_DIRECTIVES:
plugins.append(import_class(cls))
return plugins
@classmethod

View file

@ -61,7 +61,7 @@ WEBSITES_ENABLED_DIRECTIVES = Setting('WEBSITES_ENABLED_DIRECTIVES',
'orchestra.contrib.websites.directives.MoodleSaaS',
),
# lazy loading
choices=lambda : ((d.get_class_path(), d.get_class_path()) for d in websites.directives.SiteDirective.get_plugins()),
choices=lambda : ((d.get_class_path(), d.get_class_path()) for d in websites.directives.SiteDirective.get_plugins(all=True)),
multiple=True,
)