From 39fd326ac2788d182db20a93d40656c47c2202da Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 18:26:52 +0100 Subject: [PATCH 01/25] allow generated password for new osusers - change osusers.admin.UserCreationForm to allow empty password input which triggers the creation of a new password --- docs/changelog.rst | 4 ++++ gnuviechadmin/osusers/admin.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ff8024b..149e45c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,10 @@ Changelog ========= +* :feature:`-` allow empty password input in + :py:class:`osusers.admin.UserCreationForm` to allow generated passwords for + new users + * :release:`0.3.0 <2014-12-27>` * :feature:`-` call create/delete mailbox tasks when saving/deleting mailboxes * :support:`-` use celery routers from gvacommon diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index a46dbc5..d2835e2 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -27,10 +27,14 @@ class UserCreationForm(forms.ModelForm): A form for creating system users. """ - password1 = forms.CharField(label=_('Password'), - widget=forms.PasswordInput) - password2 = forms.CharField(label=_('Password (again)'), - widget=forms.PasswordInput) + password1 = forms.CharField( + label=_('Password'), widget=forms.PasswordInput, + required=False, + ) + password2 = forms.CharField( + label=_('Password (again)'), widget=forms.PasswordInput, + required=False, + ) class Meta: model = User From 18e47d73b4f507a634b54331408628820e6036b3 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 19:26:16 +0100 Subject: [PATCH 02/25] add customer field to osusers.models.User - allow association of os users to Django users (customers) - adapt admin forms - add migration --- docs/changelog.rst | 1 + gnuviechadmin/osusers/admin.py | 5 +++-- .../osusers/migrations/0003_user_customer.py | 22 +++++++++++++++++++ gnuviechadmin/osusers/models.py | 7 ++++-- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 gnuviechadmin/osusers/migrations/0003_user_customer.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 149e45c..acc838a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* :feature:`-` add a `customer` field to :py:class:`osusers.models.User` * :feature:`-` allow empty password input in :py:class:`osusers.admin.UserCreationForm` to allow generated passwords for new users diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index d2835e2..1025038 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -38,7 +38,7 @@ class UserCreationForm(forms.ModelForm): class Meta: model = User - fields = [] + fields = ['customer'] def clean_password2(self): """ @@ -57,6 +57,7 @@ class UserCreationForm(forms.ModelForm): """ user = User.objects.create_user( + customer=self.cleaned_data['customer'], password=self.cleaned_data['password1'], commit=commit) return user @@ -75,7 +76,7 @@ class UserAdmin(admin.ModelAdmin): add_fieldsets = ( (None, { 'classes': ('wide',), - 'fields': ('password1', 'password2')}), + 'fields': ('customer', 'password1', 'password2')}), ) def get_form(self, request, obj=None, **kwargs): diff --git a/gnuviechadmin/osusers/migrations/0003_user_customer.py b/gnuviechadmin/osusers/migrations/0003_user_customer.py new file mode 100644 index 0000000..b8043ef --- /dev/null +++ b/gnuviechadmin/osusers/migrations/0003_user_customer.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('osusers', '0002_auto_20141226_1456'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='customer', + field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 93a4c3d..1b0d567 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -104,7 +104,9 @@ class UserManager(models.Manager): return nextuser @transaction.atomic - def create_user(self, username=None, password=None, commit=False): + def create_user( + self, customer, username=None, password=None, commit=False + ): uid = self.get_next_uid() gid = Group.objects.get_next_gid() if username is None: @@ -114,7 +116,7 @@ class UserManager(models.Manager): homedir = os.path.join(settings.OSUSER_HOME_BASEPATH, username) group = Group.objects.create(groupname=username, gid=gid) user = self.create(username=username, group=group, uid=uid, - homedir=homedir, + homedir=homedir, customer=customer, shell=settings.OSUSER_DEFAULT_SHELL) user.set_password(password) if commit: @@ -132,6 +134,7 @@ class User(TimeStampedModel, models.Model): gecos = models.CharField(_('Gecos field'), max_length=128, blank=True) homedir = models.CharField(_('Home directory'), max_length=256) shell = models.CharField(_('Login shell'), max_length=64) + customer = models.ForeignKey(settings.AUTH_USER_MODEL) objects = UserManager() From 0df67e7154879aa786ae7fc1c8aa34712287d638 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 22:44:27 +0100 Subject: [PATCH 03/25] document osusers code --- gnuviechadmin/osusers/__init__.py | 5 + gnuviechadmin/osusers/admin.py | 100 ++++++++++++++++- gnuviechadmin/osusers/apps.py | 17 +++ gnuviechadmin/osusers/models.py | 181 ++++++++++++++++++++++++++++-- gnuviechadmin/osusers/tasks.py | 154 +++++++++++++++++++++++-- 5 files changed, 430 insertions(+), 27 deletions(-) create mode 100644 gnuviechadmin/osusers/apps.py diff --git a/gnuviechadmin/osusers/__init__.py b/gnuviechadmin/osusers/__init__.py index e69de29..24380a5 100644 --- a/gnuviechadmin/osusers/__init__.py +++ b/gnuviechadmin/osusers/__init__.py @@ -0,0 +1,5 @@ +""" +This app is for managing operating system users and groups. + +""" +default_app_config = 'osusers.apps.OsusersAppConfig' diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index 1025038..3bb3579 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -1,3 +1,7 @@ +""" +This module contains the Django admin classes of the :py:mod:`osusers` app. + +""" from django import forms from django.utils.translation import ugettext as _ from django.contrib import admin @@ -10,13 +14,24 @@ from .models import ( ) PASSWORD_MISMATCH_ERROR = _("Passwords don't match") +""" +Error message for non matching passwords. +""" class AdditionalGroupInline(admin.TabularInline): + """ + Inline for :py:class:`osusers.models.AdditionalGroup` instances. + + """ model = AdditionalGroup class ShadowInline(admin.TabularInline): + """ + Inline for :py:class:`osusers.models.ShadowInline` instances. + + """ model = Shadow readonly_fields = ['passwd'] can_delete = False @@ -24,7 +39,8 @@ class ShadowInline(admin.TabularInline): class UserCreationForm(forms.ModelForm): """ - A form for creating system users. + A form for creating :py:class:`operating system users + `. """ password1 = forms.CharField( @@ -44,6 +60,9 @@ class UserCreationForm(forms.ModelForm): """ Check that the two password entries match. + :return: the validated password + :rtype: str or None + """ password1 = self.cleaned_data.get('password1') password2 = self.cleaned_data.get('password2') @@ -55,6 +74,10 @@ class UserCreationForm(forms.ModelForm): """ Save the provided password in hashed format. + :param boolean commit: whether to save the created user + :return: user instance + :rtype: :py:class:`osusers.models.User` + """ user = User.objects.create_user( customer=self.cleaned_data['customer'], @@ -65,10 +88,16 @@ class UserCreationForm(forms.ModelForm): """ No additional groups are created when this form is saved, so this method just does nothing. + """ class UserAdmin(admin.ModelAdmin): + """ + Admin class for working with :py:class:`operating system users + `. + + """ actions = ['perform_delete_selected'] add_form = UserCreationForm inlines = [AdditionalGroupInline, ShadowInline] @@ -83,6 +112,13 @@ class UserAdmin(admin.ModelAdmin): """ Use special form during user creation. + :param request: the current HTTP request + :param obj: either a :py:class:`User ` instance or + None for a new user + :param kwargs: keyword arguments to be passed to + :py:meth:`django.contrib.admin.ModelAdmin.get_form` + :return: form instance + """ defaults = {} if obj is None: @@ -94,16 +130,47 @@ class UserAdmin(admin.ModelAdmin): return super(UserAdmin, self).get_form(request, obj, **defaults) def get_readonly_fields(self, request, obj=None): + """ + Make sure that uid is not editable for existing users. + + :param request: the current HTTP request + :param obj: either a :py:class:`User ` instance or + None for a new user + :return: a list of fields + :rtype: list + + """ if obj: return ['uid'] return [] def perform_delete_selected(self, request, queryset): + """ + Action to delete a list of selected users. + + This action calls the delete method of each selected user in contrast + to the default `delete_selected`. + + :param request: the current HTTP request + :param queryset: Django ORM queryset representing the selected users + + """ for user in queryset.all(): user.delete() perform_delete_selected.short_description = _('Delete selected users') def get_actions(self, request): + """ + Get the available actions for users. + + This overrides the default behavior to remove the default + `delete_selected` action. + + :param request: the current HTTP request + :return: list of actions + :rtype: list + + """ actions = super(UserAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] @@ -111,19 +178,40 @@ class UserAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): + """ + Admin class for workint with :py:class:`operating system groups + `. + + """ actions = ['perform_delete_selected'] - def get_inline_instances(self, request, obj=None): - if obj is None: - return [] - return super(GroupAdmin, self).get_inline_instances(request, obj) - def perform_delete_selected(self, request, queryset): + """ + Action to delete a list of selected groups. + + This action calls the delete method of each selected group in contrast + to the default `delete_selected`. + + :param request: the current HTTP request + :param queryset: Django ORM queryset representing the selected groups + + """ for group in queryset.all(): group.delete() perform_delete_selected.short_description = _('Delete selected groups') def get_actions(self, request): + """ + Get the available actions for groups. + + This overrides the default behavior to remove the default + `delete_selected` action. + + :param request: the current HTTP request + :return: list of actions + :rtype: list + + """ actions = super(GroupAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] diff --git a/gnuviechadmin/osusers/apps.py b/gnuviechadmin/osusers/apps.py new file mode 100644 index 0000000..4ca9b7f --- /dev/null +++ b/gnuviechadmin/osusers/apps.py @@ -0,0 +1,17 @@ +""" +This module contains the :py:class:`django.apps.AppConfig` instance for the +:py:module:`osusers` app. + +""" +from __future__ import unicode_literals +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class OsusersAppConfig(AppConfig): + """ + AppConfig for the :py:mod:`osusers` app. + + """ + name = 'osusers' + verbose_name = _('Operating System Users and Groups') diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index 1b0d567..d5a65ef 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -1,3 +1,7 @@ +""" +This module defines the database models of operating system users. + +""" from __future__ import unicode_literals from datetime import date @@ -30,7 +34,7 @@ from .tasks import ( ) -logger = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( @@ -38,8 +42,19 @@ CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( class GroupManager(models.Manager): + """ + Manager class for :py:class:`osusers.models.Group`. + + """ def get_next_gid(self): + """ + Get the next available group id. + + :returns: group id + :rtype: int + + """ q = self.aggregate(models.Max('gid')) if q['gid__max'] is None: return settings.OSUSER_MINGID @@ -48,6 +63,10 @@ class GroupManager(models.Manager): @python_2_unicode_compatible class Group(TimeStampedModel, models.Model): + """ + This entity class corresponds to an operating system group. + + """ groupname = models.CharField( _('Group name'), max_length=16, unique=True) gid = models.PositiveSmallIntegerField( @@ -67,34 +86,73 @@ class Group(TimeStampedModel, models.Model): @transaction.atomic def save(self, *args, **kwargs): + """ + Save the group to the database and synchronizes group information to + LDAP. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.save` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.save` + :return: self + :rtype: :py:class:`osusers.models.Group` + + """ super(Group, self).save(*args, **kwargs) dn = create_ldap_group.delay( self.groupname, self.gid, self.descr).get() - logger.info("created LDAP group with dn %s", dn) + _LOGGER.info("created LDAP group with dn %s", dn) return self @transaction.atomic def delete(self, *args, **kwargs): + """ + Delete the group from LDAP and the database. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.delete` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.delete` + + """ delete_ldap_group_if_empty.delay(self.groupname).get() super(Group, self).delete(*args, **kwargs) class UserManager(models.Manager): + """ + Manager class for :py:class:`osusers.models.User`. + + """ def get_next_uid(self): + """ + Get the next available user id. + + :return: user id + :rtype: int + + """ q = self.aggregate(models.Max('uid')) if q['uid__max'] is None: return settings.OSUSER_MINUID return max(settings.OSUSER_MINUID, q['uid__max'] + 1) def get_next_username(self): + """ + Get the next available user name. + + :return: user name + :rtype: str + + """ count = 1 usernameformat = "{0}{1:02d}" nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, count) for user in self.values('username').filter( - username__startswith=settings.OSUSER_USERNAME_PREFIX).order_by( - 'username'): + username__startswith=settings.OSUSER_USERNAME_PREFIX + ).order_by('username'): if user['username'] == nextuser: count += 1 nextuser = usernameformat.format( @@ -107,6 +165,23 @@ class UserManager(models.Manager): def create_user( self, customer, username=None, password=None, commit=False ): + """ + Create a new user with a primary group named the same as the user and + an initial password. + + If username is None the result of :py:meth:`get_next_username` is used. + If password is None a new password will be generated using passlib's + :py:func:`generate_password`. + + :param customer: Django User instance this user is associated to + :param str username: the username or None + :param str password: the password or None + :param boolean commit: whether to commit the user data to the database + or not + :return: new user + :rtype: :py:class:`osusers.models.User` + + """ uid = self.get_next_uid() gid = Group.objects.get_next_gid() if username is None: @@ -126,6 +201,10 @@ class UserManager(models.Manager): @python_2_unicode_compatible class User(TimeStampedModel, models.Model): + """ + This entity class corresponds to an operating system user. + + """ username = models.CharField( _('User name'), max_length=64, unique=True) uid = models.PositiveSmallIntegerField( @@ -147,6 +226,15 @@ class User(TimeStampedModel, models.Model): @transaction.atomic def set_password(self, password): + """ + Set the password of the user. + + The password is set to the user's + :py:class:`Shadow ` instance and to LDAP. + + :param str password: the new password + + """ if hasattr(self, 'shadow'): self.shadow.set_password(password) else: @@ -161,12 +249,24 @@ class User(TimeStampedModel, models.Model): @transaction.atomic def save(self, *args, **kwargs): + """ + Save the user to the database, create user directories and synchronize + user information to LDAP. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.save` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.save` + :return: self + :rtype: :py:class:`osusers.models.User` + + """ dn = create_ldap_user.delay( self.username, self.uid, self.group.gid, self.gecos, self.homedir, self.shell, password=None).get() sftp_dir = setup_file_sftp_userdir.delay(self.username).get() mail_dir = setup_file_mail_userdir.delay(self.username).get() - logger.info( + _LOGGER.info( "created user %(user)s with LDAP dn %(dn)s, home directory " "%(homedir)s and mail base directory %(maildir)s.", { 'user': self, 'dn': dn, @@ -176,6 +276,16 @@ class User(TimeStampedModel, models.Model): @transaction.atomic def delete(self, *args, **kwargs): + """ + Delete the user and its groups from LDAP and the database and remove + the user's directories. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.delete` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.delete` + + """ delete_file_mail_userdir.delay(self.username).get() delete_file_sftp_userdir.delay(self.username).get() for group in [ag.group for ag in self.additionalgroup_set.all()]: @@ -187,9 +297,23 @@ class User(TimeStampedModel, models.Model): class ShadowManager(models.Manager): + """ + Manager class for :py:class:`osusers.models.Shadow`. + + """ @transaction.atomic def create_shadow(self, user, password): + """ + Create a new shadow instance with typical Linux settings for the given + user with the given password. + + :param user: :py:class:`User ` instance + :param str password: the password + :return: new Shadow instance + :rtype: :py:class:`osusers.models.Shadow` instance + + """ changedays = (timezone.now().date() - date(1970, 1, 1)).days shadow = self.create( user=user, changedays=changedays, @@ -203,6 +327,11 @@ class ShadowManager(models.Manager): @python_2_unicode_compatible class Shadow(TimeStampedModel, models.Model): + """ + This entity class corresponds to an operating system user's shadow file + entry. + + """ user = models.OneToOneField(User, primary_key=True, verbose_name=_('User')) passwd = models.CharField(_('Encrypted password'), max_length=128) changedays = models.PositiveSmallIntegerField( @@ -245,11 +374,21 @@ class Shadow(TimeStampedModel, models.Model): return 'for user {0}'.format(self.user) def set_password(self, password): + """ + Set and encrypt the password. + + :param str password: the password + """ self.passwd = sha512_crypt.encrypt(password) @python_2_unicode_compatible class AdditionalGroup(TimeStampedModel, models.Model): + """ + This entity class corresponds to additional group assignments for an + :py:class:`operating system user `. + + """ user = models.ForeignKey(User) group = models.ForeignKey(Group) @@ -258,21 +397,45 @@ class AdditionalGroup(TimeStampedModel, models.Model): verbose_name = _('Additional group') verbose_name_plural = _('Additional groups') + def __str__(self): + return '{0} in {1}'.format(self.user, self.group) + def clean(self): + """ + Ensure that the assigned group is different from the user's primary + group. + + """ if self.user.group == self.group: raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) @transaction.atomic def save(self, *args, **kwargs): + """ + Persists the group assignment to LDAP and the database. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.save` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.save` + :return: this instance + :rtype: :py:class:`AdditionalGroup ` + + """ add_ldap_user_to_group.delay( self.user.username, self.group.groupname).get() - super(AdditionalGroup, self).save(*args, **kwargs) + return super(AdditionalGroup, self).save(*args, **kwargs) @transaction.atomic def delete(self, *args, **kwargs): + """ + Delete the group assignment from LDAP and the database. + + :param args: positional arguments to be passed on to + :py:meth:`django.db.Model.delete` + :param kwargs: keyword arguments to be passed on to + :py:meth:`django.db.Model.delete` + """ remove_ldap_user_from_group.delay( self.user.username, self.group.groupname).get() super(AdditionalGroup, self).delete(*args, **kwargs) - - def __str__(self): - return '{0} in {1}'.format(self.user, self.group) diff --git a/gnuviechadmin/osusers/tasks.py b/gnuviechadmin/osusers/tasks.py index 99c7b1c..ce250ab 100644 --- a/gnuviechadmin/osusers/tasks.py +++ b/gnuviechadmin/osusers/tasks.py @@ -1,3 +1,10 @@ +""" +This module defines task stubs for the tasks implemented on the `Celery`_ +workers. + +.. _Celery: http://www.celeryproject.org/ + +""" from __future__ import absolute_import from celery import shared_task @@ -5,59 +12,182 @@ from celery import shared_task @shared_task def create_ldap_group(groupname, gid, descr): - pass + """ + This task creates an :py:class:`LDAP group ` + if it does not exist yet. + + If a group with the given name exists its group id and description + attributes are updated. + + :param str groupname: the group name + :param int gid: the group id + :param str descr: description text for the group + :return: the distinguished name of the group + :rtype: str + """ @shared_task def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): - pass + """ + This task creates an :py:class:`LDAP user ` + if it does not exist yet. + + The task is rejected if the primary group of the user is not defined. + + The user's fields are updated if the user already exists. + + :param str username: the user name + :param int uid: the user id + :param int gid: the user's primary group's id + :param str gecos: the text for the GECOS field + :param str homedir: the user's home directory + :param str shell: the user's login shell + :param str or None password: the clear text password, if :py:const:`None` + is passed the password is not touched + :raises celery.exceptions.Reject: if the specified primary group does not + exist + :return: the distinguished name of the user + :rtype: str + + """ @shared_task def add_ldap_user_to_group(username, groupname): - pass + """ + This task adds the specified user to the given group. + + This task does nothing if the user is already member of the group. + + :param str username: the user name + :param str groupname: the group name + :raises celery.exceptions.Retry: if the user does not exist yet, + :py:func:`create_ldap_user` should be called before + :return: True if the user has been added to the group otherwise False + :rtype: boolean + + """ @shared_task def remove_ldap_user_from_group(username, groupname): - pass + """ + This task removes the given user from the given group. + + :param str username: the user name + :param str groupname: the group name + :return: True if the user has been removed, False otherwise + :rtype: boolean + + """ @shared_task def delete_ldap_user(username): - pass + """ + This task deletes the given user. + + :param str username: the user name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ @shared_task def delete_ldap_group_if_empty(groupname): - pass + """ + This task deletes the given group. + + :param str groupname: the group name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ @shared_task def setup_file_sftp_userdir(username): - pass + """ + This task creates the home directory for an SFTP user if it does not exist + yet. + + :param str username: the user name + :raises Exception: if the SFTP directory of the user cannot be created + :return: the created directory name + :rtype: str + + """ @shared_task def delete_file_sftp_userdir(username): - pass + """ + This task recursively deletes the home directory of an SFTP user if it + does not exist yet. + + :param str username: the user name + :raises Exception: if the SFTP directory of the user cannot be removed + :return: the removed directory name + :rtype: str + + """ @shared_task def setup_file_mail_userdir(username): - pass + """ + This task creates the mail base directory for a user if it does not exist + yet. + + :param str username: the user name + :raises Exception: if the mail base directory for the user cannot be + created + :return: the created directory name + :rtype: str + + """ @shared_task def delete_file_mail_userdir(username): - pass + """ + This task recursively deletes the mail base directory for a user if it + does not exist yet. + + :param str username: the user name + :raises Exception: if the mail base directory of the user cannot be removed + :return: the removed directory name + :rtype: str + + """ @shared_task def create_file_mailbox(username, mailboxname): - pass + """ + This task creates a new mailbox directory for the given user and mailbox + name. + + :param str username: the user name + :param str mailboxname: the mailbox name + :raises GVAFileException: if the mailbox directory cannot be created + :return: the created mailbox directory name + :rtype: str + + """ @shared_task def delete_file_mailbox(username, mailboxname): - pass + """ + This task deletes the given mailbox of the given user. + + :param str username: the user name + :param str mailboxname: the mailbox name + :raises GVAFileException: if the mailbox directory cannot be deleted + :return: the deleted mailbox directory name + :rtype: str + + """ From 48d509c5db42477a20eefdf3327eaf783a21fc7d Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 22:44:45 +0100 Subject: [PATCH 04/25] add code documentation to docs --- docs/code.rst | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/conf.py | 11 +++- docs/index.rst | 1 + 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 docs/code.rst diff --git a/docs/code.rst b/docs/code.rst new file mode 100644 index 0000000..14c6e08 --- /dev/null +++ b/docs/code.rst @@ -0,0 +1,150 @@ +****************** +Code documentation +****************** + +.. index:: Django + +gva is implemented as `Django`_ project and provides a frontend for +administrators and customers. + +.. _Django: https://www.djangoproject.com/ + + +The project module :py:mod:`gnuviechadmin` +========================================== + +.. automodule:: gnuviechadmin + + +:py:mod:`celery ` +--------------------------------------- + +.. automodule:: gnuviechadmin.celery + :members: + + +:py:mod:`urls ` +----------------------------------- + +.. automodule:: gnuviechadmin.urls + + +:py:mod:`wsgi ` +----------------------------------- + +.. automodule:: gnuviechadmin.wsgi + :members: + + +:py:mod:`settings ` +------------------------------------------- + +.. automodule:: gnuviechadmin.settings + + +:py:mod:`base ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.base + :members: + + +:py:mod:`local ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.local + + +:py:mod:`production ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.production + + +:py:mod:`test ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: gnuviechadmin.settings.test + + +:py:mod:`gvacommon` +=================== + +This module is imported from a separate git project via git subtree and +provides some functionality that is common to all gnuviechadmin subprojects. + +.. automodule:: gvacommon + + +:py:mod:`celeryrouters ` +------------------------------------------------- + +.. automodule:: gvacommon.celeryrouters + :members: + :undoc-members: + + +:py:mod:`managemails` app +========================= + +.. automodule:: managemails + + +:py:mod:`admin ` +----------------------------------- + +.. automodule:: managemails.admin + :members: + + +:py:mod:`models ` +------------------------------------- + +.. automodule:: managemails.models + :members: + + +:py:mod:`osusers` app +===================== + +.. automodule:: osusers + + +:py:mod:`admin ` +------------------------------- + +.. automodule:: osusers.admin + :members: + + +:py:mod:`apps ` +----------------------------- + +.. automodule:: osusers.apps + :members: + + +:py:mod:`models ` +--------------------------------- + +.. automodule:: osusers.models + :members: + + +:py:mod:`tasks ` +------------------------------- + +.. automodule:: osusers.tasks + +.. autotask:: osusers.tasks.add_ldap_user_to_group +.. autotask:: osusers.tasks.create_file_mailbox +.. autotask:: osusers.tasks.create_ldap_group +.. autotask:: osusers.tasks.create_ldap_user +.. autotask:: osusers.tasks.delete_file_mail_userdir +.. autotask:: osusers.tasks.delete_file_mailbox +.. autotask:: osusers.tasks.delete_file_sftp_userdir +.. autotask:: osusers.tasks.delete_ldap_group_if_empty +.. autotask:: osusers.tasks.delete_ldap_user +.. autotask:: osusers.tasks.remove_ldap_user_from_group +.. autotask:: osusers.tasks.setup_file_mail_userdir +.. autotask:: osusers.tasks.setup_file_sftp_userdir diff --git a/docs/conf.py b/docs/conf.py index ffdf565..748e1c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,13 +13,18 @@ # All configuration values have a default; values that are commented out # serve to show the default. -#import sys -#import os +import sys +import os +import django # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join('..', 'gnuviechadmin'))) + +os.environ['GVA_SITE_ADMINMAIL'] = 'admin@gva.example.org' + +django.setup() # -- General configuration ----------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 0d9e0c2..3f1d47e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Contents: install deploy tests + code changelog From d4e62bf6f31f8628d2395d14ed27694675121445 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 27 Dec 2014 22:58:57 +0100 Subject: [PATCH 05/25] add german translation --- .gitignore | 1 + .../locale/de/LC_MESSAGES/django.po | 60 ++++++ .../osusers/locale/de/LC_MESSAGES/django.po | 175 ++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po create mode 100644 gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po diff --git a/.gitignore b/.gitignore index f25647b..ca72d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ Desktop.ini htmlcov/ tags _build/ +*.mo diff --git a/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..442c816 --- /dev/null +++ b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,60 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: managemails\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-12-27 22:45+0100\n" +"PO-Revision-Date: 2014-12-27 22:57+0100\n" +"Last-Translator: Jan Dittberner \n" +"Language-Team: de \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: admin.py:14 +msgid "Passwords don't match" +msgstr "Passwörter stimmen nicht überein" + +#: admin.py:21 tests/test_admin.py:37 +msgid "Hash" +msgstr "Hash-Code" + +#: admin.py:44 +msgid "Password" +msgstr "Passwort" + +#: admin.py:46 +msgid "Password (again)" +msgstr "Passwortwiederholung" + +#: admin.py:100 +msgid "Activate" +msgstr "Aktivieren" + +#: admin.py:101 +msgid "Deactivate" +msgstr "Deaktivieren" + +#: models.py:51 +msgid "Mailbox" +msgstr "Postfach" + +#: models.py:52 +msgid "Mailboxes" +msgstr "Postfächer" + +#: models.py:76 +msgid "Mail address" +msgstr "E-Mailadresse" + +#: models.py:77 +msgid "Mail addresses" +msgstr "E-Mailadressen" diff --git a/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..b6919a5 --- /dev/null +++ b/gnuviechadmin/osusers/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,175 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: osusers\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-12-27 22:46+0100\n" +"PO-Revision-Date: 2014-12-27 22:54+0100\n" +"Last-Translator: Jan Dittberner \n" +"Language-Team: de \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: admin.py:16 +msgid "Passwords don't match" +msgstr "Passwörter stimmen nicht überein" + +#: admin.py:47 +msgid "Password" +msgstr "Passwort" + +#: admin.py:51 +msgid "Password (again)" +msgstr "Passwortwiederholung" + +#: admin.py:160 +msgid "Delete selected users" +msgstr "Ausgewählte Nutzer löschen" + +#: admin.py:201 +msgid "Delete selected groups" +msgstr "Ausgewählte Gruppen löschen" + +#: apps.py:17 +msgid "Operating System Users and Groups" +msgstr "Betriebssystemnutzer- und Gruppen" + +#: models.py:41 +msgid "You can not use a user's primary group." +msgstr "Sie können nicht die primäre Gruppe des Nutzers verwenden." + +#: models.py:71 +msgid "Group name" +msgstr "Gruppenname" + +#: models.py:73 +msgid "Group ID" +msgstr "Gruppen-ID" + +#: models.py:74 +msgid "Description" +msgstr "Beschreibung" + +#: models.py:76 +msgid "Group password" +msgstr "Gruppenpasswort" + +#: models.py:81 models.py:212 +msgid "Group" +msgstr "Gruppe" + +#: models.py:82 +msgid "Groups" +msgstr "Gruppen" + +#: models.py:209 +msgid "User name" +msgstr "Nutzername" + +#: models.py:211 +msgid "User ID" +msgstr "Nutzer-ID" + +#: models.py:213 +msgid "Gecos field" +msgstr "GECOS-Feld" + +#: models.py:214 +msgid "Home directory" +msgstr "Home-Verzeichnis" + +#: models.py:215 +msgid "Login shell" +msgstr "Loginshell" + +#: models.py:221 models.py:335 +msgid "User" +msgstr "Nutzer" + +#: models.py:222 +msgid "Users" +msgstr "Nutzer" + +#: models.py:336 +msgid "Encrypted password" +msgstr "Verschlüsseltes Passwort" + +#: models.py:338 +msgid "Date of last change" +msgstr "Datum der letzten Änderung" + +#: models.py:339 +msgid "This is expressed in days since Jan 1, 1970" +msgstr "Ausgedrückt als Tage seit dem 1. Januar 1970" + +#: models.py:342 +msgid "Minimum age" +msgstr "Minimales Alter" + +#: models.py:343 +msgid "Minimum number of days before the password can be changed" +msgstr "Minmale Anzahl von Tagen bevor das Passwort geändert werden kann" + +#: models.py:347 +msgid "Maximum age" +msgstr "Maximales Alter" + +#: models.py:348 +msgid "Maximum number of days after which the password has to be changed" +msgstr "" +"Maximale Anzahl von Tagen, nach denen das Passwort geändert werden muss" + +#: models.py:352 +msgid "Grace period" +msgstr "Duldungsperiode" + +#: models.py:353 +msgid "The number of days before the password is going to expire" +msgstr "Anzahl von Tagen nach denen das Passwort verfällt" + +#: models.py:357 +msgid "Inactivity period" +msgstr "Inaktivitätsperiode" + +#: models.py:358 +msgid "" +"The number of days after the password has expired during which the password " +"should still be accepted" +msgstr "" +"Die Anzahl von Tagen für die ein verfallenes Passwort noch akzeptiert werden " +"soll" + +#: models.py:362 +msgid "Account expiration date" +msgstr "Kontoverfallsdatum" + +#: models.py:363 +msgid "" +"The date of expiration of the account, expressed as number of days since Jan " +"1, 1970" +msgstr "Kontoverfallsdatum in Tagen seit dem 1. Januar 1970" + +#: models.py:370 +msgid "Shadow password" +msgstr "Shadow-Passwort" + +#: models.py:371 +msgid "Shadow passwords" +msgstr "Shadow-Passwörter" + +#: models.py:397 +msgid "Additional group" +msgstr "Weitere Gruppe" + +#: models.py:398 +msgid "Additional groups" +msgstr "Weitere Gruppen" From f2b8af39a02ee9b7018427470abc1d4a040992f6 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 29 Dec 2014 12:57:02 +0100 Subject: [PATCH 06/25] define celery timezone, restrict celery content to json --- gnuviechadmin/gnuviechadmin/settings/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index f05a821..b770e8d 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -282,7 +282,9 @@ CELERY_ROUTES = ( 'gvacommon.celeryrouters.LdapRouter', 'gvacommon.celeryrouters.FileRouter', ) -CELERY_ACCEPT_CONTENT = ['pickle', 'yaml', 'json'] +CELERY_TIMEZONE = 'Europe/Berlin' +CELERY_ENABLE_UTC = True +CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' ########## END CELERY CONFIGURATION From 9b4bef0050a9cb4d7ede6d3c6313d6c4ec69488b Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 29 Dec 2014 15:22:52 +0100 Subject: [PATCH 07/25] add new task delete_ldap_group --- docs/changelog.rst | 2 ++ docs/code.rst | 1 + gnuviechadmin/osusers/tasks.py | 14 +++++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index acc838a..5ab9cd9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,8 @@ Changelog ========= +* :feature:`-` add new task :py:func:`osusers.tasks.delete_ldap_group` (needs + gvaldap >= 0.2.0 on the LDAP side) * :feature:`-` add a `customer` field to :py:class:`osusers.models.User` * :feature:`-` allow empty password input in :py:class:`osusers.admin.UserCreationForm` to allow generated passwords for diff --git a/docs/code.rst b/docs/code.rst index 14c6e08..db64b7f 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -143,6 +143,7 @@ provides some functionality that is common to all gnuviechadmin subprojects. .. autotask:: osusers.tasks.delete_file_mail_userdir .. autotask:: osusers.tasks.delete_file_mailbox .. autotask:: osusers.tasks.delete_file_sftp_userdir +.. autotask:: osusers.tasks.delete_ldap_group .. autotask:: osusers.tasks.delete_ldap_group_if_empty .. autotask:: osusers.tasks.delete_ldap_user .. autotask:: osusers.tasks.remove_ldap_user_from_group diff --git a/gnuviechadmin/osusers/tasks.py b/gnuviechadmin/osusers/tasks.py index ce250ab..a14265e 100644 --- a/gnuviechadmin/osusers/tasks.py +++ b/gnuviechadmin/osusers/tasks.py @@ -98,7 +98,19 @@ def delete_ldap_user(username): @shared_task def delete_ldap_group_if_empty(groupname): """ - This task deletes the given group. + This task deletes the given group if it is empty. + + :param str groupname: the group name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ + + +@shared_task +def delete_ldap_group(groupname): + """ + This taks deletes the given group. :param str groupname: the group name :return: True if the user has been deleted, False otherwise From a336af46c2b70ea997f4d0bdb9790ceb5a092e9f Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 29 Dec 2014 15:55:57 +0100 Subject: [PATCH 08/25] add taskresults app to handle celery task results --- docs/changelog.rst | 7 ++- docs/code.rst | 26 ++++++++++ gnuviechadmin/gnuviechadmin/settings/base.py | 1 + gnuviechadmin/taskresults/__init__.py | 5 ++ gnuviechadmin/taskresults/admin.py | 12 +++++ .../taskresults/management/__init__.py | 0 .../management/commands/__init__.py | 4 ++ .../management/commands/fetch_taskresults.py | 20 ++++++++ .../taskresults/migrations/0001_initial.py | 29 ++++++++++++ .../taskresults/migrations/__init__.py | 0 gnuviechadmin/taskresults/models.py | 47 +++++++++++++++++++ 11 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 gnuviechadmin/taskresults/__init__.py create mode 100644 gnuviechadmin/taskresults/admin.py create mode 100644 gnuviechadmin/taskresults/management/__init__.py create mode 100644 gnuviechadmin/taskresults/management/commands/__init__.py create mode 100644 gnuviechadmin/taskresults/management/commands/fetch_taskresults.py create mode 100644 gnuviechadmin/taskresults/migrations/0001_initial.py create mode 100644 gnuviechadmin/taskresults/migrations/__init__.py create mode 100644 gnuviechadmin/taskresults/models.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ab9cd9..67fc19f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,8 @@ Changelog ========= +* :feature:`-` add new app :py:mod:`taskresults` that takes care of handling + asynchronous `Celery`_ results * :feature:`-` add new task :py:func:`osusers.tasks.delete_ldap_group` (needs gvaldap >= 0.2.0 on the LDAP side) * :feature:`-` add a `customer` field to :py:class:`osusers.models.User` @@ -30,10 +32,11 @@ Changelog * :feature:`-` full test suite for osusers * :feature:`-` full test suite for managemails app * :feature:`-` full test suite for domains app -* :feature:`-` `Celery `_ integration for ldap - synchronization +* :feature:`-` `Celery`_ integration for ldap synchronization * :release:`0.1 <2014-05-25>` * :feature:`-` initial model code for os users * :feature:`-` initial model code for mail address and mailbox management * :feature:`-` initial model code for domains + +.. _Celery: http://www.celeryproject.org/ diff --git a/docs/code.rst b/docs/code.rst index db64b7f..eb4f62f 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -149,3 +149,29 @@ provides some functionality that is common to all gnuviechadmin subprojects. .. autotask:: osusers.tasks.remove_ldap_user_from_group .. autotask:: osusers.tasks.setup_file_mail_userdir .. autotask:: osusers.tasks.setup_file_sftp_userdir + + +:py:mod:`taskresults` app +========================= + +.. automodule:: taskresults + +:py:mod:`admin ` +----------------------------------- + +.. automodule:: taskresults.admin + +:py:mod:`management.commands ` +--------------------------------------------------------------- + +.. automodule:: taskresults.management.commands + +:py:mod:`fetch_taskresults ` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: taskresults.management.commands.fetch_taskresults + +:py:mod:`models ` +------------------------------------- + +.. automodule:: taskresults.models diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index b770e8d..e30df47 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -224,6 +224,7 @@ DJANGO_APPS = ( # Apps specific for this project go here. LOCAL_APPS = ( + 'taskresults', 'domains', 'osusers', 'managemails', diff --git a/gnuviechadmin/taskresults/__init__.py b/gnuviechadmin/taskresults/__init__.py new file mode 100644 index 0000000..0b3d916 --- /dev/null +++ b/gnuviechadmin/taskresults/__init__.py @@ -0,0 +1,5 @@ +""" +This is the taskresults app that is used for storing the results from +asynchronous `Celery `_ tasks. + +""" diff --git a/gnuviechadmin/taskresults/admin.py b/gnuviechadmin/taskresults/admin.py new file mode 100644 index 0000000..cd47dc8 --- /dev/null +++ b/gnuviechadmin/taskresults/admin.py @@ -0,0 +1,12 @@ +""" +This module defines the admin interface for the taskresults app. + +""" +from __future__ import absolute_import + +from django.contrib import admin + +from .models import TaskResult + + +admin.site.register(TaskResult) diff --git a/gnuviechadmin/taskresults/management/__init__.py b/gnuviechadmin/taskresults/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/taskresults/management/commands/__init__.py b/gnuviechadmin/taskresults/management/commands/__init__.py new file mode 100644 index 0000000..4e6e692 --- /dev/null +++ b/gnuviechadmin/taskresults/management/commands/__init__.py @@ -0,0 +1,4 @@ +""" +This module defines management commands for the taskresults app. + +""" diff --git a/gnuviechadmin/taskresults/management/commands/fetch_taskresults.py b/gnuviechadmin/taskresults/management/commands/fetch_taskresults.py new file mode 100644 index 0000000..8ccbd78 --- /dev/null +++ b/gnuviechadmin/taskresults/management/commands/fetch_taskresults.py @@ -0,0 +1,20 @@ +""" +This model contains the implementation of a management command to fetch the +results of all `Celery `_ tasks that are not +marked as finished yet. + +""" +from __future__ import unicode_literals + +from django.core.management.base import BaseCommand + +from taskresults.models import TaskResult + + +class Command(BaseCommand): + help = "fetch task results" + + def handle(self, *args, **options): + for taskresult in TaskResult.objects.filter(finished=False): + taskresult.fetch_result() + taskresult.save() diff --git a/gnuviechadmin/taskresults/migrations/0001_initial.py b/gnuviechadmin/taskresults/migrations/0001_initial.py new file mode 100644 index 0000000..7c405be --- /dev/null +++ b/gnuviechadmin/taskresults/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='TaskResult', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('task_id', models.CharField(max_length=36, verbose_name='Task id')), + ('task_name', models.CharField(max_length=64, verbose_name='Task name')), + ('result', models.TextField(verbose_name='Task result')), + ('finished', models.BooleanField(default=False)), + ('state', models.CharField(max_length=16, verbose_name='Task state')), + ], + options={ + 'verbose_name': 'Task result', + 'verbose_name_plural': 'Task results', + }, + bases=(models.Model,), + ), + ] diff --git a/gnuviechadmin/taskresults/migrations/__init__.py b/gnuviechadmin/taskresults/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/taskresults/models.py b/gnuviechadmin/taskresults/models.py new file mode 100644 index 0000000..7178c84 --- /dev/null +++ b/gnuviechadmin/taskresults/models.py @@ -0,0 +1,47 @@ +""" +This model defines the database models to handle Celery AsyncResults. + +""" +from __future__ import unicode_literals + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext as _ + +from gnuviechadmin.celery import app + + +class TaskResultManager(models.Manager): + def create_task_result(self, asyncresult, name): + taskresult = self.create(task_id=asyncresult.id, task_name=name) + return taskresult + + +@python_2_unicode_compatible +class TaskResult(models.Model): + task_id = models.CharField(_('Task id'), max_length=36) + task_name = models.CharField(_('Task name'), max_length=64) + result = models.TextField(_('Task result')) + finished = models.BooleanField(default=False) + state = models.CharField(_('Task state'), max_length=16) + + objects = TaskResultManager() + + class Meta: + verbose_name = _('Task result') + verbose_name_plural = _('Task results') + + def __str__(self): + return "{task_name} ({task_id}): {finished}".format( + task_name=self.task_name, + task_id=self.task_id, + finished=_('yes') if self.finished else _('no') + ) + + def fetch_result(self): + if not self.finished: + ar = app.AsyncResult(self.task_id) + res = ar.get(no_ack=True, timeout=1) + self.result = str(res) + self.state = ar.state + self.finished = True From 6bf65bba3b572dc348e1f90e29ee10ed536d7dcd Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 29 Dec 2014 15:57:03 +0100 Subject: [PATCH 09/25] use taskresults app and delete_ldap_group task --- gnuviechadmin/osusers/apps.py | 2 +- gnuviechadmin/osusers/models.py | 41 ++++++++++++++++++++++++--------- gnuviechadmin/osusers/tasks.py | 4 +--- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/gnuviechadmin/osusers/apps.py b/gnuviechadmin/osusers/apps.py index 4ca9b7f..26f373d 100644 --- a/gnuviechadmin/osusers/apps.py +++ b/gnuviechadmin/osusers/apps.py @@ -1,6 +1,6 @@ """ This module contains the :py:class:`django.apps.AppConfig` instance for the -:py:module:`osusers` app. +:py:mod:`osusers` app. """ from __future__ import unicode_literals diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py index d5a65ef..8b5edfe 100644 --- a/gnuviechadmin/osusers/models.py +++ b/gnuviechadmin/osusers/models.py @@ -20,13 +20,15 @@ from model_utils.models import TimeStampedModel from passlib.hash import sha512_crypt from passlib.utils import generate_password +from taskresults.models import TaskResult + from .tasks import ( add_ldap_user_to_group, create_ldap_group, create_ldap_user, delete_file_mail_userdir, delete_file_sftp_userdir, - delete_ldap_group_if_empty, + delete_ldap_group, delete_ldap_user, remove_ldap_user_from_group, setup_file_mail_userdir, @@ -115,7 +117,10 @@ class Group(TimeStampedModel, models.Model): :py:meth:`django.db.Model.delete` """ - delete_ldap_group_if_empty.delay(self.groupname).get() + TaskResult.objects.create_task_result( + delete_ldap_group.delay(self.groupname), + 'delete_ldap_group' + ) super(Group, self).delete(*args, **kwargs) @@ -264,13 +269,18 @@ class User(TimeStampedModel, models.Model): dn = create_ldap_user.delay( self.username, self.uid, self.group.gid, self.gecos, self.homedir, self.shell, password=None).get() - sftp_dir = setup_file_sftp_userdir.delay(self.username).get() - mail_dir = setup_file_mail_userdir.delay(self.username).get() + TaskResult.objects.create_task_result( + setup_file_sftp_userdir.delay(self.username), + 'setup_file_sftp_userdir' + ) + TaskResult.objects.create_task_result( + setup_file_mail_userdir.delay(self.username), + 'setup_file_mail_userdir' + ) _LOGGER.info( - "created user %(user)s with LDAP dn %(dn)s, home directory " - "%(homedir)s and mail base directory %(maildir)s.", { + "created user %(user)s with LDAP dn %(dn)s, scheduled home " + "directory and mail base directory creation.", { 'user': self, 'dn': dn, - 'homedir': sftp_dir, 'maildir': mail_dir }) return super(User, self).save(*args, **kwargs) @@ -286,8 +296,14 @@ class User(TimeStampedModel, models.Model): :py:meth:`django.db.Model.delete` """ - delete_file_mail_userdir.delay(self.username).get() - delete_file_sftp_userdir.delay(self.username).get() + TaskResult.objects.create_task_result( + delete_file_mail_userdir.delay(self.username), + 'delete_file_mail_userdir' + ) + TaskResult.objects.create_task_result( + delete_file_sftp_userdir.delay(self.username), + 'delete_file_sftp_userdir' + ) for group in [ag.group for ag in self.additionalgroup_set.all()]: remove_ldap_user_from_group.delay( self.username, group.groupname).get() @@ -436,6 +452,9 @@ class AdditionalGroup(TimeStampedModel, models.Model): :param kwargs: keyword arguments to be passed on to :py:meth:`django.db.Model.delete` """ - remove_ldap_user_from_group.delay( - self.user.username, self.group.groupname).get() + TaskResult.objects.create_task_result( + remove_ldap_user_from_group.delay( + self.user.username, self.group.groupname), + 'remove_ldap_user_from_group' + ) super(AdditionalGroup, self).delete(*args, **kwargs) diff --git a/gnuviechadmin/osusers/tasks.py b/gnuviechadmin/osusers/tasks.py index a14265e..23cc612 100644 --- a/gnuviechadmin/osusers/tasks.py +++ b/gnuviechadmin/osusers/tasks.py @@ -1,9 +1,7 @@ """ -This module defines task stubs for the tasks implemented on the `Celery`_ +This module defines task stubs for the tasks implemented on the Celery workers. -.. _Celery: http://www.celeryproject.org/ - """ from __future__ import absolute_import From 546441d49920caf43eb7d33976c2d71ae85f16d5 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Thu, 1 Jan 2015 22:32:37 +0100 Subject: [PATCH 10/25] unify routers, add support for mysql and pgsql tasks - add new celeryrouters.GvaRouter - remove LdapRouter and FileRouter --- .gitignore | 1 + celeryrouters.py | 23 +++++++---------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 3bb2efd..5f1ace6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .*.swp *.pyc +.ropeproject/ diff --git a/celeryrouters.py b/celeryrouters.py index e468813..ec7b122 100644 --- a/celeryrouters.py +++ b/celeryrouters.py @@ -2,23 +2,14 @@ from __future__ import unicode_literals -class LdapRouter(object): +class GvaRouter(object): def route_for_task(self, task, args=None, kwargs=None): - if 'ldap' in task: - return {'exchange': 'ldap', + for route in ['ldap', 'file', 'mysql', 'pgsql']: + if route in task: + return { + 'exchange': route, 'exchange_type': 'direct', - 'queue': 'ldap'} + 'queue': route, + } return None - - -class FileRouter(object): - - def route_for_task(self, task, args=None, kwargs=None): - if 'file' in task: - return {'exchange': 'file', - 'exchange_type': 'direct', - 'queue': 'file'} - return None - - From 60301eea468ba835a6e0d658d333b487d2a3804e Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Thu, 1 Jan 2015 22:35:55 +0100 Subject: [PATCH 11/25] switch to gvacommon.celeryrouters.GvaRouter --- gnuviechadmin/gnuviechadmin/settings/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index e30df47..05c6f0f 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -280,8 +280,7 @@ CELERY_RESULT_BACKEND = 'amqp' CELERY_RESULT_PERSISTENT = True CELERY_TASK_RESULT_EXPIRES = None CELERY_ROUTES = ( - 'gvacommon.celeryrouters.LdapRouter', - 'gvacommon.celeryrouters.FileRouter', + 'gvacommon.celeryrouters.GvaRouter', ) CELERY_TIMEZONE = 'Europe/Berlin' CELERY_ENABLE_UTC = True From 0a17528c0cc1897ccc355258c41e91229f6f6a8e Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 17:54:59 +0100 Subject: [PATCH 12/25] add migration for verbose_name and verbose_name_plural in osusers.models.User --- .../migrations/0004_auto_20150104_1751.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py diff --git a/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py b/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py new file mode 100644 index 0000000..b1dcd60 --- /dev/null +++ b/gnuviechadmin/osusers/migrations/0004_auto_20150104_1751.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osusers', '0003_user_customer'), + ] + + operations = [ + migrations.AlterModelOptions( + name='user', + options={'verbose_name': 'User', 'verbose_name_plural': 'Users'}, + ), + migrations.AlterField( + model_name='shadow', + name='user', + field=models.OneToOneField(primary_key=True, serialize=False, to='osusers.User', verbose_name='User'), + preserve_default=True, + ), + ] From ab50907b9713545749091f4369f4d24672916c29 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 17:56:38 +0100 Subject: [PATCH 13/25] set default locale to en-us to avoid translated migrations --- gnuviechadmin/gnuviechadmin/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index 05c6f0f..8715186 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -82,7 +82,7 @@ DATABASES = { TIME_ZONE = 'Europe/Berlin' # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code -LANGUAGE_CODE = 'de-de' +LANGUAGE_CODE = 'en-us' # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id SITE_ID = 1 From e7ae0054b5e83eb90d13e99ede1e5cdd77bc0a88 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 18:06:44 +0100 Subject: [PATCH 14/25] add mysqltasks and pgsqltasks with placeholders for the real tasks --- gnuviechadmin/gnuviechadmin/settings/base.py | 2 + gnuviechadmin/mysqltasks/__init__.py | 4 ++ gnuviechadmin/mysqltasks/models.py | 4 ++ gnuviechadmin/mysqltasks/tasks.py | 72 ++++++++++++++++++++ gnuviechadmin/pgsqltasks/__init__.py | 4 ++ gnuviechadmin/pgsqltasks/models.py | 4 ++ gnuviechadmin/pgsqltasks/tasks.py | 72 ++++++++++++++++++++ 7 files changed, 162 insertions(+) create mode 100644 gnuviechadmin/mysqltasks/__init__.py create mode 100644 gnuviechadmin/mysqltasks/models.py create mode 100644 gnuviechadmin/mysqltasks/tasks.py create mode 100644 gnuviechadmin/pgsqltasks/__init__.py create mode 100644 gnuviechadmin/pgsqltasks/models.py create mode 100644 gnuviechadmin/pgsqltasks/tasks.py diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index 8715186..cfd72f7 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -225,6 +225,8 @@ DJANGO_APPS = ( # Apps specific for this project go here. LOCAL_APPS = ( 'taskresults', + 'mysqltasks', + 'pgsqltasks', 'domains', 'osusers', 'managemails', diff --git a/gnuviechadmin/mysqltasks/__init__.py b/gnuviechadmin/mysqltasks/__init__.py new file mode 100644 index 0000000..9bc503f --- /dev/null +++ b/gnuviechadmin/mysqltasks/__init__.py @@ -0,0 +1,4 @@ +""" +This module contains :py:mod:`mysqltasks.tasks`. + +""" diff --git a/gnuviechadmin/mysqltasks/models.py b/gnuviechadmin/mysqltasks/models.py new file mode 100644 index 0000000..cf80bcf --- /dev/null +++ b/gnuviechadmin/mysqltasks/models.py @@ -0,0 +1,4 @@ +""" +Empty models to make Django accept mysqltasks as an app. + +""" diff --git a/gnuviechadmin/mysqltasks/tasks.py b/gnuviechadmin/mysqltasks/tasks.py new file mode 100644 index 0000000..cda59d4 --- /dev/null +++ b/gnuviechadmin/mysqltasks/tasks.py @@ -0,0 +1,72 @@ +""" +This module defines Celery_ tasks to manage MySQL users and databases. + +""" +from __future__ import absolute_import + +from celery import shared_task + + +@shared_task +def create_mysql_user(username, password): + """ + This task creates a new MySQL user. + + :param str username: the user name + :param str password: the password + :return: the created user's name + :rtype: str + + """ + + +@shared_task +def set_mysql_userpassword(username, password): + """ + Set a new password for an existing MySQL user. + + :param str username: the user name + :param str password: the password + :return: True if the password could be set, False otherwise + :rtype: boolean + + """ + + +@shared_task +def delete_mysql_user(username): + """ + This task deletes an existing MySQL user. + + :param str username: the user name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ + + +@shared_task +def create_mysql_database(dbname, username): + """ + This task creates a new MySQL database for the given MySQL user. + + :param str dbname: database name + :param str username: the user name of an existing MySQL user + :return: the database name + :rtype: str + + """ + + +@shared_task +def delete_mysql_database(dbname, username): + """ + This task deletes an existing MySQL database and revokes privileges of the + given user on that database. + + :param str dbname: database name + :param str username: the user name of an existing MySQL user + :return: True if the database has been deleted, False otherwise + :rtype: boolean + + """ diff --git a/gnuviechadmin/pgsqltasks/__init__.py b/gnuviechadmin/pgsqltasks/__init__.py new file mode 100644 index 0000000..c49a650 --- /dev/null +++ b/gnuviechadmin/pgsqltasks/__init__.py @@ -0,0 +1,4 @@ +""" +This module contains :py:mod:`pgsqltasks.tasks`. + +""" diff --git a/gnuviechadmin/pgsqltasks/models.py b/gnuviechadmin/pgsqltasks/models.py new file mode 100644 index 0000000..3a43087 --- /dev/null +++ b/gnuviechadmin/pgsqltasks/models.py @@ -0,0 +1,4 @@ +""" +Empty models to make Django accept pgsqltasks as an app. + +""" diff --git a/gnuviechadmin/pgsqltasks/tasks.py b/gnuviechadmin/pgsqltasks/tasks.py new file mode 100644 index 0000000..039b74a --- /dev/null +++ b/gnuviechadmin/pgsqltasks/tasks.py @@ -0,0 +1,72 @@ +""" +This module defines Celery_ tasks to manage PostgreSQL users and databases. + +""" +from __future__ import absolute_import + +from celery import shared_task + + +@shared_task +def create_pgsql_user(username, password): + """ + This task creates a new PostgreSQL user. + + :param str username: the user name + :param str password: the password + :return: the created user's name + :rtype: str + + """ + + +@shared_task +def set_pgsql_userpassword(username, password): + """ + Set a new password for an existing PostgreSQL user. + + :param str username: the user name + :param str password: the password + :return: True if the password could be set, False otherwise + :rtype: boolean + + """ + + +@shared_task +def delete_pgsql_user(username): + """ + This task deletes an existing PostgreSQL user. + + :param str username: the user name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ + + +@shared_task +def create_pgsql_database(dbname, username): + """ + This task creates a new PostgreSQL database for the given PostgreSQL user. + + :param str dbname: database name + :param str username: the user name of an existing PostgreSQL user + :return: the database name + :rtype: str + + """ + + +@shared_task +def delete_pgsql_database(dbname, username): + """ + This task deletes an existing PostgreSQL database and revokes privileges of + the given user on that database. + + :param str dbname: database name + :param str username: the user name of an existing PostgreSQL user + :return: True if the database has been deleted, False otherwise + :rtype: boolean + + """ From 6695d1bad4a1fbd9dd3784a4a6cc0c7c82d64f5e Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 20:19:48 +0100 Subject: [PATCH 15/25] document addition of mysqltasks and pgsqltasks --- docs/changelog.rst | 1 + docs/code.rst | 24 ++++++++++++++++++++++++ docs/conf.py | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 67fc19f..77f3668 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* :feature:`-` add mysqltasks and pgsqltasks * :feature:`-` add new app :py:mod:`taskresults` that takes care of handling asynchronous `Celery`_ results * :feature:`-` add new task :py:func:`osusers.tasks.delete_ldap_group` (needs diff --git a/docs/code.rst b/docs/code.rst index eb4f62f..22f2a53 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -104,6 +104,18 @@ provides some functionality that is common to all gnuviechadmin subprojects. :members: +:py:mod:`mysqltasks` app +======================== + +.. automodule:: mysqltasks + + +:py:mod:`tasks ` +---------------------------------- + +.. automodule:: mysqltasks.tasks + + :py:mod:`osusers` app ===================== @@ -151,6 +163,18 @@ provides some functionality that is common to all gnuviechadmin subprojects. .. autotask:: osusers.tasks.setup_file_sftp_userdir +:py:mod:`pgsqltasks` app +======================== + +.. automodule:: pgsqltasks + + +:py:mod:`tasks ` +---------------------------------- + +.. automodule:: pgsqltasks.tasks + + :py:mod:`taskresults` app ========================= diff --git a/docs/conf.py b/docs/conf.py index 748e1c4..fc43377 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ master_doc = 'index' # General information about the project. project = u'gnuviechadmin' -copyright = u'2014, Jan Dittberner' +copyright = u'2014, 2015 Jan Dittberner' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 3eef84d373e31fc95b20146e6db0c781b6eb3113 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 17:57:51 +0100 Subject: [PATCH 16/25] add new incomplete userdbs app --- gnuviechadmin/gnuviechadmin/settings/base.py | 1 + gnuviechadmin/userdbs/__init__.py | 0 gnuviechadmin/userdbs/admin.py | 3 + gnuviechadmin/userdbs/migrations/__init__.py | 0 gnuviechadmin/userdbs/models.py | 272 +++++++++++++++++++ gnuviechadmin/userdbs/tests.py | 3 + gnuviechadmin/userdbs/views.py | 3 + 7 files changed, 282 insertions(+) create mode 100644 gnuviechadmin/userdbs/__init__.py create mode 100644 gnuviechadmin/userdbs/admin.py create mode 100644 gnuviechadmin/userdbs/migrations/__init__.py create mode 100644 gnuviechadmin/userdbs/models.py create mode 100644 gnuviechadmin/userdbs/tests.py create mode 100644 gnuviechadmin/userdbs/views.py diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py index cfd72f7..33f5b85 100644 --- a/gnuviechadmin/gnuviechadmin/settings/base.py +++ b/gnuviechadmin/gnuviechadmin/settings/base.py @@ -230,6 +230,7 @@ LOCAL_APPS = ( 'domains', 'osusers', 'managemails', + 'userdbs', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps diff --git a/gnuviechadmin/userdbs/__init__.py b/gnuviechadmin/userdbs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/userdbs/admin.py b/gnuviechadmin/userdbs/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/gnuviechadmin/userdbs/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/gnuviechadmin/userdbs/migrations/__init__.py b/gnuviechadmin/userdbs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/userdbs/models.py b/gnuviechadmin/userdbs/models.py new file mode 100644 index 0000000..2a230a3 --- /dev/null +++ b/gnuviechadmin/userdbs/models.py @@ -0,0 +1,272 @@ +from __future__ import unicode_literals + +from django.db import models +from django.db import transaction +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext as _ + +from model_utils import Choices +from model_utils.models import TimeStampedModel + +from passlib.utils import generate_password + +from osusers.models import User as OsUser + +from mysqltasks.tasks import ( + create_mysql_database, + create_mysql_user, + delete_mysql_database, + delete_mysql_user, + set_mysql_userpassword, +) +from pgsqltasks.tasks import ( + create_pgsql_database, + create_pgsql_user, + delete_pgsql_database, + delete_pgsql_user, + set_pgsql_userpassword, +) + + +DB_TYPES = Choices( + (0, 'pgsql', _('PostgreSQL')), + (1, 'mysql', _('MySQL')), +) +""" +Database type choice enumeration. +""" + + +class DatabaseUserManager(models.Manager): + """ + Default Manager for :py:class:`userdbs.models.DatabaseUser`. + + """ + + def _get_next_username(self, osuser, db_type): + """ + Get the next available database user name. + + :param osuser: :py:class:`osusers.models.User` instance + :param db_type: value from :py:data:`DB_TYPES` + :return: database user name + :rtype: str + """ + count = 1 + db_username_format = "{0}db{{1:02d}}".format(osuser.username) + nextname = db_username_format.format(count) + + for user in self.values('name').filter( + osuser=osuser, db_type=db_type + ).order_by('name'): + if user['name'] == nextname: + count += 1 + nextname = db_username_format.format(count) + else: + break + return nextname + + @transaction.atomic + def create_database_user( + self, osuser, db_type, username=None, password=None, commit=True + ): + """ + Create a database user of the given type for the given OS user. + + If username or password are not specified they are generated. + + :param osuser: the :py:class:`osusers.models.User` instance + :param db_type: value from :py:data:`DB_TYPES` + :param str username: database user name + :param str password: password for the user + :param boolean commit: whether the user should be persisted + :return: :py:class:`userdbs.models.DatabaseUser` instance + + .. note:: + + The password is not persisted it is only used to set the password + on the database side. + + """ + if username is None: + username = self._get_next_username(osuser, db_type) + if password is None: + password = generate_password() + db_user = DatabaseUser( + osuser=osuser, db_type=db_type, username=username) + if commit: + db_user.create_in_database(password) + # TODO: send GPG encrypted mail with this information + db_user.save() + return db_user + + + +@python_2_unicode_compatible +class DatabaseUser(TimeStampedModel, models.Model): + osuser = models.ForeignKey(OsUser) + name = models.CharField( + _('username'), max_length=63) + db_type = models.PositiveSmallIntegerField( + _('database type'), choices=DB_TYPES) + + objects = DatabaseUserManager() + + class Meta: + unique_together = ['name', 'db_type'] + verbose_name = _('database user') + verbose_name_plural = _('database users') + + def __str__(self): + return "%(name)s (%(db_type)s for %(osuser)s)" % { + 'name': self.name, + 'db_type': self.db_type, + 'osuser': self.osuser.username, + } + + def create_in_database(self, password): + """ + Create this user in the target database. + + :param str password: initial password for the database user + """ + if self.db_type == DB_TYPES.pgsql: + create_pgsql_user.delay(self.name, password).get() + elif self.db_type == DB_TYPES.mysql: + create_mysql_user.delay(self.name, password).get() + else: + raise ValueError('Unknown database type %d' % self.db_type) + + def set_password(self, password): + """ + Set an existing user's password. + + :param str password: new password for the database user + """ + if self.db_type == DB_TYPES.pgsql: + set_pgsql_userpassword.delay(self.name, password).get() + elif self.db_type == DB_TYPES.mysql: + set_mysql_userpassword.delay(self.name, password).get() + else: + raise ValueError('Unknown database type %d' % self.db_type) + + @transaction.atomic + def delete(self, *args, **kwargs): + """ + Delete the database user from the target database and the Django + database. + + :param args: positional arguments for + :py:meth:`django.db.models.Model.delete` + :param kwargs: keyword arguments for + :py:meth:`django.db.models.Model.delete` + + """ + if self.db_type == DB_TYPES.pgsql: + delete_pgsql_user.delay(self.name).get() + elif self.db_type == DB_TYPES.mysql: + delete_mysql_user.delay(self.name).get() + else: + raise ValueError('Unknown database type %d' % self.db_type) + super(DatabaseUser, self).delete(*args, **kwargs) + + +class UserDatabaseManager(models.Manager): + """ + Default manager for :py:class:`userdbs.models.UserDatabase` instances. + + """ + + def _get_next_dbname(self, db_user): + """ + Get the next available database name for the given database user. + + :param db_user: :py:class:`userdbs.models.DatabaseUser` instance + :return: database name + :rtype: str + + """ + count = 1 + db_name_format = "{0}_{{1:02d}}".format(db_user.name) + # first db is named the same as the user + nextname = db_user.name + for name in self.values('db_name').filter(db_user=db_user).order_by( + 'db_name' + ): + if name['db_name'] == nextname: + count += 1 + nextname = db_name_format.format(count) + else: + break + return nextname + + @transaction.atomic + def create_userdatabase(self, db_user, db_name=None, commit=True): + """ + Creates a new user database. + + :param db_user: :py:class:`userdbs.models.DatabaseUser` instance + :param str db_name: database name + :param boolean commit: whether the database should be persisted + :return: :py:class:`userdbs.models.UserDatabase` instance + + """ + if db_name is None: + db_name = self._get_next_dbname(db_user) + database = UserDatabase(db_user=db_user, db_name=db_name) + if commit: + database.create_in_database() + # TODO: send GPG encrypted mail with this information + database.save() + return database + + +@python_2_unicode_compatible +class UserDatabase(TimeStampedModel, models.Model): + # MySQL limits to 64, PostgreSQL to 63 characters + db_name = models.CharField( + _('database name'), max_length=63) + db_user = models.ForeignKey(DatabaseUser, verbose_name=_('database user')) + + class Meta: + unique_together = ['db_name', 'db_user'] + verbose_name = _('user database') + verbose_name_plural = _('user specific database') + + def __str__(self): + return "%(db_name)s (%(db_user)s)" % { + 'db_name': self.db_name, + 'db_user': self.db_user, + } + + def create_in_database(self): + """ + Create this database (schema) in the target database. + + """ + if self.db_user.db_type == DB_TYPES.pgsql: + create_pgsql_database.delay(self.db_name, self.db_user.name).get() + elif self.db_user.db_type == DB_TYPES.mysql: + create_mysql_database.delay(self.db_name, self.db_user.name).get() + else: + raise ValueError('Unknown database type %d' % self.db_type) + + @transaction.atomic + def delete(self, *args, **kwargs): + """ + Delete the database (schema) from the target database and the Django + database. + + :param args: positional arguments for + :py:meth:`django.db.models.Model.delete` + :param kwargs: keyword arguments for + :py:meth:`django.db.models.Model.delete` + + """ + if self.db_type == DB_TYPES.pgsql: + delete_pgsql_database.delay(self.db_name, self.db_user.name).get() + elif self.db_type == DB_TYPES.mysql: + delete_mysql_database.delay(self.db_name, self.db_user.name).get() + else: + raise ValueError('Unknown database type %d' % self.db_type) + super(UserDatabase, self).delete(*args, **kwargs) diff --git a/gnuviechadmin/userdbs/tests.py b/gnuviechadmin/userdbs/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/gnuviechadmin/userdbs/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gnuviechadmin/userdbs/views.py b/gnuviechadmin/userdbs/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/gnuviechadmin/userdbs/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 8697f843819587fd850aaac24cd01ece10e0781b Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 20:22:47 +0100 Subject: [PATCH 17/25] add admin and a bit of documentation --- docs/changelog.rst | 2 ++ docs/code.rst | 18 ++++++++++++++++++ gnuviechadmin/userdbs/admin.py | 15 ++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 77f3668..f9dd798 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= * :feature:`-` add mysqltasks and pgsqltasks +* :feature:`-` add :py:mod:`userdbs` app to allow management of user databases + via :py:mod:`mysqltasks` and :py:mod:`pgsqltasks` * :feature:`-` add new app :py:mod:`taskresults` that takes care of handling asynchronous `Celery`_ results * :feature:`-` add new task :py:func:`osusers.tasks.delete_ldap_group` (needs diff --git a/docs/code.rst b/docs/code.rst index 22f2a53..e61db41 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -199,3 +199,21 @@ provides some functionality that is common to all gnuviechadmin subprojects. ------------------------------------- .. automodule:: taskresults.models + + +:py:mod:`userdbs` app +===================== + +.. automodule:: userdbs + + +:py:mod:`admin ` +------------------------------- + +.. automodule:: userdbs.admin + + +:py:mod:`models ` +--------------------------------- + +.. automodule:: userdbs.models diff --git a/gnuviechadmin/userdbs/admin.py b/gnuviechadmin/userdbs/admin.py index 8c38f3f..368b3f1 100644 --- a/gnuviechadmin/userdbs/admin.py +++ b/gnuviechadmin/userdbs/admin.py @@ -1,3 +1,16 @@ +""" +Admin functionality for the :py:mod:`userdbs.models` models. + +""" +from __future__ import absolute_import + from django.contrib import admin -# Register your models here. +from .models import ( + DatabaseUser, + UserDatabase, +) + + +admin.site.register(DatabaseUser) +admin.site.register(UserDatabase) \ No newline at end of file From be4ea9cc77ffa19bca5ef95ef3dec24a5c1cc820 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 20:24:56 +0100 Subject: [PATCH 18/25] add initial migration for userdbs --- .../userdbs/migrations/0001_initial.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 gnuviechadmin/userdbs/migrations/0001_initial.py diff --git a/gnuviechadmin/userdbs/migrations/0001_initial.py b/gnuviechadmin/userdbs/migrations/0001_initial.py new file mode 100644 index 0000000..7a81c3c --- /dev/null +++ b/gnuviechadmin/userdbs/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('osusers', '0004_auto_20150104_1751'), + ] + + operations = [ + migrations.CreateModel( + name='DatabaseUser', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('name', models.CharField(max_length=63, verbose_name='username')), + ('db_type', models.PositiveSmallIntegerField(verbose_name='database type', choices=[(0, 'PostgreSQL'), (1, 'MySQL')])), + ('osuser', models.ForeignKey(to='osusers.User')), + ], + options={ + 'verbose_name': 'database user', + 'verbose_name_plural': 'database users', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='UserDatabase', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('db_name', models.CharField(max_length=63, verbose_name='database name')), + ('db_user', models.ForeignKey(verbose_name='database user', to='userdbs.DatabaseUser')), + ], + options={ + 'verbose_name': 'user database', + 'verbose_name_plural': 'user specific database', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='userdatabase', + unique_together=set([('db_name', 'db_user')]), + ), + migrations.AlterUniqueTogether( + name='databaseuser', + unique_together=set([('name', 'db_type')]), + ), + ] From 6edbe17a3b40204d6f9f48fbfcf2fb3d648adc5a Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 22:27:43 +0100 Subject: [PATCH 19/25] make userdbs admin work properly * add userdbs app docstring * add userdbs.app.UserdbsAppConfig * implement userdbs.admin.DatabaseUserCreationForm, userdbs.admin.UserDatabaseCreationForm, userdbs.admin.DatabaseUserAdmin, userdbs.admin.UserDatabaseAdmin * add docstrings to userdbs.models * rename userdbs.models.DatabaseUserManager._get_next_username to _get_next_dbuser_name * fix format string issues in userdbs.models.DatabaseUserManager and userdbs.UserDatabaseManager._get_next_dbname * delete related databases in userdbs.models.UserDatabase.delete --- gnuviechadmin/userdbs/__init__.py | 5 + gnuviechadmin/userdbs/admin.py | 267 +++++++++++++++++++++++++++++- gnuviechadmin/userdbs/apps.py | 18 ++ gnuviechadmin/userdbs/models.py | 46 ++--- 4 files changed, 313 insertions(+), 23 deletions(-) create mode 100644 gnuviechadmin/userdbs/apps.py diff --git a/gnuviechadmin/userdbs/__init__.py b/gnuviechadmin/userdbs/__init__.py index e69de29..6f1b8ad 100644 --- a/gnuviechadmin/userdbs/__init__.py +++ b/gnuviechadmin/userdbs/__init__.py @@ -0,0 +1,5 @@ +""" +This app is for managing database users and user databases. + +""" +default_app_config = 'userdbs.apps.UserdbsAppConfig' diff --git a/gnuviechadmin/userdbs/admin.py b/gnuviechadmin/userdbs/admin.py index 368b3f1..7a498c8 100644 --- a/gnuviechadmin/userdbs/admin.py +++ b/gnuviechadmin/userdbs/admin.py @@ -4,7 +4,9 @@ Admin functionality for the :py:mod:`userdbs.models` models. """ from __future__ import absolute_import +from django import forms from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ from .models import ( DatabaseUser, @@ -12,5 +14,266 @@ from .models import ( ) -admin.site.register(DatabaseUser) -admin.site.register(UserDatabase) \ No newline at end of file +class DatabaseUserCreationForm(forms.ModelForm): + """ + A form for creating :py:class:`database users + ` + + """ + + class Meta: + model = DatabaseUser + fields = ['osuser', 'db_type'] + + def save(self, commit=True): + """ + Save the database user. + + :param boolean commit: whether to save the created database user + :return: database user instance + :rtype: :py:class:`userdbs.models.DatabaseUser` + + """ + dbuser = DatabaseUser.objects.create_database_user( + osuser=self.cleaned_data['osuser'], + db_type=self.cleaned_data['db_type'], commit=commit) + return dbuser + + def save_m2m(self): + """ + Noop. + + """ + + +class UserDatabaseCreationForm(forms.ModelForm): + """ + A form for creating :py:class:`user databases + ` + + """ + + class Meta: + model = UserDatabase + fields = ['db_user'] + + def save(self, commit=True): + """ + Save the user database. + + :param boolean commit: whether to save the created user database + :return: user database instance + :rtype: :py:class:`userdbs.models.UserDatabase` + + """ + database = UserDatabase.objects.create_userdatabase( + db_user=self.cleaned_data['db_user'], commit=commit) + return database + + def save_m2m(self): + """ + Noop. + + """ + + +class DatabaseUserAdmin(admin.ModelAdmin): + """ + Admin class for working with :py:class:`database users + ` + + """ + actions = ['perform_delete_selected'] + add_form = DatabaseUserCreationForm + + def get_form(self, request, obj=None, **kwargs): + """ + Use special form for database user creation. + + :param request: the current HTTP request + :param obj: either a :py:class:`Database user + ` instance or None for a new database + user + :param kwargs: keyword arguments to be passed to + :py:meth:`django.contrib.admin.ModelAdmin.get_form` + :return: form instance + + """ + defaults = {} + if obj is None: + defaults.update({ + 'form': self.add_form, + }) + defaults.update(kwargs) + return super(DatabaseUserAdmin, self).get_form( + request, obj, **defaults) + + def get_readonly_fields(self, request, obj=None): + """ + Make sure that osuser, name and db_type are not editable for existing + database users. + + :param request: the current HTTP request + :param obj: either a :py:class:`Database user + ` instance or None for a new database + user + :return: a list of fields + :rtype: list + + """ + if obj: + return ['osuser', 'name', 'db_type'] + return [] + + def save_model(self, request, obj, form, change): + """ + Make sure that the user is created in the target database. + + :param request: the current HTTP request + :param obj: a :py:class:`Database user ` + instance + :param form: the form instance + :param boolean change: whether this is a change operation or not + + """ + if not change: + obj.create_in_database() + super(DatabaseUserAdmin, self).save_model(request, obj, form, change) + + def perform_delete_selected(self, request, queryset): + """ + Action to delete a list of selected database users. + + This action calls the delete method of each selected database user in + contrast to the default `delete_selected` + + :param request: the current HTTP request + :param queryset: Django ORM queryset representing the selected database + users + + """ + for dbuser in queryset.all(): + dbuser.delete() + perform_delete_selected.short_description = _( + 'Delete selected database users') + + def get_actions(self, request): + """ + Get the available actions for database users. + + This overrides the default behavior to remove the default + `delete_selected` action. + + :param request: the current HTTP request + :return: list of actions + :rtype: list + + """ + actions = super(DatabaseUserAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions + + +class UserDatabaseAdmin(admin.ModelAdmin): + """ + Admin class for working with :py:class:`user databases + ` + + """ + actions = ['perform_delete_selected'] + add_form = UserDatabaseCreationForm + + def get_form(self, request, obj=None, **kwargs): + """ + Use special form for user database creation. + + :param request: the current HTTP request + :param obj: either a :py:class:`User database + ` instance or None for a new user + database + :param kwargs: keyword arguments to be passed to + :py:meth:`django.contrib.admin.ModelAdmin.get_form` + :return: form instance + + """ + defaults = {} + if obj is None: + defaults.update({ + 'form': self.add_form, + }) + defaults.update(kwargs) + return super(UserDatabaseAdmin, self).get_form( + request, obj, **defaults) + + def get_readonly_fields(self, request, obj=None): + """ + Make sure that db_name and db_user are not editable for existing user + databases. + + :param request: the current HTTP request + :param obj: either a :py:class:`User database + ` instance or None for a new user + database + :return: a list of fields + :rtype: list + + """ + if obj: + return ['db_name', 'db_user'] + return [] + + def save_model(self, request, obj, form, change): + """ + Make sure that the database is created in the target database server. + + :param request: the current HTTP request + :param obj: a :py:class:`Database user ` + instance + :param form: the form instance + :param boolean change: whether this is a change operation or not + + """ + if not change: + obj.create_in_database() + super(UserDatabaseAdmin, self).save_model(request, obj, form, change) + + def perform_delete_selected(self, request, queryset): + """ + Action to delete a list of selected user databases. + + This action calls the delete method of each selected user database in + contrast to the default `delete_selected` + + :param request: the current HTTP request + :param queryset: Django ORM queryset representing the selected user + databases + + """ + for dbuser in queryset.all(): + dbuser.delete() + for database in queryset.all(): + database.delete() + perform_delete_selected.short_description = _( + 'Delete selected user databases') + + def get_actions(self, request): + """ + Get the available actions for user databases. + + This overrides the default behavior to remove the default + `delete_selected` action. + + :param request: the current HTTP request + :return: list of actions + :rtype: list + + """ + actions = super(UserDatabaseAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions + + +admin.site.register(DatabaseUser, DatabaseUserAdmin) +admin.site.register(UserDatabase, UserDatabaseAdmin) diff --git a/gnuviechadmin/userdbs/apps.py b/gnuviechadmin/userdbs/apps.py new file mode 100644 index 0000000..40fd5ec --- /dev/null +++ b/gnuviechadmin/userdbs/apps.py @@ -0,0 +1,18 @@ +""" +This module contains the :py:class:`django.apps.AppConfig` instance for the +:py:mod:`userdbs` app. + +""" +from __future__ import unicode_literals + +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class UserdbsAppConfig(AppConfig): + """ + AppConfig for the :py:mod:`userdbs` app. + + """ + name = 'userdbs' + verbose_name = _('Database Users and their Databases') diff --git a/gnuviechadmin/userdbs/models.py b/gnuviechadmin/userdbs/models.py index 2a230a3..899ed58 100644 --- a/gnuviechadmin/userdbs/models.py +++ b/gnuviechadmin/userdbs/models.py @@ -43,7 +43,7 @@ class DatabaseUserManager(models.Manager): """ - def _get_next_username(self, osuser, db_type): + def _get_next_dbuser_name(self, osuser, db_type): """ Get the next available database user name. @@ -53,15 +53,15 @@ class DatabaseUserManager(models.Manager): :rtype: str """ count = 1 - db_username_format = "{0}db{{1:02d}}".format(osuser.username) - nextname = db_username_format.format(count) + dbuser_name_format = "{0}db{{0:02d}}".format(osuser.username) + nextname = dbuser_name_format.format(count) for user in self.values('name').filter( osuser=osuser, db_type=db_type ).order_by('name'): if user['name'] == nextname: count += 1 - nextname = db_username_format.format(count) + nextname = dbuser_name_format.format(count) else: break return nextname @@ -78,7 +78,7 @@ class DatabaseUserManager(models.Manager): :param osuser: the :py:class:`osusers.models.User` instance :param db_type: value from :py:data:`DB_TYPES` :param str username: database user name - :param str password: password for the user + :param str password: initial password or None :param boolean commit: whether the user should be persisted :return: :py:class:`userdbs.models.DatabaseUser` instance @@ -89,14 +89,11 @@ class DatabaseUserManager(models.Manager): """ if username is None: - username = self._get_next_username(osuser, db_type) - if password is None: - password = generate_password() + username = self._get_next_dbuser_name(osuser, db_type) db_user = DatabaseUser( - osuser=osuser, db_type=db_type, username=username) + osuser=osuser, db_type=db_type, name=username, password=password) if commit: - db_user.create_in_database(password) - # TODO: send GPG encrypted mail with this information + db_user.create_in_database() db_user.save() return db_user @@ -120,16 +117,19 @@ class DatabaseUser(TimeStampedModel, models.Model): def __str__(self): return "%(name)s (%(db_type)s for %(osuser)s)" % { 'name': self.name, - 'db_type': self.db_type, + 'db_type': self.get_db_type_display(), 'osuser': self.osuser.username, } - def create_in_database(self, password): + def create_in_database(self, password=None): """ Create this user in the target database. :param str password: initial password for the database user """ + if password is None: + password = generate_password() + # TODO: send GPG encrypted mail with this information if self.db_type == DB_TYPES.pgsql: create_pgsql_user.delay(self.name, password).get() elif self.db_type == DB_TYPES.mysql: @@ -144,9 +144,9 @@ class DatabaseUser(TimeStampedModel, models.Model): :param str password: new password for the database user """ if self.db_type == DB_TYPES.pgsql: - set_pgsql_userpassword.delay(self.name, password).get() + set_pgsql_userpassword.delay(self.name, password).get(timeout=5) elif self.db_type == DB_TYPES.mysql: - set_mysql_userpassword.delay(self.name, password).get() + set_mysql_userpassword.delay(self.name, password).get(timeout=5) else: raise ValueError('Unknown database type %d' % self.db_type) @@ -162,10 +162,12 @@ class DatabaseUser(TimeStampedModel, models.Model): :py:meth:`django.db.models.Model.delete` """ + for database in self.userdatabase_set.all(): + database.delete() if self.db_type == DB_TYPES.pgsql: - delete_pgsql_user.delay(self.name).get() + delete_pgsql_user.delay(self.name).get(propagate=False, timeout=5) elif self.db_type == DB_TYPES.mysql: - delete_mysql_user.delay(self.name).get() + delete_mysql_user.delay(self.name).get(propagate=False, timeout=5) else: raise ValueError('Unknown database type %d' % self.db_type) super(DatabaseUser, self).delete(*args, **kwargs) @@ -187,7 +189,7 @@ class UserDatabaseManager(models.Manager): """ count = 1 - db_name_format = "{0}_{{1:02d}}".format(db_user.name) + db_name_format = "{0}_{{0:02d}}".format(db_user.name) # first db is named the same as the user nextname = db_user.name for name in self.values('db_name').filter(db_user=db_user).order_by( @@ -216,7 +218,6 @@ class UserDatabaseManager(models.Manager): database = UserDatabase(db_user=db_user, db_name=db_name) if commit: database.create_in_database() - # TODO: send GPG encrypted mail with this information database.save() return database @@ -228,6 +229,8 @@ class UserDatabase(TimeStampedModel, models.Model): _('database name'), max_length=63) db_user = models.ForeignKey(DatabaseUser, verbose_name=_('database user')) + objects = UserDatabaseManager() + class Meta: unique_together = ['db_name', 'db_user'] verbose_name = _('user database') @@ -244,6 +247,7 @@ class UserDatabase(TimeStampedModel, models.Model): Create this database (schema) in the target database. """ + # TODO: send GPG encrypted mail with this information if self.db_user.db_type == DB_TYPES.pgsql: create_pgsql_database.delay(self.db_name, self.db_user.name).get() elif self.db_user.db_type == DB_TYPES.mysql: @@ -263,9 +267,9 @@ class UserDatabase(TimeStampedModel, models.Model): :py:meth:`django.db.models.Model.delete` """ - if self.db_type == DB_TYPES.pgsql: + if self.db_user.db_type == DB_TYPES.pgsql: delete_pgsql_database.delay(self.db_name, self.db_user.name).get() - elif self.db_type == DB_TYPES.mysql: + elif self.db_user.db_type == DB_TYPES.mysql: delete_mysql_database.delay(self.db_name, self.db_user.name).get() else: raise ValueError('Unknown database type %d' % self.db_type) From 0e22815ee57876d1a0409b50071ef0206c1f0355 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 22:36:40 +0100 Subject: [PATCH 20/25] add autogenerated documentation for module members --- docs/code.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/code.rst b/docs/code.rst index e61db41..33978fe 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -114,6 +114,7 @@ provides some functionality that is common to all gnuviechadmin subprojects. ---------------------------------- .. automodule:: mysqltasks.tasks + :members: :py:mod:`osusers` app @@ -167,6 +168,7 @@ provides some functionality that is common to all gnuviechadmin subprojects. ======================== .. automodule:: pgsqltasks + :members: :py:mod:`tasks ` @@ -211,9 +213,11 @@ provides some functionality that is common to all gnuviechadmin subprojects. ------------------------------- .. automodule:: userdbs.admin + :members: :py:mod:`models ` --------------------------------- .. automodule:: userdbs.models + :members: From 706a4ef177b7cd19af282fec482f5aa20b6545f8 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 4 Jan 2015 22:47:14 +0100 Subject: [PATCH 21/25] fix documentation issues - add autotask directives for pgsqltasks and mysqltasks - remove duplicate Celery_ link target --- docs/code.rst | 12 ++++++++++++ gnuviechadmin/taskresults/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/code.rst b/docs/code.rst index 33978fe..8cc0a03 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -116,6 +116,12 @@ provides some functionality that is common to all gnuviechadmin subprojects. .. automodule:: mysqltasks.tasks :members: +.. autotask:: mysqltasks.tasks.create_mysql_database +.. autotask:: mysqltasks.tasks.create_mysql_user +.. autotask:: mysqltasks.tasks.delete_mysql_database +.. autotask:: mysqltasks.tasks.delete_mysql_user +.. autotask:: mysqltasks.tasks.set_mysql_userpassword + :py:mod:`osusers` app ===================== @@ -176,6 +182,12 @@ provides some functionality that is common to all gnuviechadmin subprojects. .. automodule:: pgsqltasks.tasks +.. autotask:: pgsqltasks.tasks.create_pgsql_database +.. autotask:: pgsqltasks.tasks.create_pgsql_user +.. autotask:: pgsqltasks.tasks.delete_pgsql_database +.. autotask:: pgsqltasks.tasks.delete_pgsql_user +.. autotask:: pgsqltasks.tasks.set_pgsql_userpassword + :py:mod:`taskresults` app ========================= diff --git a/gnuviechadmin/taskresults/__init__.py b/gnuviechadmin/taskresults/__init__.py index 0b3d916..dace904 100644 --- a/gnuviechadmin/taskresults/__init__.py +++ b/gnuviechadmin/taskresults/__init__.py @@ -1,5 +1,5 @@ """ This is the taskresults app that is used for storing the results from -asynchronous `Celery `_ tasks. +asynchronous Celery_ tasks. """ From 1af553c1fafdd7cfb5cdb15d96bb650afd5de8a2 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Fri, 9 Jan 2015 19:37:12 +0100 Subject: [PATCH 22/25] remove username argument of pgsqltasks.tasks.delete_pgsql_database --- gnuviechadmin/pgsqltasks/tasks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gnuviechadmin/pgsqltasks/tasks.py b/gnuviechadmin/pgsqltasks/tasks.py index 039b74a..5aa40d6 100644 --- a/gnuviechadmin/pgsqltasks/tasks.py +++ b/gnuviechadmin/pgsqltasks/tasks.py @@ -59,13 +59,11 @@ def create_pgsql_database(dbname, username): @shared_task -def delete_pgsql_database(dbname, username): +def delete_pgsql_database(dbname): """ - This task deletes an existing PostgreSQL database and revokes privileges of - the given user on that database. + This task deletes an existing PostgreSQL database. :param str dbname: database name - :param str username: the user name of an existing PostgreSQL user :return: True if the database has been deleted, False otherwise :rtype: boolean From 780a6705656a5fdea8987c0665953699c91f3eb1 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Fri, 9 Jan 2015 19:38:30 +0100 Subject: [PATCH 23/25] remove username argument from delete_pgsql_database call --- gnuviechadmin/userdbs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnuviechadmin/userdbs/models.py b/gnuviechadmin/userdbs/models.py index 899ed58..21bb882 100644 --- a/gnuviechadmin/userdbs/models.py +++ b/gnuviechadmin/userdbs/models.py @@ -268,7 +268,7 @@ class UserDatabase(TimeStampedModel, models.Model): """ if self.db_user.db_type == DB_TYPES.pgsql: - delete_pgsql_database.delay(self.db_name, self.db_user.name).get() + delete_pgsql_database.delay(self.db_name).get() elif self.db_user.db_type == DB_TYPES.mysql: delete_mysql_database.delay(self.db_name, self.db_user.name).get() else: From ba39e3f76556c0ff6000a27afdac1a41c9a89150 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 10 Jan 2015 18:43:43 +0100 Subject: [PATCH 24/25] set database password at the appropriate place --- gnuviechadmin/userdbs/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnuviechadmin/userdbs/models.py b/gnuviechadmin/userdbs/models.py index 21bb882..c678a31 100644 --- a/gnuviechadmin/userdbs/models.py +++ b/gnuviechadmin/userdbs/models.py @@ -91,9 +91,9 @@ class DatabaseUserManager(models.Manager): if username is None: username = self._get_next_dbuser_name(osuser, db_type) db_user = DatabaseUser( - osuser=osuser, db_type=db_type, name=username, password=password) + osuser=osuser, db_type=db_type, name=username) if commit: - db_user.create_in_database() + db_user.create_in_database(password=password) db_user.save() return db_user From 1050f098f457ea114a1b783b790c323f77ca51d7 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 11 Jan 2015 15:32:46 +0100 Subject: [PATCH 25/25] define version number, update changelog --- docs/changelog.rst | 1 + docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f9dd798..411c535 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* :release:`0.4.0 <2015-01-11>` * :feature:`-` add mysqltasks and pgsqltasks * :feature:`-` add :py:mod:`userdbs` app to allow management of user databases via :py:mod:`mysqltasks` and :py:mod:`pgsqltasks` diff --git a/docs/conf.py b/docs/conf.py index fc43377..998aeeb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ copyright = u'2014, 2015 Jan Dittberner' # built documents. # # The short X.Y version. -version = '0.3' +version = '0.4' # The full version, including alpha/beta/rc tags. -release = '0.3.0' +release = '0.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages.