Mailbox tests passing

This commit is contained in:
Marc 2014-10-07 13:08:59 +00:00
parent 6240fa3139
commit 9082770642
34 changed files with 365 additions and 230 deletions

View File

@ -163,3 +163,6 @@ APPS app?
* pip upgrade or install
* disable account triggers save on cascade to execute backends save(update_field=[])

View File

@ -111,14 +111,15 @@ class AdminPasswordChangeForm(forms.Form):
if password:
self.user.set_password(password)
if commit:
self.user.save()
self.user.save(update_fields=['password'])
for ix, rel in enumerate(self.related):
password = self.cleaned_data['password1_%s' % ix]
if password:
print password
set_password = getattr(rel, 'set_password')
set_password(password)
if commit:
rel.save()
rel.save(update_fields=['password'])
return self.user
def _get_changed_data(self):

View File

@ -33,28 +33,28 @@ def get_modeladmin(model, import_module=True):
def insertattr(model, name, value, weight=0):
""" Inserts attribute to a modeladmin """
modeladmin = model
modeladmin_class = model
if models.Model in model.__mro__:
modeladmin = type(get_modeladmin(model))
modeladmin_class = type(get_modeladmin(model))
# Avoid inlines defined on parent class be shared between subclasses
# Seems that if we use tuples they are lost in some conditions like changing
# the tuple in modeladmin.__init__
if not getattr(modeladmin, name):
setattr(type(modeladmin), name, [])
if not getattr(modeladmin_class, name):
setattr(modeladmin_class, name, [])
inserted_attrs = getattr(modeladmin, '__inserted_attrs__', {})
inserted_attrs = getattr(modeladmin_class, '__inserted_attrs__', {})
if not name in inserted_attrs:
weights = {}
if hasattr(modeladmin, 'weights') and name in modeladmin.weights:
weights = modeladmin.weights.get(name)
if hasattr(modeladmin_class, 'weights') and name in modeladmin_class.weights:
weights = modeladmin_class.weights.get(name)
inserted_attrs[name] = [
(attr, weights.get(attr, 0)) for attr in getattr(modeladmin, name)
(attr, weights.get(attr, 0)) for attr in getattr(modeladmin_class, name)
]
inserted_attrs[name].append((value, weight))
inserted_attrs[name].sort(key=lambda a: a[1])
setattr(modeladmin, name, [ attr[0] for attr in inserted_attrs[name] ])
setattr(modeladmin, '__inserted_attrs__', inserted_attrs)
setattr(modeladmin_class, name, [ attr[0] for attr in inserted_attrs[name] ])
setattr(modeladmin_class, '__inserted_attrs__', inserted_attrs)
def wrap_admin_view(modeladmin, view):

View File

@ -9,10 +9,13 @@ class SetPasswordApiMixin(object):
@action(serializer_class=SetPasswordSerializer)
def set_password(self, request, pk):
obj = self.get_object()
serializer = SetPasswordSerializer(data=request.DATA)
data = request.DATA
if isinstance(data, basestring):
data = {'password': data}
serializer = SetPasswordSerializer(data=data)
if serializer.is_valid():
obj.set_password(serializer.data['password'])
obj.save()
obj.save(update_fields=['password'])
return Response({'status': 'password changed'})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -1,8 +1,9 @@
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import autodiscover_modules
from rest_framework.routers import DefaultRouter, Route, flatten, replace_methodname
from orchestra import settings
from orchestra.utils.apps import autodiscover as module_autodiscover
#from orchestra.utils.apps import autodiscover as module_autodiscover
from orchestra.utils.python import import_class
from .helpers import insert_links, replace_collectionmethodname
@ -99,16 +100,16 @@ class LinkHeaderRouter(DefaultRouter):
def insert(self, prefix_or_model, name, field, **kwargs):
""" Dynamically add new fields to an existing serializer """
viewset = self.get_viewset(prefix_or_model)
setattr(viewset, 'inserted', getattr(viewset, 'inserted', []))
# setattr(viewset, 'inserted', getattr(viewset, 'inserted', []))
if viewset.serializer_class is None:
viewset.serializer_class = viewset().get_serializer_class()
viewset.serializer_class.base_fields.update({name: field(**kwargs)})
if not name in viewset.inserted:
viewset.serializer_class.Meta.fields += (name,)
viewset.inserted.append(name)
# if not name in viewset.inserted:
viewset.serializer_class.Meta.fields += (name,)
# viewset.inserted.append(name)
# Create a router and register our viewsets with it.
router = LinkHeaderRouter()
autodiscover = lambda: (module_autodiscover('api'), module_autodiscover('serializers'))
autodiscover = lambda: (autodiscover_modules('api'), autodiscover_modules('serializers'))

View File

@ -1,6 +1,6 @@
from rest_framework import viewsets
from orchestra.api import router
from orchestra.api import router, SetPasswordApiMixin
from .models import Account
from .serializers import AccountSerializer
@ -12,7 +12,7 @@ class AccountApiMixin(object):
return qs.filter(account=self.request.user.pk)
class AccountViewSet(viewsets.ModelViewSet):
class AccountViewSet(SetPasswordApiMixin, viewsets.ModelViewSet):
model = Account
serializer_class = AccountSerializer
singleton_pk = lambda _,request: request.user.pk

View File

@ -42,7 +42,7 @@ class PermissionInline(AccountAdminMixin, admin.TabularInline):
""" Make value input widget bigger """
formfield = super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'database':
# Hack widget render in order to append ?account=id to the add url
# Hack widget render in order to append ?type='db_type' to the add url
db_type = self.parent_object.type
old_render = formfield.widget.render
def render(*args, **kwargs):

View File

@ -6,7 +6,7 @@ from orchestra.apps.resources import ServiceMonitor
from . import settings
class MySQLDBBackend(ServiceController):
class MySQLBackend(ServiceController):
verbose_name = "MySQL database"
model = 'databases.Database'

View File

@ -32,7 +32,7 @@ class Database(models.Model):
@property
def owner(self):
self.users.get(is_owner=True)
return self.roles.get(is_owner=True).user
class Role(models.Model):
@ -52,6 +52,11 @@ class Role(models.Model):
if self.user.type != self.database.type:
msg = _("Database and user type doesn't match")
raise validators.ValidationError(msg)
roles = self.database.roles.values('id')
print roles
if not roles or (len(roles) == 1 and roles[0].id == self.id):
print 'seld'
self.is_owner = True
class DatabaseUser(models.Model):

View File

@ -14,7 +14,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
fields = ('user', 'is_owner',)
class PermissionSerializer(serializers.HyperlinkedModelSerializer):
class RoleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Role
fields = ('database', 'is_owner',)
@ -32,9 +32,9 @@ class DatabaseUserSerializer(AccountSerializerMixin, serializers.HyperlinkedMode
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True,
widget=widgets.PasswordInput)
permission = PermissionSerializer(source='roles', many=True)
roles = RoleSerializer(many=True, read_only=True)
class Meta:
model = DatabaseUser
fields = ('url', 'username', 'password', 'type', 'permission')
fields = ('url', 'username', 'password', 'type', 'roles')
write_only_fields = ('username',)

View File

@ -1,4 +1,5 @@
#import MySQLdb
import MySQLdb
import os
from functools import partial
from django.conf import settings as djsettings
@ -9,21 +10,22 @@ from selenium.webdriver.support.select import Select
from orchestra.apps.accounts.models import Account
from orchestra.apps.orchestration.models import Server, Route
from orchestra.utils.system import run
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
snapshot_on_error)
from ... import backends, settings
from ...models import Database
class DatabaseTestMixin(object):
MASTER_ADDR = 'localhost'
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
DEPENDENCIES = (
'orchestra.apps.orchestration',
'orcgestra.apps.databases',
)
def setUp(self):
super(SystemUserMixin, self).setUp()
super(DatabaseTestMixin, self).setUp()
self.add_route()
djsettings.DEBUG = True
@ -49,28 +51,79 @@ class DatabaseTestMixin(object):
raise NotImplementedError
def test_add(self):
self.add()
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(dbname, username, password)
self.validate_create_table(dbname, username, password)
class MysqlBackendMixin(object):
class MySQLBackendMixin(object):
db_type = 'mysql'
def add_route(self):
server = Server.objects.create(name=self.MASTER_ADDR)
backend = backends.MysqlBackend.get_name()
Route.objects.create(backend=backend, match="database.type == 'mysql'", host=server)
server = Server.objects.create(name=self.MASTER_SERVER)
backend = backends.MySQLBackend.get_name()
match = "database.type == '%s'" % self.db_type
Route.objects.create(backend=backend, match=match, host=server)
match = "databaseuser.type == '%s'" % self.db_type
backend = backends.MySQLUserBackend.get_name()
Route.objects.create(backend=backend, match=match, host=server)
def validate_create_table(self, name, username, password):
db = MySQLdb.connect(host=self.MASTER_ADDR, user=username, passwd=password, db=name)
db = MySQLdb.connect(host=self.MASTER_SERVER, port=3306, user=username, passwd=password, db=name)
cur = db.cursor()
cur.execute('CREATE TABLE test;')
def validate_delete(self, name, username, password):
self.asseRaises(MySQLdb.ConnectionError,
MySQLdb.connect(host=self.MASTER_ADDR, user=username, passwd=password, db=name))
self.validate_create_table, name, username, password)
class RESTDatabaseTest(DatabaseTestMixin):
def add(self, dbname):
self.api.databases.create(name=dbname)
class RESTDatabaseMixin(DatabaseTestMixin):
def setUp(self):
super(RESTDatabaseMixin, self).setUp()
self.rest_login()
@save_response_on_error
def add(self, dbname, username, password):
user = self.rest.databaseusers.create(username=username, password=password)
self.rest.databases.create(name=dbname, user=user, type=self.db_type)
class AdminDatabaseMixin(DatabaseTestMixin):
def setUp(self):
super(AdminDatabaseMixin, self).setUp()
self.admin_login()
@snapshot_on_error
def add(self, dbname, username, password):
url = self.live_server_url + reverse('admin:databases_database_add')
self.selenium.get(url)
type_input = self.selenium.find_element_by_id('id_type')
type_select = Select(type_input)
type_select.select_by_value(self.db_type)
name_field = self.selenium.find_element_by_id('id_name')
name_field.send_keys(dbname)
username_field = self.selenium.find_element_by_id('id_username')
username_field.send_keys(username)
password_field = self.selenium.find_element_by_id('id_password1')
password_field.send_keys(password)
password_field = self.selenium.find_element_by_id('id_password2')
password_field.send_keys(password)
name_field.submit()
self.assertNotEqual(url, self.selenium.current_url)
class RESTMysqlDatabaseTest(MySQLBackendMixin, RESTDatabaseMixin, BaseLiveServerTestCase):
pass
class AdminMysqlDatabaseTest(MySQLBackendMixin, AdminDatabaseMixin, BaseLiveServerTestCase):
pass

View File

@ -257,12 +257,7 @@ class AdminDomainMixin(DomainTestMixin):
@snapshot_on_error
def delete(self, domain_name):
domain = Domain.objects.get(name=domain_name)
delete = reverse('admin:domains_domain_delete', args=(domain.pk,))
url = self.live_server_url + delete
self.selenium.get(url)
form = self.selenium.find_element_by_name('post')
form.submit()
self.assertNotEqual(url, self.selenium.current_url)
self.admin_delete(domain)
@snapshot_on_error
def update(self, domain_name, records):

View File

@ -43,7 +43,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('account_link', 'name', 'password'),
'fields': ('name', 'password', 'is_active', 'account_link'),
}),
(_("Filtering"), {
'classes': ('collapse',),

View File

@ -1,6 +1,6 @@
from rest_framework import viewsets
from orchestra.api import router
from orchestra.api import router, SetPasswordApiMixin
from orchestra.apps.accounts.api import AccountApiMixin
from .models import Address, Mailbox
@ -13,7 +13,7 @@ class AddressViewSet(AccountApiMixin, viewsets.ModelViewSet):
class MailboxViewSet(AccountApiMixin, viewsets.ModelViewSet):
class MailboxViewSet(SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Mailbox
serializer_class = MailboxSerializer

View File

@ -28,7 +28,7 @@ class PasswdVirtualUserBackend(ServiceController):
def set_user(self, context):
self.append(textwrap.dedent("""
if [[ $( grep "^%(username)s:" %(passwd_path)s ) ]]; then
sed -i "s/^%(username)s:.*/%(passwd)s/" %(passwd_path)s
sed -i 's#^%(username)s:.*#%(passwd)s#' %(passwd_path)s
else
echo '%(passwd)s' >> %(passwd_path)s
fi""" % context
@ -49,22 +49,6 @@ class PasswdVirtualUserBackend(ServiceController):
context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve')
self.append("echo '%(filtering)s' > %(filter_path)s" % context)
def set_quota(self, mailbox, context):
if not hasattr(mailbox, 'resources'):
return
context.update({
'maildir_path': '~%(username)s/Maildir' % context,
'maildirsize_path': '~%(username)s/Maildir/maildirsize' % context,
'quota': mailbox.resources.disk.allocated*1000*1000,
})
self.append("mkdir -p %(maildir_path)s" % context)
self.append(textwrap.dedent("""
sed -i '1s/.*/%(quota)s,S/' %(maildirsize_path)s || {
echo '%(quota)s,S' > %(maildirsize_path)s &&
chown %(username)s %(maildirsize_path)s;
}""" % context
))
def save(self, mailbox):
context = self.get_context(mailbox)
self.set_user(context)
@ -73,9 +57,11 @@ class PasswdVirtualUserBackend(ServiceController):
def delete(self, mailbox):
context = self.get_context(mailbox)
self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context)
self.append("killall -u %(uid)s" % context)
self.append("killall -u %(uid)s || true" % context)
self.append("sed -i '/^%(username)s:.*/d' %(passwd_path)s" % context)
self.append("rm -fr %(home)s" % context)
# TODO delete
context['deleted'] = context['home'].rstrip('/') + '.deleted'
self.append("mv %(home)s %(deleted)s" % context)
def get_extra_fields(self, mailbox, context):
context['quota'] = self.get_quota(mailbox)

View File

@ -36,7 +36,10 @@ class Mailbox(models.Model):
@cached_property
def active(self):
return self.is_active and self.account.is_active
try:
return self.is_active and self.account.is_active
except type(self).account.field.rel.to.DoesNotExist:
return self.is_active
def set_password(self, raw_password):
self.password = make_password(raw_password)

View File

@ -9,7 +9,7 @@ class MailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
class Meta:
model = Mailbox
# TODO 'use_custom_filtering',
fields = ('url', 'name', 'password', 'custom_filtering', 'addresses')
fields = ('url', 'name', 'password', 'custom_filtering', 'addresses', 'is_active')
def validate_password(self, attrs, source):
""" POST only password """
@ -44,6 +44,6 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
def validate(self, attrs):
if not attrs['mailboxes'] and not attrs['forward']:
raise serializers.ValidationError("mailboxes or forward should be provided")
raise serializers.ValidationError("mailboxes or forward addresses should be provided")
return attrs

View File

@ -20,31 +20,8 @@ from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot
from ... import backends, settings
from ...models import Mailbox
#>>> mail.list()
#('OK', ['(\\HasNoChildren) "." INBOX'])
#>>> mail.select('INBOX')
#('OK', ['18'])
#>>> mail.getquota('INBOX')
#imaplib.error: GETQUOTA command error: BAD ['Error in IMAP command GETQUOTA: Unknown command.']
#mail.fetch(10, '(RFC822)')
#('OK', [('10 (FLAGS (\\Seen) RFC822 {550}', 'Return-Path: <root@test3.orchestra.lan>\r\nDelivered-To: <rata@orchestra.lan>\r\nReceived: from test3.orchestra.lan\r\n\tby test3.orchestra.lan (Dovecot) with LMTP id hvDUEAIKL1QlOQAAL4hJug\r\n\tfor <rata@orchestra.lan>; Fri, 03 Oct 2014 16:41:38 -0400\r\nReceived: by test3.orchestra.lan (Postfix, from userid 0)\r\n\tid 43BB1F94633; Fri, 3 Oct 2014 16:41:38 -0400 (EDT)\r\nTo: rata@orchestra.lan\r\nSubject: hola\r\nMessage-Id: <20141003204138.43BB1F94633@test3.orchestra.lan>\r\nDate: Fri, 3 Oct 2014 16:41:38 -0400 (EDT)\r\nFrom: root@test3.orchestra.lan (root)\r\n\r\n\r\n\r\n'), ')'])
#>>> mail.close()
#('OK', ['Close completed.'])
#pop = poplib.POP3('localhost')
#pop.user('rata')
#pop.pass_('3')
#>>> pop.list()
#('+OK 18 messages:', ['1 552', '2 550', '3 550', '4 548', '5 546', '6 546', '7 554', '8 548', '9 550', '10 550', '11 546', '12 546', '13 546', '14 544', '15 548', '16 577', '17 546', '18 546'], 135)
#>>> pop.quit()
#'+OK Logging out.'
# FIXME django load production database at the begining of tests
class MailboxMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
DEPENDENCIES = (
@ -56,7 +33,10 @@ class MailboxMixin(object):
def setUp(self):
super(MailboxMixin, self).setUp()
self.add_route()
# apps.get_app_config('resources').reload_relations() doesn't work
# TODO fix this
from django.apps import apps
# clean resource relation from other tests
apps.get_app_config('resources').reload_relations()
djsettings.DEBUG = True
def add_route(self):
@ -96,27 +76,6 @@ class MailboxMixin(object):
def add_group(self, username, groupname):
raise NotImplementedError
def validate_user(self, username):
idcmd = sshr(self.MASTER_SERVER, "id %s" % username)
self.assertEqual(0, idcmd.return_code)
user = SystemUser.objects.get(username=username)
groups = list(user.groups.values_list('username', flat=True))
groups.append(user.username)
idgroups = idcmd.stdout.strip().split(' ')[2]
idgroups = re.findall(r'\d+\((\w+)\)', idgroups)
self.assertEqual(set(groups), set(idgroups))
def validate_delete(self, username):
self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER,'id %s' % username, display=False)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/groups' % username, display=False)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/passwd' % username, display=False)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/shadow' % username, display=False)
def login_imap(self, username, password):
mail = imaplib.IMAP4_SSL(self.MASTER_SERVER)
status, msg = mail.login(username, password)
@ -144,6 +103,9 @@ class MailboxMixin(object):
finally:
server.quit()
def validate_mailbox(self, username):
sshrun(self.MASTER_SERVER, "doveadm search -u %s ALL" % username, display=False)
def validate_email(self, username, token):
home = Mailbox.objects.get(name=username).get_home()
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
@ -152,14 +114,15 @@ class MailboxMixin(object):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
imap = self.login_imap(username, password)
self.validate_mailbox(username)
def test_change_password(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
imap = self.login_imap(username, password)
new_password = '@!?%spppP001' % random_ascii(5)
self.change_password(username, new_password)
@ -171,7 +134,7 @@ class MailboxMixin(object):
self.add_quota_resource()
quota = 100
self.add(username, password, quota=quota)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
get_quota = "doveadm quota get -u %s 2>&1|grep STORAGE|awk {'print $5'}" % username
stdout = sshrun(self.MASTER_SERVER, get_quota, display=False).stdout
self.assertEqual(quota*1024, int(stdout))
@ -183,7 +146,7 @@ class MailboxMixin(object):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
msg = MIMEText("Hola bishuns")
msg['To'] = 'noexists@example.com'
msg['From'] = '%s@%s' % (username, self.MASTER_SERVER)
@ -199,7 +162,7 @@ class MailboxMixin(object):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
domain = '%s_domain.lan' % random_ascii(5)
name = '%s_name' % random_ascii(5)
domain = self.account.domains.create(name=domain)
@ -207,6 +170,47 @@ class MailboxMixin(object):
token = random_ascii(100)
self.send_email("%s@%s" % (name, domain), token)
self.validate_email(username, token)
def test_disable(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.validate_mailbox(username)
self.addCleanup(self.delete, username)
imap = self.login_imap(username, password)
self.disable(username)
self.assertRaises(imap.error, self.login_imap, username, password)
def test_delete(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%sppppP001' % random_ascii(5)
self.add(username, password)
imap = self.login_imap(username, password)
self.validate_mailbox(username)
mailbox = Mailbox.objects.get(name=username)
home = mailbox.get_home()
self.delete(username)
self.assertRaises(Mailbox.DoesNotExist, Mailbox.objects.get, name=username)
self.assertRaises(CommandError, self.validate_mailbox, username)
self.assertRaises(imap.error, self.login_imap, username, password)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False)
def test_delete_address(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(self.delete, username)
domain = '%s_domain.lan' % random_ascii(5)
name = '%s_name' % random_ascii(5)
domain = self.account.domains.create(name=domain)
self.add_address(username, name, domain)
token = random_ascii(100)
self.send_email("%s@%s" % (name, domain), token)
self.validate_email(username, token)
self.delete_address(username)
self.send_email("%s@%s" % (name, domain), token)
self.validate_email(username, token)
class RESTMailboxMixin(MailboxMixin):
@ -243,7 +247,21 @@ class RESTMailboxMixin(MailboxMixin):
mailbox = self.rest.mailboxes.retrieve(name=username).get()
domain = self.rest.domains.retrieve(name=domain.name).get()
self.rest.addresses.create(name=name, domain=domain, mailboxes=[mailbox])
@save_response_on_error
def delete_address(self, username):
mailbox = self.rest.mailboxes.retrieve(name=username).get()
self.rest.addresses.delete()
@save_response_on_error
def change_password(self, username, password):
mailbox = self.rest.mailboxes.retrieve(name=username).get()
mailbox.set_password(password=password)
@save_response_on_error
def disable(self, username):
mailbox = self.rest.mailboxes.retrieve(name=username).get()
mailbox.update(is_active=False)
class AdminMailboxMixin(MailboxMixin):
@ -267,8 +285,12 @@ class AdminMailboxMixin(MailboxMixin):
password_field.send_keys(password)
password_field = self.selenium.find_element_by_id('id_password2')
password_field.send_keys(password)
if quota is not None:
from orchestra.admin.utils import get_modeladmin
m = get_modeladmin(Mailbox)
print 't', type(m).inlines
print 'm', m.inlines
self.take_screenshot()
quota_field = self.selenium.find_element_by_id(
'id_resources-resourcedata-content_type-object_id-0-allocated')
quota_field.clear()
@ -280,27 +302,12 @@ class AdminMailboxMixin(MailboxMixin):
@snapshot_on_error
def delete(self, username):
mailbox = Mailbox.objects.get(name=username)
delete = reverse('admin:mails_mailbox_delete', args=(mailbox.pk,))
url = self.live_server_url + delete
self.selenium.get(url)
confirmation = self.selenium.find_element_by_name('post')
confirmation.submit()
self.assertNotEqual(url, self.selenium.current_url)
self.admin_delete(mailbox)
@snapshot_on_error
def change_password(self, username, password):
mailbox = Mailbox.objects.get(name=username)
change_password = reverse('admin:mails_mailbox_change_password', args=(mailbox.pk,))
url = self.live_server_url + change_password
self.selenium.get(url)
password_field = self.selenium.find_element_by_id('id_password1')
password_field.send_keys(password)
password_field = self.selenium.find_element_by_id('id_password2')
password_field.send_keys(password)
password_field.submit()
self.assertNotEqual(url, self.selenium.current_url)
self.admin_change_password(mailbox, password)
@snapshot_on_error
def add_address(self, username, name, domain):
@ -320,6 +327,18 @@ class AdminMailboxMixin(MailboxMixin):
name_field.submit()
self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def delete_address(self, username):
mailbox = Mailbox.objects.get(name=username)
address = mailbox.addresses.get()
self.admin_delete(address)
@snapshot_on_error
def disable(self, username):
mailbox = Mailbox.objects.get(name=username)
self.admin_disable(mailbox)
class RESTMailboxTest(RESTMailboxMixin, BaseLiveServerTestCase):
pass

View File

@ -3,11 +3,12 @@ import socket
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _
from orchestra.core.validators import validate_ip_address, ValidationError
from orchestra.models.fields import NullableCharField
from orchestra.utils.apps import autodiscover
#from orchestra.utils.apps import autodiscover
from . import settings, manager
from .backends import ServiceBackend
@ -133,7 +134,7 @@ class BackendOperation(models.Model):
return ServiceBackend.get_backend(self.backend)
autodiscover('backends')
autodiscover_modules('backends')
class Route(models.Model):

View File

@ -132,23 +132,26 @@ def resource_inline_factory(resources):
def has_add_permission(self, *args, **kwargs):
""" Hidde add another """
return False
return ResourceInline
from orchestra.utils import database_ready
def insert_resource_inlines():
# Clean previous state
for related in Resource._related:
modeladmin = get_modeladmin(related)
modeladmin_class = type(modeladmin)
for inline in getattr(modeladmin_class, 'inlines', []):
if inline.__name__ == 'ResourceInline':
modeladmin_class.inlines.remove(inline)
modeladmin.inlines = modeladmin_class.inlines
for ct, resources in Resource.objects.group_by('content_type').iteritems():
inline = resource_inline_factory(resources)
model = ct.model_class()
modeladmin = get_modeladmin(model)
inserted = False
inlines = []
for existing in getattr(modeladmin, 'inlines', []):
if type(inline) == type(existing):
existing = inline
inserted = True
inlines.append(existing)
if inserted:
modeladmin.inlines = inlines
else:
insertattr(model, 'inlines', inline)
insertattr(model, 'inlines', inline)
modeladmin.inlines = type(modeladmin).inlines
if database_ready():
insert_resource_inlines()

View File

@ -9,15 +9,13 @@ class ResourcesConfig(AppConfig):
def ready(self):
if database_ready():
from .admin import insert_resource_inlines
from .models import create_resource_relation
create_resource_relation()
insert_resource_inlines()
def reload_relations(self):
from .admin import insert_resource_inlines
from .models import create_resource_relation
from .serializers import insert_resource_serializers
create_resource_relation()
insert_resource_inlines()
insert_resource_serializers()
create_resource_relation()

View File

@ -31,6 +31,7 @@ class Resource(models.Model):
(MONTHLY_SUM, _("Monthly Sum")),
(MONTHLY_AVG, _("Monthly Average")),
)
_related = set() # keeps track of related models for resource cleanup
name = models.CharField(_("name"), max_length=32,
help_text=_('Required. 32 characters or fewer. Lowercase letters, '
@ -102,6 +103,7 @@ class Resource(models.Model):
task.save(update_fields=['crontab'])
if created:
# This only work on tests because of multiprocessing used on real deployments
print 'saved'
apps.get_app_config('resources').reload_relations()
def delete(self, *args, **kwargs):
@ -192,8 +194,21 @@ def create_resource_relation():
self.obj = obj
return self
# Clean previous state
for related in Resource._related:
try:
delattr(related, 'resource_set')
delattr(related, 'resources')
except AttributeError:
pass
else:
related._meta.virtual_fields = [
field for field in related._meta.virtual_fields if field.rel.to != ResourceData
]
relation = GenericRelation('resources.ResourceData')
for ct, resources in Resource.objects.group_by('content_type').iteritems():
model = ct.model_class()
model.add_to_class('resource_set', relation)
model.resources = ResourceHandler()
Resource._related.add(model)

View File

@ -30,6 +30,15 @@ class ResourceSerializer(serializers.ModelSerializer):
# Monkey-patching section
def insert_resource_serializers():
# clean previous state
for related in Resource._related:
viewset = router.get_viewset(related)
fields = list(viewset.serializer_class.Meta.fields)
try:
fields.remove('resources')
except ValueError:
pass
viewset.serializer_class.Meta.fields = fields
# Create nested serializers on target models
for ct, resources in Resource.objects.group_by('content_type').iteritems():
model = ct.model_class()

View File

@ -6,11 +6,12 @@ from django.db.models.loading import get_model
from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.utils.functional import cached_property
from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches, services, accounts
from orchestra.models import queryset
from orchestra.utils.apps import autodiscover
#from orchestra.utils.apps import autodiscover
from . import settings, rating
from .handlers import ServiceHandler
@ -70,7 +71,7 @@ class Rate(models.Model):
return "{}-{}".format(str(self.price), self.quantity)
autodiscover('handlers')
autodiscover_modules('handlers')
class Service(models.Model):

View File

@ -37,7 +37,7 @@ class SystemUserBackend(ServiceController):
self.append("groupdel %(username)s || true" % context)
if user.is_main:
# TODO delete instead of this shit
context['deleted'] = context['home'][:-1]+'.deleted'
context['deleted'] = context['home'].rstrip('/') + '.deleted'
self.append("mv %(home)s %(deleted)s" % context)
def get_groups(self, user):

View File

@ -48,7 +48,6 @@ class SystemUser(models.Model):
@cached_property
def active(self):
a = type(self).account.field.model
try:
return self.is_active and self.account.is_active
except type(self).account.field.rel.to.DoesNotExist:

View File

@ -9,11 +9,20 @@ from orchestra.core.validators import validate_password
from .models import SystemUser
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = ('username',)
def from_native(self, data, files=None):
return SystemUser.objects.get(username=data['username'])
class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False,
widget=widgets.PasswordInput)
groups = serializers.RelatedField(many=True)
groups = GroupSerializer(many=True, allow_add_remove=True, required=False)
class Meta:
model = SystemUser
@ -30,6 +39,8 @@ class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelS
raise serializers.ValidationError(_("Password required"))
return attrs
# TODO validate gruops != self
def save_object(self, obj, **kwargs):
# FIXME this method will be called when saving nested serializers :(
if not obj.pk:

View File

@ -13,7 +13,8 @@ from selenium.webdriver.support.select import Select
from orchestra.apps.accounts.models import Account
from orchestra.apps.orchestration.models import Server, Route
from orchestra.utils.system import run, sshrun
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot_on_error
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, snapshot_on_error,
save_response_on_error)
from ... import backends, settings
from ...models import SystemUser
@ -71,13 +72,14 @@ class SystemUserMixin(object):
def validate_delete(self, username):
self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER,'id %s' % username, display=False)
sshrun, self.MASTER_SERVER, 'id %s' % username, display=False)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/groups' % username, display=False)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/passwd' % username, display=False)
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/shadow' % username, display=False)
# Home will be deleted on account delete, see test_delete_account
def validate_ftp(self, username, password):
connection = ftplib.FTP(self.MASTER_SERVER)
@ -105,22 +107,24 @@ class SystemUserMixin(object):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
self.validate_user(username)
def test_ftp(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password, shell='/dev/null')
self.addCleanup(partial(self.delete, username))
self.assertRaises(paramiko.AuthenticationException, self.validate_sftp, username, password)
self.assertRaises(paramiko.AuthenticationException, self.validate_ssh, username, password)
self.addCleanup(self.delete, username)
self.assertRaises(paramiko.AuthenticationException,
self.validate_sftp, username, password)
self.assertRaises(paramiko.AuthenticationException,
self.validate_ssh, username, password)
def test_sftp(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password, shell='/bin/rssh')
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
self.validate_sftp(username, password)
self.assertRaises(AssertionError, self.validate_ssh, username, password)
@ -128,7 +132,7 @@ class SystemUserMixin(object):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password, shell='/bin/bash')
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
self.validate_ssh(username, password)
def test_delete(self):
@ -143,12 +147,12 @@ class SystemUserMixin(object):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
self.validate_user(username)
username2 = '%s_systemuser' % random_ascii(10)
password2 = '@!?%spppP001' % random_ascii(5)
self.add(username2, password2)
self.addCleanup(partial(self.delete, username2))
self.addCleanup(self.delete, username2)
self.validate_user(username2)
self.add_group(username, username2)
user = SystemUser.objects.get(username=username)
@ -160,7 +164,7 @@ class SystemUserMixin(object):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password, shell='/dev/null')
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
self.validate_ftp(username, password)
self.disable(username)
self.validate_user(username)
@ -170,7 +174,7 @@ class SystemUserMixin(object):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.addCleanup(self.delete, username)
self.validate_ftp(username, password)
new_password = '@!?%spppP001' % random_ascii(5)
self.change_password(username, new_password)
@ -185,33 +189,38 @@ class RESTSystemUserMixin(SystemUserMixin):
self.rest_login()
# create main user
self.save(self.account.username)
self.addCleanup(partial(self.delete, self.account.username))
self.addCleanup(self.delete, self.account.username)
@save_response_on_error
def add(self, username, password, shell='/dev/null'):
self.rest.systemusers.create(username=username, password=password, shell=shell)
@save_response_on_error
def delete(self, username):
user = self.rest.systemusers.retrieve(username=username).get()
user.delete()
@save_response_on_error
def add_group(self, username, groupname):
user = self.rest.systemusers.retrieve(username=username).get()
group = self.rest.systemusers.retrieve(username=groupname).get()
user.groups.append(group) # TODO
user.groups.append({'username': groupname})
user.save()
@save_response_on_error
def disable(self, username):
user = self.rest.systemusers.retrieve(username=username).get()
user.is_active = False
user.save()
@save_response_on_error
def save(self, username):
user = self.rest.systemusers.retrieve(username=username).get()
user.save()
@save_response_on_error
def change_password(self, username, password):
user = self.rest.systemusers.retrieve(username=username).get()
user.change_password(password)
user.set_password(password)
class AdminSystemUserMixin(SystemUserMixin):
@ -220,7 +229,7 @@ class AdminSystemUserMixin(SystemUserMixin):
self.admin_login()
# create main user
self.save(self.account.username)
self.addCleanup(partial(self.delete, self.account.username))
self.addCleanup(self.delete, self.account.username)
@snapshot_on_error
def add(self, username, password, shell='/dev/null'):
@ -249,24 +258,12 @@ class AdminSystemUserMixin(SystemUserMixin):
@snapshot_on_error
def delete(self, username):
user = SystemUser.objects.get(username=username)
delete = reverse('admin:systemusers_systemuser_delete', args=(user.pk,))
url = self.live_server_url + delete
self.selenium.get(url)
confirmation = self.selenium.find_element_by_name('post')
confirmation.submit()
self.assertNotEqual(url, self.selenium.current_url)
self.admin_delete(user)
@snapshot_on_error
def disable(self, username):
user = SystemUser.objects.get(username=username)
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
url = self.live_server_url + change
self.selenium.get(url)
is_active = self.selenium.find_element_by_id('id_is_active')
is_active.click()
save = self.selenium.find_element_by_name('_save')
save.submit()
self.assertNotEqual(url, self.selenium.current_url)
self.admin_disable(user)
@snapshot_on_error
def add_group(self, username, groupname):
@ -294,17 +291,8 @@ class AdminSystemUserMixin(SystemUserMixin):
@snapshot_on_error
def change_password(self, username, password):
user = SystemUser.objects.get(username=username)
change_password = reverse('admin:systemusers_systemuser_change_password', args=(user.pk,))
url = self.live_server_url + change_password
self.selenium.get(url)
password_field = self.selenium.find_element_by_id('id_password1')
password_field.send_keys(password)
password_field = self.selenium.find_element_by_id('id_password2')
password_field.send_keys(password)
password_field.submit()
self.assertNotEqual(url, self.selenium.current_url)
self.admin_change_password(user, password)
class RESTSystemUserTest(RESTSystemUserMixin, BaseLiveServerTestCase):
pass
@ -337,10 +325,10 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
email = self.selenium.find_element_by_id('id_contacts-0-email')
email.send_keys(account_email)
email.submit()
self.assertNotEqual(url, self.selenium.current_url)
account = Account.objects.get(username=account_username)
self.addCleanup(account.delete)
self.assertNotEqual(url, self.selenium.current_url)
self.addCleanup(self.delete, account_username)
self.assertEqual(0, sshr(self.MASTER_SERVER, "id %s" % account.username).return_code)
@snapshot_on_error

View File

@ -130,7 +130,8 @@ function install_requirements () {
libxml2-dev \
libxslt1-dev \
wkhtmltopdf \
xvfb"
xvfb \
python-mysqldb"
PIP="django==1.7 \
django-celery-email==1.0.4 \

View File

@ -107,7 +107,7 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'orchestra.apps.accounts',
'orchestra.apps.contacts',

View File

@ -25,9 +25,9 @@ urlpatterns = patterns('',
url(r'^media/(?P<path>.*)$', 'django.views.static.serve',
{'document_root': settings.MEDIA_ROOT, 'show_indexes': True}
)
)
if settings.DEBUG:
import debug_toolbar
urlpatterns += patterns('',

View File

@ -1,20 +1,21 @@
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
#from django.utils.importlib import import_module
#from django.utils.module_loading import module_has_submodule
def autodiscover(module):
""" Auto-discover INSTALLED_APPS module.py """
from django.conf import settings
for app in settings.INSTALLED_APPS:
mod = import_module(app)
try:
import_module('%s.%s' % (app, module))
except ImportError:
# Decide whether to bubble up this error. If the app just
# doesn't have the module, we can ignore the error
# attempting to import it, otherwise we want it to bubble up.
if module_has_submodule(mod, module):
print '%s module caused this error:' % module
raise
#def autodiscover(module):
# """ Auto-discover INSTALLED_APPS module.py """
# from django.conf import settings
# for app in settings.INSTALLED_APPS:
# mod = import_module(app)
# try:
# import_module('%s.%s' % (app, module))
# except ImportError:
# # Decide whether to bubble up this error. If the app just
# # doesn't have the module, we can ignore the error
# # attempting to import it, otherwise we want it to bubble up.
# if module_has_submodule(mod, module):
# print '%s module caused this error:' % module
# raise
def isinstalled(app):
""" returns True if app is installed """

View File

@ -7,6 +7,7 @@ from functools import wraps
from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model
from django.contrib.sessions.backends.db import SessionStore
from django.core.urlresolvers import reverse
from django.test import LiveServerTestCase, TestCase
from orm.api import Api
from selenium.webdriver.firefox.webdriver import WebDriver
@ -115,7 +116,43 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
filename = 'screenshot_%s_%s.png' % (self.id(), timestamp)
path = '/home/orchestra/snapshots'
self.selenium.save_screenshot(os.path.join(path, filename))
def admin_delete(self, obj):
opts = obj._meta
app_label, model_name = opts.app_label, opts.model_name
delete = reverse('admin:%s_%s_delete' % (app_label, model_name), args=(obj.pk,))
url = self.live_server_url + delete
self.selenium.get(url)
confirmation = self.selenium.find_element_by_name('post')
confirmation.submit()
self.assertNotEqual(url, self.selenium.current_url)
def admin_disable(self, obj):
opts = obj._meta
app_label, model_name = opts.app_label, opts.model_name
change = reverse('admin:%s_%s_change' % (app_label, model_name), args=(obj.pk,))
url = self.live_server_url + change
self.selenium.get(url)
is_active = self.selenium.find_element_by_id('id_is_active')
is_active.click()
save = self.selenium.find_element_by_name('_save')
save.submit()
self.assertNotEqual(url, self.selenium.current_url)
def admin_change_password(self, obj, password):
opts = obj._meta
app_label, model_name = opts.app_label, opts.model_name
change_password = reverse('admin:%s_%s_change_password' % (app_label, model_name), args=(obj.pk,))
url = self.live_server_url + change_password
self.selenium.get(url)
password_field = self.selenium.find_element_by_id('id_password1')
password_field.send_keys(password)
password_field = self.selenium.find_element_by_id('id_password2')
password_field.send_keys(password)
password_field.submit()
self.assertNotEqual(url, self.selenium.current_url)
def snapshot_on_error(test):
@wraps(test)

View File

@ -2,3 +2,5 @@ MySQL
=====
apt-get install mysql-server
sed -i "s/bind-address = 127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf