diff --git a/TODO.md b/TODO.md
index 59614868..e52c80b1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -359,3 +359,10 @@ resorce monitoring more efficient, less mem an better queries for calc current d
ciphers=['arcfour128', 'aes256']
http://paramiko-docs.readthedocs.org/en/latest/api/transport.html
+
+
+
+* Grant and revoke permissions
+
+setfacl: /home/marcay//logs: Operation not permitted
+
diff --git a/orchestra/contrib/orchestration/methods.py b/orchestra/contrib/orchestration/methods.py
index 938db0e5..8ff5ed38 100644
--- a/orchestra/contrib/orchestration/methods.py
+++ b/orchestra/contrib/orchestration/methods.py
@@ -6,7 +6,6 @@ import socket
import sys
import select
-import paramiko
from celery.datastructures import ExceptionInfo
from django.conf import settings as djsettings
@@ -25,11 +24,12 @@ def Paramiko(backend, log, server, cmds, async=False):
"""
Executes cmds to remote server using Pramaiko
"""
+ import paramiko
script = '\n'.join(cmds)
script = script.replace('\r', '')
log.state = log.STARTED
log.script = script
- log.save(update_fields=('script', 'state'))
+ log.save(update_fields=('script', 'state', 'updated_at'))
if not cmds:
return
channel = None
@@ -48,7 +48,7 @@ def Paramiko(backend, log, server, cmds, async=False):
logger.error('%s timed out on %s' % (backend, addr))
log.state = log.TIMEOUT
log.stderr = str(e)
- log.save(update_fields=['state', 'stderr'])
+ log.save(update_fields=('state', 'stderr', 'updated_at'))
return
paramiko_connections[addr] = ssh
transport = ssh.get_transport()
@@ -73,7 +73,7 @@ def Paramiko(backend, log, server, cmds, async=False):
while part:
log.stderr += part
part = channel.recv_stderr(1024).decode('utf-8')
- log.save(update_fields=['stdout', 'stderr'])
+ log.save(update_fields=('stdout', 'stderr', 'updated_at'))
if channel.exit_status_ready():
if second:
break
@@ -95,7 +95,7 @@ def Paramiko(backend, log, server, cmds, async=False):
finally:
if log.state == log.STARTED:
log.state = log.ABORTED
- log.save(update_fields=['state'])
+ log.save(update_fields=('state', 'updated_at'))
if channel is not None:
channel.close()
@@ -108,25 +108,25 @@ def OpenSSH(backend, log, server, cmds, async=False):
script = script.replace('\r', '')
log.state = log.STARTED
log.script = script
- log.save(update_fields=('script', 'state'))
+ log.save(update_fields=('script', 'state', 'updated_at'))
if not cmds:
return
channel = None
ssh = None
try:
ssh = sshrun(server.get_address(), script, executable=backend.script_executable,
- persist=True, async=async)
+ persist=True, async=async, silent=True)
logger.debug('%s running on %s' % (backend, server))
if async:
second = False
for state in ssh:
log.stdout += state.stdout.decode('utf8')
log.stderr += state.stderr.decode('utf8')
- log.save()
+ log.save(update_fields=('stdout', 'stderr', 'updated_at'))
log.exit_code = state.exit_code
else:
- log.stdout = ssh.stdout
- log.stderr = ssh.stderr
+ log.stdout = ssh.stdout.decode('utf8')
+ log.stderr = ssh.stderr.decode('utf8')
log.exit_code = ssh.exit_code
log.state = log.SUCCESS if log.exit_code == 0 else log.FAILURE
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
@@ -140,7 +140,7 @@ def OpenSSH(backend, log, server, cmds, async=False):
finally:
if log.state == log.STARTED:
log.state = log.ABORTED
- log.save(update_fields=['state'])
+ log.save(update_fields=('state', 'updated_at'))
def SSH(*args, **kwargs):
@@ -150,12 +150,11 @@ def SSH(*args, **kwargs):
def Python(backend, log, server, cmds, async=False):
- # TODO collect stdout?
script = [ str(cmd.func.__name__) + str(cmd.args) for cmd in cmds ]
script = json.dumps(script, indent=4).replace('"', '')
log.state = log.STARTED
- log.script = '\n'.join([log.script, script])
- log.save(update_fields=('script', 'state'))
+ log.script = '\n'.join((log.script, script))
+ log.save(update_fields=('script', 'state', 'updated_at'))
try:
for cmd in cmds:
with CaptureStdout() as stdout:
@@ -163,7 +162,7 @@ def Python(backend, log, server, cmds, async=False):
for line in stdout:
log.stdout += line + '\n'
if async:
- log.save(update_fields=['stdout'])
+ log.save(update_fields=('stdout', 'updated_at'))
except:
log.exit_code = 1
log.state = log.FAILURE
diff --git a/orchestra/contrib/orchestration/settings.py b/orchestra/contrib/orchestration/settings.py
index 7539dc8b..b115ce48 100644
--- a/orchestra/contrib/orchestration/settings.py
+++ b/orchestra/contrib/orchestration/settings.py
@@ -43,9 +43,9 @@ ORCHESTRATION_BACKEND_CLEANUP_DAYS = Setting('ORCHESTRATION_BACKEND_CLEANUP_DAYS
ORCHESTRATION_SSH_METHOD_BACKEND = Setting('ORCHESTRATION_SSH_METHOD_BACKEND',
'orchestra.contrib.orchestration.methods.OpenSSH',
- help_text=_("Two methods provided:
"
- "orchestra.contrib.orchestration.methods.OpenSSH with ControlPersist.
"
- "orchestra.contrib.orchestration.methods.Paramiko with connection pool.
"
- "Both perform similarly, but OpenSSH has the advantage that the connections are shared between workers,
"
+ help_text=_("Two methods are provided:
"
+ "1) orchestra.contrib.orchestration.methods.OpenSSH with ControlPersist.
"
+ "2) orchestra.contrib.orchestration.methods.Paramiko with connection pool.
"
+ "Both perform similarly, but OpenSSH has the advantage that the connections are shared between workers. "
"Paramiko, in contrast, has a per worker connection pool.")
)
diff --git a/orchestra/contrib/plans/ratings.py b/orchestra/contrib/plans/ratings.py
index 70c9d80f..58bb8d4d 100644
--- a/orchestra/contrib/plans/ratings.py
+++ b/orchestra/contrib/plans/ratings.py
@@ -155,6 +155,29 @@ match_price.help_text = _("Only the rate with a) inmediate inferior metri
def best_price(rates, metric):
- pass
+ candidates = []
+ selected = False
+ prev = None
+ rates = _prepend_missing(rates.distinct())
+ for rate in rates:
+ if prev:
+ if prev.plan != rate.plan:
+ if not selected and prev.quantity <= metric:
+ candidates.append(prev)
+ selected = False
+ if not selected and rate.quantity > metric:
+ if prev.quantity <= metric:
+ candidates.append(prev)
+ selected = True
+ prev = rate
+ if not selected and prev.quantity <= metric:
+ candidates.append(prev)
+ candidates.sort(key=lambda r: r.price)
+ if candidates:
+ return [AttrDict(**{
+ 'quantity': metric,
+ 'price': candidates[0].price,
+ })]
+ return None
best_price.verbose_name = _("Best price")
best_price.help_text = _("Produces the best possible price given all active rating lines.")
diff --git a/orchestra/contrib/systemusers/actions.py b/orchestra/contrib/systemusers/actions.py
index 5bcfa37c..6363bb86 100644
--- a/orchestra/contrib/systemusers/actions.py
+++ b/orchestra/contrib/systemusers/actions.py
@@ -7,54 +7,67 @@ from django.template.response import TemplateResponse
from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation
-from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
+from orchestra.contrib.orchestration import Operation, helpers
-from .forms import GrantPermissionForm
+from .forms import PermissionForm
-def grant_permission(modeladmin, request, queryset):
+def get_verbose_choice(choices, value):
+ for choice, verbose in choices:
+ if choice == value:
+ return verbose
+
+
+def set_permission(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("Users from the same account should be selected.")
user = queryset[0]
+ form = PermissionForm(user)
if request.method == 'POST':
- form = GrantPermissionForm(user, request.POST)
+ form = PermissionForm(user, request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
- to = os.path.join(cleaned_data['base_path'], cleaned_data['path_extension'])
- ro = cleaned_data['read_only']
+ operations = []
for user in queryset:
- user.grant_to = to
- user.grant_ro = ro
- # DOn't collect, execute right away for path validation
- OperationsMiddleware.collect('grant_permission', instance=user)
+ user.set_perm_action = cleaned_data['set_action']
+ user.set_perm_base_home = cleaned_data['base_home']
+ user.set_perm_home_extension = cleaned_data['home_extension']
+ user.set_perm_perms = cleaned_data['permissions']
+ operations.extend(Operation.create_for_action(user, 'set_permission'))
+ verbose_action = get_verbose_choice(form.fields['set_action'].choices,
+ user.set_perm_action)
+ verbose_permissions = get_verbose_choice(form.fields['permissions'].choices,
+ user.set_perm_perms)
context = {
- 'type': _("read-only") if ro else _("read-write"),
- 'to': to,
+ 'action': verbose_action,
+ 'perms': verbose_permissions,
+ 'to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension),
}
- msg = _("Granted %(type)s permissions on %(to)s") % context
+ msg = _("%(action)s %(perms)s permission to %(to)s") % context
modeladmin.log_change(request, user, msg)
- # TODO feedback message
+ logs = Operation.execute(operations)
+ helpers.message_user(request, logs)
return
opts = modeladmin.model._meta
app_label = opts.app_label
context = {
- 'title': _("Grant permission"),
- 'action_name': _("Grant permission"),
- 'action_value': 'grant_permission',
+ 'title': _("Set permission"),
+ 'action_name': _("Set permission"),
+ 'action_value': 'set_permission',
'queryset': queryset,
'opts': opts,
'obj': user,
'app_label': app_label,
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
- 'form': GrantPermissionForm(user),
+ 'form': form,
}
- return TemplateResponse(request, 'admin/systemusers/systemuser/grant_permission.html',
+ return TemplateResponse(request, 'admin/systemusers/systemuser/set_permission.html',
context, current_app=modeladmin.admin_site.name)
-grant_permission.url_name = 'grant-permission'
-grant_permission.verbose_name = _("Grant permission")
+set_permission.url_name = 'set-permission'
+set_permission.verbose_name = _("Set permission")
def delete_selected(modeladmin, request, queryset):
diff --git a/orchestra/contrib/systemusers/admin.py b/orchestra/contrib/systemusers/admin.py
index 0c21ed8b..c0fa6cf4 100644
--- a/orchestra/contrib/systemusers/admin.py
+++ b/orchestra/contrib/systemusers/admin.py
@@ -6,7 +6,7 @@ from orchestra.admin.actions import disable
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter
-from .actions import grant_permission, delete_selected
+from .actions import set_permission, 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',)
- actions = (delete_selected, grant_permission, disable)
+ actions = (delete_selected, set_permission, disable)
change_view_actions = actions
def display_main(self, user):
diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py
index a831a418..e0366c75 100644
--- a/orchestra/contrib/systemusers/backends.py
+++ b/orchestra/contrib/systemusers/backends.py
@@ -15,7 +15,7 @@ class UNIXUserBackend(ServiceController):
"""
verbose_name = _("UNIX user")
model = 'systemusers.SystemUser'
- actions = ('save', 'delete', 'grant_permission')
+ actions = ('save', 'delete', 'set_permission', 'validate_path')
doc_settings = (settings,
('SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', 'SYSTEMUSERS_MOVE_ON_DELETE_PATH')
)
@@ -65,17 +65,69 @@ class UNIXUserBackend(ServiceController):
else:
self.append("rm -fr %(base_home)s" % context)
- def grant_permission(self, user):
- # TODO
+ def set_permission(self, user):
context = self.get_context(user)
context.update({
- 'to': user.grant_to,
- 'ro': user.grant_ro,
+ 'perm_action': user.set_perm_action,
+ 'perm_home': user.set_perm_base_home,
+ 'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension),
+ 'exclude': '',
})
- if user.grant_ro:
- self.append('echo "acl add read permissions for %(user)s to %(to)s"' % context)
+
+ exclude_acl = []
+ for exclude in settings.SYSTEMUSERS_EXLUDE_ACL_PATHS:
+ context['exclude'] = exclude
+ exclude_acl.append('-not -path "%(perm_home)s/%(exclude)s"' % context)
+ if exclude_acl:
+ context['exclude'] = ' \\\n -a '.join(exclude_acl)
+
+ if user.set_perm_perms == 'read-write':
+ context['perm_perms'] = 'rwx' if user.set_perm_action == 'grant' else '---'
+ elif user.set_perm_perms == 'read-only':
+ context['perm_perms'] = 'r-x' if user.set_perm_action == 'grant' else '-wx'
+ elif user.set_perm_perms == 'write-only':
+ context['perm_perms'] = '-wx' if user.set_perm_action == 'grant' else 'r-x'
+ if user.set_perm_action == 'grant':
+ self.append(textwrap.dedent("""\
+ # Home access
+ setfacl -m u:%(user)s:--x '%(perm_home)s'
+ # Grant perms to existing and future files
+ find '%(perm_to)s' %(exclude)s \\
+ -exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
+ find '%(perm_to)s' -type d %(exclude)s \\
+ -exec setfacl -m d:u:%(user)s:%(perm_perms)s {} \\;
+ # Account group as the owner of new files
+ chmod g+s '%(perm_to)s'
+ """) % context
+ )
+ if not user.is_main:
+ self.append(textwrap.dedent("""\
+ # Grant access to main user
+ find '%(perm_to)s' -type d %(exclude)s \\
+ -exec setfacl -m d:u:%(mainuser)s:rwx {} \\;
+ """) % context
+ )
+ elif user.set_perm_action == 'revoke':
+ self.append(textwrap.dedent("""\
+ # Revoke permissions
+ find '%(perm_to)s' %(exclude)s \\
+ -exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
+ """) % context
+ )
else:
- self.append('echo "acl add read-write permissions for %(user)s to %(to)s"' % context)
+ raise NotImplementedError()
+
+ def validate_path(self, user):
+ context = {
+ 'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension)
+ }
+ self.append(textwrap.dedent("""\
+ if [[ ! -e '%(perm_to)s' ]]; then
+ echo "%(perm_to)s path does not exists." >&2
+ exit 1
+ fi
+ """) % context
+ )
def get_groups(self, user):
if user.is_main:
diff --git a/orchestra/contrib/systemusers/forms.py b/orchestra/contrib/systemusers/forms.py
index a6682a86..825cbd74 100644
--- a/orchestra/contrib/systemusers/forms.py
+++ b/orchestra/contrib/systemusers/forms.py
@@ -1,8 +1,10 @@
import textwrap
from django import forms
+from django.core.exceptions import ValidationError
from django.utils.translation import ngettext, ugettext_lazy as _
+from orchestra.contrib.orchestration import Operation
from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings
@@ -79,21 +81,44 @@ class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm):
pass
-class GrantPermissionForm(forms.Form):
- base_path = forms.ChoiceField(label=_("Grant access to"), choices=(),
- help_text=_("User will be granted access to this directory."))
- path_extension = forms.CharField(label=_("Path extension"), required=False, initial='',
+class PermissionForm(forms.Form):
+ set_action = forms.ChoiceField(label=_("Action"), initial='grant',
+ choices=(
+ ('grant', _("Grant")),
+ ('revoke', _("Revoke"))
+ ))
+ base_home = forms.ChoiceField(label=_("Set permissions to"), choices=(),
+ help_text=_("User will be granted/revoked access to this directory."))
+ home_extension = forms.CharField(label=_("Home extension"), required=False, initial='',
widget=forms.TextInput(attrs={'size':'70'}), help_text=_("Relative to chosen home."))
- read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False,
- help_text=_("Designates whether the permissions granted will be read-only or read/write."))
+ permissions = forms.ChoiceField(label=_("Permissions"), initial='read-write',
+ choices=(
+ ('read-write', _("Read and write")),
+ ('read-only', _("Read only")),
+ ('write-only', _("Write only"))
+ ))
def __init__(self, *args, **kwargs):
- instance = args[0]
+ self.instance = args[0]
super_args = []
if len(args) > 1:
super_args.append(args[1])
- super(GrantPermissionForm, self).__init__(*super_args, **kwargs)
- related_users = type(instance).objects.filter(account=instance.account_id)
- self.fields['base_path'].choices = (
+ 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(self):
+ cleaned_data = super(PermissionForm, self).clean()
+ user = self.instance
+ user.set_perm_action = cleaned_data['set_action']
+ user.set_perm_base_home = cleaned_data['base_home']
+ user.set_perm_home_extension = cleaned_data['home_extension']
+ user.set_perm_perms = cleaned_data['permissions']
+ log = Operation.execute_action(user, 'validate_path')[0]
+ if 'path does not exists' in log.stderr:
+ raise ValidationError({
+ 'home_extension': log.stderr,
+ })
+ return cleaned_data
diff --git a/orchestra/contrib/systemusers/settings.py b/orchestra/contrib/systemusers/settings.py
index d2ac7c46..d435c18b 100644
--- a/orchestra/contrib/systemusers/settings.py
+++ b/orchestra/contrib/systemusers/settings.py
@@ -58,3 +58,10 @@ SYSTEMUSERS_MOVE_ON_DELETE_PATH = Setting('SYSTEMUSERS_MOVE_ON_DELETE_PATH',
help_text="Available fromat names: %s" % ', '.join(_backend_names),
validators=[Setting.string_format_validator(_backend_names)],
)
+
+
+SYSTEMUSERS_EXLUDE_ACL_PATHS = Setting('SYSTEMUSERS_EXLUDE_ACL_PATHS',
+ (),
+ help_text=("Relative to user's home.
"
+ "e.g. ('logs', 'logs/apache*', 'webapps')"),
+)
diff --git a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/grant_permission.html b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/grant_permission.html
deleted file mode 100644
index 3fa452c3..00000000
--- a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/grant_permission.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% load i18n l10n %}
-{% load url from future %}
-{% load admin_urls static utils %}
-
-{% block extrastyle %}
-{{ block.super }}
-
-
-{% endblock %}
-
-{% block breadcrumbs %}
-