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/docs/changelog.rst b/docs/changelog.rst
index ff8024b..411c535 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,19 @@
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`
+* :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`
+* :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
@@ -23,10 +36,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
new file mode 100644
index 0000000..8cc0a03
--- /dev/null
+++ b/docs/code.rst
@@ -0,0 +1,235 @@
+******************
+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:`mysqltasks` app
+========================
+
+.. automodule:: mysqltasks
+
+
+:py:mod:`tasks `
+----------------------------------
+
+.. 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
+=====================
+
+.. 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
+.. 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
+
+
+:py:mod:`pgsqltasks` app
+========================
+
+.. automodule:: pgsqltasks
+ :members:
+
+
+:py:mod:`tasks `
+----------------------------------
+
+.. 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
+=========================
+
+.. 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
+
+
+:py:mod:`userdbs` app
+=====================
+
+.. automodule:: userdbs
+
+
+:py:mod:`admin `
+-------------------------------
+
+.. automodule:: userdbs.admin
+ :members:
+
+
+:py:mod:`models `
+---------------------------------
+
+.. automodule:: userdbs.models
+ :members:
diff --git a/docs/conf.py b/docs/conf.py
index ffdf565..998aeeb 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 -----------------------------------------------------
@@ -48,16 +53,16 @@ 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
# 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.
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
diff --git a/gnuviechadmin/gnuviechadmin/settings/base.py b/gnuviechadmin/gnuviechadmin/settings/base.py
index f05a821..33f5b85 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
@@ -224,9 +224,13 @@ DJANGO_APPS = (
# Apps specific for this project go here.
LOCAL_APPS = (
+ 'taskresults',
+ 'mysqltasks',
+ 'pgsqltasks',
'domains',
'osusers',
'managemails',
+ 'userdbs',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@@ -279,10 +283,11 @@ 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_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
diff --git a/gnuviechadmin/gvacommon/.gitignore b/gnuviechadmin/gvacommon/.gitignore
index 3bb2efd..5f1ace6 100644
--- a/gnuviechadmin/gvacommon/.gitignore
+++ b/gnuviechadmin/gvacommon/.gitignore
@@ -1,2 +1,3 @@
.*.swp
*.pyc
+.ropeproject/
diff --git a/gnuviechadmin/gvacommon/celeryrouters.py b/gnuviechadmin/gvacommon/celeryrouters.py
index e468813..ec7b122 100644
--- a/gnuviechadmin/gvacommon/celeryrouters.py
+++ b/gnuviechadmin/gvacommon/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
-
-
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/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/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 a46dbc5..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,22 +39,30 @@ 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(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
- fields = []
+ fields = ['customer']
def clean_password2(self):
"""
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')
@@ -51,8 +74,13 @@ 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'],
password=self.cleaned_data['password1'], commit=commit)
return user
@@ -60,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]
@@ -71,13 +105,20 @@ class UserAdmin(admin.ModelAdmin):
add_fieldsets = (
(None, {
'classes': ('wide',),
- 'fields': ('password1', 'password2')}),
+ 'fields': ('customer', 'password1', 'password2')}),
)
def get_form(self, request, obj=None, **kwargs):
"""
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:
@@ -89,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']
@@ -106,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..26f373d
--- /dev/null
+++ b/gnuviechadmin/osusers/apps.py
@@ -0,0 +1,17 @@
+"""
+This module contains the :py:class:`django.apps.AppConfig` instance for the
+:py:mod:`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/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"
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/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,
+ ),
+ ]
diff --git a/gnuviechadmin/osusers/models.py b/gnuviechadmin/osusers/models.py
index 93a4c3d..8b5edfe 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
@@ -16,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,
@@ -30,7 +36,7 @@ from .tasks import (
)
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _(
@@ -38,8 +44,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 +65,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 +88,76 @@ 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_ldap_group_if_empty.delay(self.groupname).get()
+ """
+ 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`
+
+ """
+ TaskResult.objects.create_task_result(
+ delete_ldap_group.delay(self.groupname),
+ 'delete_ldap_group'
+ )
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(
@@ -104,7 +167,26 @@ 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
+ ):
+ """
+ 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:
@@ -114,7 +196,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:
@@ -124,6 +206,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(
@@ -132,6 +218,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()
@@ -144,6 +231,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:
@@ -158,23 +254,56 @@ 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(
- "created user %(user)s with LDAP dn %(dn)s, home directory "
- "%(homedir)s and mail base directory %(maildir)s.", {
+ 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, 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)
@transaction.atomic
def delete(self, *args, **kwargs):
- delete_file_mail_userdir.delay(self.username).get()
- delete_file_sftp_userdir.delay(self.username).get()
+ """
+ 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`
+
+ """
+ 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()
@@ -184,9 +313,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,
@@ -200,6 +343,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(
@@ -242,11 +390,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)
@@ -255,21 +413,48 @@ 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):
- remove_ldap_user_from_group.delay(
- self.user.username, self.group.groupname).get()
- super(AdditionalGroup, self).delete(*args, **kwargs)
+ """
+ Delete the group assignment from LDAP and the database.
- def __str__(self):
- return '{0} in {1}'.format(self.user, self.group)
+ :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`
+ """
+ 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 99c7b1c..23cc612 100644
--- a/gnuviechadmin/osusers/tasks.py
+++ b/gnuviechadmin/osusers/tasks.py
@@ -1,3 +1,8 @@
+"""
+This module defines task stubs for the tasks implemented on the Celery
+workers.
+
+"""
from __future__ import absolute_import
from celery import shared_task
@@ -5,59 +10,194 @@ 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 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
+ :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
+
+ """
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..5aa40d6
--- /dev/null
+++ b/gnuviechadmin/pgsqltasks/tasks.py
@@ -0,0 +1,70 @@
+"""
+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):
+ """
+ This task deletes an existing PostgreSQL database.
+
+ :param str dbname: database name
+ :return: True if the database has been deleted, False otherwise
+ :rtype: boolean
+
+ """
diff --git a/gnuviechadmin/taskresults/__init__.py b/gnuviechadmin/taskresults/__init__.py
new file mode 100644
index 0000000..dace904
--- /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
diff --git a/gnuviechadmin/userdbs/__init__.py b/gnuviechadmin/userdbs/__init__.py
new file mode 100644
index 0000000..6f1b8ad
--- /dev/null
+++ 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
new file mode 100644
index 0000000..7a498c8
--- /dev/null
+++ b/gnuviechadmin/userdbs/admin.py
@@ -0,0 +1,279 @@
+"""
+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,
+ UserDatabase,
+)
+
+
+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/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')]),
+ ),
+ ]
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..c678a31
--- /dev/null
+++ b/gnuviechadmin/userdbs/models.py
@@ -0,0 +1,276 @@
+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_dbuser_name(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
+ 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 = dbuser_name_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: initial password or None
+ :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_dbuser_name(osuser, db_type)
+ db_user = DatabaseUser(
+ osuser=osuser, db_type=db_type, name=username)
+ if commit:
+ db_user.create_in_database(password=password)
+ 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.get_db_type_display(),
+ 'osuser': self.osuser.username,
+ }
+
+ 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:
+ 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(timeout=5)
+ elif self.db_type == DB_TYPES.mysql:
+ set_mysql_userpassword.delay(self.name, password).get(timeout=5)
+ 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`
+
+ """
+ for database in self.userdatabase_set.all():
+ database.delete()
+ if self.db_type == DB_TYPES.pgsql:
+ 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(propagate=False, timeout=5)
+ 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}_{{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(
+ '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()
+ 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'))
+
+ objects = UserDatabaseManager()
+
+ 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.
+
+ """
+ # 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:
+ 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_user.db_type == DB_TYPES.pgsql:
+ 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:
+ 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.