Added create link systemuser action
This commit is contained in:
parent
2b41593783
commit
acfa74d9ae
124
TODO.md
124
TODO.md
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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.")),
|
||||
])
|
||||
|
|
|
@ -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.")
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
› <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
{% if obj %}
|
||||
› <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
|
||||
› {{ action_name }}
|
||||
{% elif add %}
|
||||
› <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
|
||||
› {{ action_name }}
|
||||
{% else %}
|
||||
› {{ 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" %} {% 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 %}
|
||||
|
|
@ -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>
|
||||
› <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
{% if obj %}
|
||||
› <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
|
||||
› {{ action_name }}
|
||||
{% elif add %}
|
||||
› <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
|
||||
› {{ action_name }}
|
||||
{% else %}
|
||||
› {{ 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" %} {% 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" %} {% 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" %} {% 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" %} {% 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" %} {% endfor %}
|
||||
<p class="help">{{ form.permissions.help_text|safe }}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue