domains app functional tests passing

This commit is contained in:
Marc 2014-10-03 14:02:11 +00:00
parent 56ee1ba4a3
commit 9ecfc8d4dd
25 changed files with 322 additions and 136 deletions

View File

@ -59,7 +59,8 @@ Note `*` _for sustancial progress_
1. [ ] Integration with third-party service providers, e.g. Gandi 1. [ ] Integration with third-party service providers, e.g. Gandi
2. [ ] Scheduling of service cancellations and deactivations 2. [ ] Scheduling of service cancellations and deactivations
1. [ ] Object level permissions system 1. [ ] Object-level permission system
2. [ ] REST API for superusers 2. [ ] REST API functionality for superusers
3. [ ] Responsive user interface 3. [ ] Responsive user interface, based on a JS framework.
4. [ ] Full documentation 4. [ ] Full documentation
5. [ ] [http://www.ansible.com/home](Ansible) orchestration method, which synchronize the whole service config everytime instead of incremental changes.

15
TODO.md
View File

@ -141,7 +141,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Redirect junk emails and delete every 30 days? * Redirect junk emails and delete every 30 days?
* Complitely decouples scripts execution, billing, service definition * DOC: Complitely decouples scripts execution, billing, service definition
* Create SystemUser on account creation. username=username, is_main=True, * Create SystemUser on account creation. username=username, is_main=True,
* Exclude is_main=True from queryset filter default is_main=False * Exclude is_main=True from queryset filter default is_main=False
@ -149,8 +149,15 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Unify all users * Unify all users
* backend message with link * backend admin message with link
* test fucking user
* delete main user -> delete account or prevent delete main user * delete main user -> delete account or prevent delete main user
APPS app?
* https://blog.flameeyes.eu/2011/01/mostly-unknown-openssh-tricks
* Ansible orchestration *method* (methods.py)
* interdependency user <-> account with the old usermodel

View File

@ -127,6 +127,7 @@ class AccountAdminMixin(object):
filter_by_account_fields = [] filter_by_account_fields = []
change_list_template = 'admin/accounts/account/change_list.html' change_list_template = 'admin/accounts/account/change_list.html'
change_form_template = 'admin/accounts/account/change_form.html' change_form_template = 'admin/accounts/account/change_form.html'
account = None
def account_link(self, instance): def account_link(self, instance):
account = instance.account if instance.pk else self.account account = instance.account if instance.pk else self.account
@ -151,7 +152,7 @@ class AccountAdminMixin(object):
""" Filter by account """ """ Filter by account """
formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs) formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name in self.filter_by_account_fields: if db_field.name in self.filter_by_account_fields:
if hasattr(self, 'account'): if self.account:
# Hack widget render in order to append ?account=id to the add url # Hack widget render in order to append ?account=id to the add url
old_render = formfield.widget.render old_render = formfield.widget.render
def render(*args, **kwargs): def render(*args, **kwargs):
@ -161,6 +162,11 @@ class AccountAdminMixin(object):
formfield.widget.render = render formfield.widget.render = render
# Filter related object by account # Filter related object by account
formfield.queryset = formfield.queryset.filter(account=self.account) formfield.queryset = formfield.queryset.filter(account=self.account)
elif db_field.name == 'account':
if self.account:
formfield.initial = self.account.pk
elif Account.objects.count() == 1:
formfield.initial = 1
return formfield return formfield
def get_account_from_preserve_filters(self, request): def get_account_from_preserve_filters(self, request):
@ -215,7 +221,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
""" Provides support for accounts on ModelAdmin """ """ Provides support for accounts on ModelAdmin """
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj) inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj)
if hasattr(self, 'account'): if self.account:
account = self.account account = self.account
else: else:
account = Account.objects.get(pk=request.GET['account']) account = Account.objects.get(pk=request.GET['account'])

View File

@ -0,0 +1,76 @@
import MySQLdb
from functools import partial
from django.conf import settings as djsettings
from django.core.management.base import CommandError
from django.core.urlresolvers import reverse
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 ... import backends, settings
from ...models import Satabase
class DatabaseTestMixin(object):
MASTER_ADDR = 'localhost'
DEPENDENCIES = (
'orchestra.apps.orchestration',
'orcgestra.apps.databases',
)
def setUp(self):
super(SystemUserMixin, self).setUp()
self.add_route()
djsettings.DEBUG = True
def add_route(self):
raise NotImplementedError
def save(self):
raise NotImplementedError
def add(self):
raise NotImplementedError
def delete(self):
raise NotImplementedError
def update(self):
raise NotImplementedError
def disable(self):
raise NotImplementedError
def add_group(self, username, groupname):
raise NotImplementedError
def test_add(self):
self.add()
class MysqlBackendMixin(object):
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)
def validate_create_table(self, name, username, password):
db = MySQLdb.connect(host=self.MASTER_ADDR, 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))
class RESTDatabaseTest(DatabaseTestMixin):
def add(self, dbname):
self.api.databases.create(name=dbname)

View File

@ -33,7 +33,7 @@ class Bind9MasterDomainBackend(ServiceController):
" { echo -e '%(conf)s' >> %(conf_path)s; UPDATED=1; }" % context) " { echo -e '%(conf)s' >> %(conf_path)s; UPDATED=1; }" % context)
for subdomain in context['subdomains']: for subdomain in context['subdomains']:
context['name'] = subdomain.name context['name'] = subdomain.name
self.delete_conf(context) self.delete(subdomain)
def delete(self, domain): def delete(self, domain):
context = self.get_context(domain) context = self.get_context(domain)
@ -56,7 +56,7 @@ class Bind9MasterDomainBackend(ServiceController):
context = { context = {
'name': domain.name, 'name': domain.name,
'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name}, 'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name},
'subdomains': domain.get_subdomains(), 'subdomains': domain.subdomains.all(),
'banner': self.get_banner(), 'banner': self.get_banner(),
} }
context.update({ context.update({
@ -92,7 +92,7 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
context = { context = {
'name': domain.name, 'name': domain.name,
'masters': '; '.join(settings.DOMAINS_MASTERS), 'masters': '; '.join(settings.DOMAINS_MASTERS),
'subdomains': domain.get_subdomains() 'subdomains': domain.subdomains.all()
} }
context.update({ context.update({
'conf_path': settings.DOMAINS_SLAVES_PATH, 'conf_path': settings.DOMAINS_SLAVES_PATH,

View File

@ -16,8 +16,8 @@ class DomainAdminForm(forms.ModelForm):
top = domain.get_top() top = domain.get_top()
if not top: if not top:
# Fake an account to make django validation happy # Fake an account to make django validation happy
Account = self.fields['account']._queryset.model account_model = self.fields['account']._queryset.model
cleaned_data['account'] = Account() cleaned_data['account'] = account_model()
msg = _("An account should be provided for top domain names") msg = _("An account should be provided for top domain names")
raise ValidationError(msg) raise ValidationError(msg)
cleaned_data['account'] = top.account cleaned_data['account'] = top.account
@ -37,20 +37,3 @@ class RecordInlineFormSet(forms.models.BaseInlineFormSet):
records.append(data) records.append(data)
domain = domain_for_validation(self.instance, records) domain = domain_for_validation(self.instance, records)
validators.validate_zone(domain.render_zone()) validators.validate_zone(domain.render_zone())
class DomainIterator(forms.models.ModelChoiceIterator):
""" Group ticket owner by superusers, ticket.group and regular users """
def __init__(self, *args, **kwargs):
self.account = kwargs.pop('account')
self.domains = kwargs.pop('domains')
super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs)
def __iter__(self):
yield ('', '---------')
account_domains = self.domains.filter(account=self.account)
account_domains = account_domains.values_list('pk', 'name')
yield (_("Account"), list(account_domains))
domains = self.domains.exclude(account=self.account)
domains = domains.values_list('pk', 'name')
yield (_("Other"), list(domains))

View File

@ -12,13 +12,22 @@ def domain_for_validation(instance, records):
for data in records: for data in records:
yield Record(type=data['type'], value=data['value']) yield Record(type=data['type'], value=data['value'])
domain.get_records = get_records domain.get_records = get_records
def get_top_subdomains(exclude=None):
subdomains = []
for subdomain in Domain.objects.filter(name__endswith='.%s' % domain.origin.name):
if exclude != subdomain.pk:
subdomain.top = domain
yield subdomain
domain.get_top_subdomains = get_top_subdomains
if domain.top: if domain.top:
subdomains = domain.get_topsubdomains().exclude(pk=instance.pk) subdomains = domain.get_top_subdomains(exclude=instance.pk)
domain.top.get_subdomains = lambda: list(subdomains) + [domain] domain.top.get_subdomains = lambda: list(subdomains) + [domain]
elif not domain.pk: elif not domain.pk:
subdomains = [] subdomains = []
for subdomain in Domain.objects.filter(name__endswith=domain.name): for subdomain in Domain.objects.filter(name__endswith=domain.name):
subdomain.top = domain subdomain.top = domain
subdomains.append(subdomain) subdomains.append(subdomain)
domain.get_subdomains = lambda: subdomains domain.get_subdomains = get_top_subdomains
return domain return domain

View File

@ -14,7 +14,7 @@ class Domain(models.Model):
name = models.CharField(_("name"), max_length=256, unique=True, name = models.CharField(_("name"), max_length=256, unique=True,
validators=[validate_hostname, validators.validate_allowed_domain]) validators=[validate_hostname, validators.validate_allowed_domain])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='domains', blank=True) related_name='domains', blank=True, help_text=_("Automatically selected for subdomains"))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains') top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains')
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
help_text=_("Serial number")) help_text=_("Serial number"))
@ -22,29 +22,32 @@ class Domain(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@cached_property @property
def origin(self): def origin(self):
# Do not cache
return self.top or self return self.top or self
@cached_property @property
def is_top(self): def is_top(self):
# Do not cache
return not bool(self.top) return not bool(self.top)
def get_records(self): def get_records(self):
""" proxy method, needed for input validation """ """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.records.all() return self.records.all()
def get_topsubdomains(self): def get_top_subdomains(self):
""" proxy method, needed for input validation """ """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.origin.subdomains.all() return self.origin.subdomains.all()
def get_subdomains(self): def get_subdomains(self):
return self.get_topsubdomains().filter(name__regex=r'.%s$' % self.name) """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.get_top_subdomains().filter(name__endswith=r'.%s' % self.name)
def render_zone(self): def render_zone(self):
origin = self.origin origin = self.origin
zone = origin.render_records() zone = origin.render_records()
for subdomain in origin.get_topsubdomains(): for subdomain in origin.get_top_subdomains():
zone += subdomain.render_records() zone += subdomain.render_records()
return zone return zone
@ -76,7 +79,7 @@ class Domain(models.Model):
records.append( records.append(
AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value) AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value)
) )
if not self.top: if self.is_top:
if Record.NS not in types: if Record.NS not in types:
for ns in settings.DOMAINS_DEFAULT_NS: for ns in settings.DOMAINS_DEFAULT_NS:
records.append(AttrDict(type=Record.NS, value=ns)) records.append(AttrDict(type=Record.NS, value=ns))
@ -129,7 +132,7 @@ class Domain(models.Model):
for domain in domains.filter(name__endswith=self.name): for domain in domains.filter(name__endswith=self.name):
domain.top = self domain.top = self
domain.save(update_fields=['top']) domain.save(update_fields=['top'])
self.get_subdomains().update(account=self.account) self.subdomains.update(account=self.account)
def get_top(self): def get_top(self):
split = self.name.split('.') split = self.name.split('.')

View File

@ -37,4 +37,3 @@ class DomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeria
self._errors = { 'all': err.message } self._errors = { 'all': err.message }
return None return None
return instance return instance

View File

@ -2,10 +2,10 @@ from django.conf import settings
DOMAINS_DEFAULT_NAME_SERVER = getattr(settings, 'DOMAINS_DEFAULT_NAME_SERVER', DOMAINS_DEFAULT_NAME_SERVER = getattr(settings, 'DOMAINS_DEFAULT_NAME_SERVER',
'ns.example.com') 'ns.orchestra.lan')
DOMAINS_DEFAULT_HOSTMASTER = getattr(settings, 'DOMAINS_DEFAULT_HOSTMASTER', DOMAINS_DEFAULT_HOSTMASTER = getattr(settings, 'DOMAINS_DEFAULT_HOSTMASTER',
'hostmaster@example.com') 'hostmaster@orchestra.lan')
DOMAINS_DEFAULT_TTL = getattr(settings, 'DOMAINS_DEFAULT_TTL', '1h') DOMAINS_DEFAULT_TTL = getattr(settings, 'DOMAINS_DEFAULT_TTL', '1h')

View File

@ -1,11 +1,14 @@
import functools import functools
import os import os
import time import time
import socket
from django.conf import settings as djsettings
from django.core.urlresolvers import reverse
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from orchestra.apps.orchestration.models import Server, Route from orchestra.apps.orchestration.models import Server, Route
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot_on_error
from orchestra.utils.system import run from orchestra.utils.system import run
from ... import settings, utils, backends from ... import settings, utils, backends
@ -16,10 +19,15 @@ run = functools.partial(run, display=False)
class DomainTestMixin(object): class DomainTestMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_MASTER_SERVER', 'localhost')
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
MASTER_SERVER_ADDR = socket.gethostbyname(MASTER_SERVER)
SLAVE_SERVER_ADDR = socket.gethostbyname(SLAVE_SERVER)
def setUp(self): def setUp(self):
djsettings.DEBUG = True
settings.DOMAINS_MASTERS = [self.MASTER_SERVER_ADDR]
super(DomainTestMixin, self).setUp() super(DomainTestMixin, self).setUp()
self.MASTER_ADDR = os.environ['ORCHESTRA_DNS_MASTER_ADDR']
self.SLAVE_ADDR = os.environ['ORCHESTRA_DNS_SLAVE_ADDR']
self.domain_name = 'orchestra%s.lan' % random_ascii(10) self.domain_name = 'orchestra%s.lan' % random_ascii(10)
self.domain_records = ( self.domain_records = (
(Record.MX, '10 mail.orchestra.lan.'), (Record.MX, '10 mail.orchestra.lan.'),
@ -33,19 +41,19 @@ class DomainTestMixin(object):
(Record.NS, 'ns1.%s.' % self.domain_name), (Record.NS, 'ns1.%s.' % self.domain_name),
(Record.NS, 'ns2.%s.' % self.domain_name), (Record.NS, 'ns2.%s.' % self.domain_name),
) )
self.subdomain1_name = 'ns1.%s' % self.domain_name self.ns1_name = 'ns1.%s' % self.domain_name
self.subdomain1_records = ( self.ns1_records = (
(Record.A, '%s' % self.SLAVE_ADDR), (Record.A, '%s' % self.SLAVE_SERVER_ADDR),
) )
self.subdomain2_name = 'ns2.%s' % self.domain_name self.ns2_name = 'ns2.%s' % self.domain_name
self.subdomain2_records = ( self.ns2_records = (
(Record.A, '%s' % self.MASTER_ADDR), (Record.A, '%s' % self.MASTER_SERVER_ADDR),
) )
self.subdomain3_name = 'www.%s' % self.domain_name self.www_name = 'www.%s' % self.domain_name
self.subdomain3_records = ( self.www_records = (
(Record.CNAME, 'external.server.org.'), (Record.CNAME, 'external.server.org.'),
) )
self.second_domain_name = 'django%s.lan' % random_ascii(10) self.django_domain_name = 'django%s.lan' % random_ascii(10)
def tearDown(self): def tearDown(self):
try: try:
@ -173,42 +181,47 @@ class DomainTestMixin(object):
self.assertEqual('external.server.org.', cname[4]) self.assertEqual('external.server.org.', cname[4])
def test_add(self): def test_add(self):
self.add(self.subdomain1_name, self.subdomain1_records) self.add(self.ns1_name, self.ns1_records)
self.add(self.subdomain2_name, self.subdomain2_records) self.add(self.ns2_name, self.ns2_records)
self.add(self.domain_name, self.domain_records) self.add(self.domain_name, self.domain_records)
self.validate_add(self.MASTER_ADDR, self.domain_name) self.validate_add(self.MASTER_SERVER_ADDR, self.domain_name)
self.validate_add(self.SLAVE_ADDR, self.domain_name) time.sleep(0.5)
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
def test_delete(self): def test_delete(self):
self.add(self.subdomain1_name, self.subdomain1_records) self.add(self.ns1_name, self.ns1_records)
self.add(self.subdomain2_name, self.subdomain2_records) self.add(self.ns2_name, self.ns2_records)
self.add(self.domain_name, self.domain_records) self.add(self.domain_name, self.domain_records)
self.delete(self.domain_name) self.delete(self.domain_name)
for name in [self.domain_name, self.subdomain1_name, self.subdomain2_name]: for name in [self.domain_name, self.ns1_name, self.ns2_name]:
self.validate_delete(self.MASTER_ADDR, name) self.validate_delete(self.MASTER_SERVER_ADDR, name)
self.validate_delete(self.SLAVE_ADDR, name) self.validate_delete(self.SLAVE_SERVER_ADDR, name)
def test_update(self): def test_update(self):
self.add(self.subdomain1_name, self.subdomain1_records) self.add(self.ns1_name, self.ns1_records)
self.add(self.subdomain2_name, self.subdomain2_records) self.add(self.ns2_name, self.ns2_records)
self.add(self.domain_name, self.domain_records) self.add(self.domain_name, self.domain_records)
self.update(self.domain_name, self.domain_update_records) self.update(self.domain_name, self.domain_update_records)
self.add(self.subdomain3_name, self.subdomain3_records) self.add(self.www_name, self.www_records)
self.validate_update(self.MASTER_ADDR, self.domain_name) self.validate_update(self.MASTER_SERVER_ADDR, self.domain_name)
time.sleep(5) time.sleep(5)
self.validate_update(self.SLAVE_ADDR, self.domain_name) self.validate_update(self.SLAVE_SERVER_ADDR, self.domain_name)
def test_add_add_delete_delete(self): def test_add_add_delete_delete(self):
self.add(self.subdomain1_name, self.subdomain1_records) self.add(self.ns1_name, self.ns1_records)
self.add(self.subdomain2_name, self.subdomain2_records) self.add(self.ns2_name, self.ns2_records)
self.add(self.domain_name, self.domain_records) self.add(self.domain_name, self.domain_records)
self.add(self.second_domain_name, self.domain_records) self.add(self.django_domain_name, self.domain_records)
self.delete(self.domain_name) self.delete(self.domain_name)
self.validate_add(self.MASTER_ADDR, self.second_domain_name) self.validate_add(self.MASTER_SERVER_ADDR, self.django_domain_name)
self.validate_add(self.SLAVE_ADDR, self.second_domain_name) self.validate_add(self.SLAVE_SERVER_ADDR, self.django_domain_name)
self.delete(self.second_domain_name) self.delete(self.django_domain_name)
self.validate_delete(self.MASTER_ADDR, self.second_domain_name) self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
self.validate_delete(self.SLAVE_ADDR, self.second_domain_name) self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
def test_bad_creation(self):
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
self.add, self.domain_name, self.domain_records)
class AdminDomainMixin(DomainTestMixin): class AdminDomainMixin(DomainTestMixin):
@ -229,27 +242,38 @@ class AdminDomainMixin(DomainTestMixin):
value_input.send_keys(value) value_input.send_keys(value)
return value_input return value_input
@snapshot_on_error
def add(self, domain_name, records): def add(self, domain_name, records):
# TODO use reverse add = reverse('admin:domains_domain_add')
url = self.live_server_url + '/admin/domains/domain/add/' url = self.live_server_url + add
self.selenium.get(url) self.selenium.get(url)
name = self.selenium.find_element_by_id('id_name') name = self.selenium.find_element_by_id('id_name')
name.send_keys(domain_name) name.send_keys(domain_name)
account_input = self.selenium.find_element_by_id('id_account')
account_select = Select(account_input)
account_select.select_by_value(str(self.account.pk))
value_input = self._add_records(records) value_input = self._add_records(records)
value_input.submit() value_input.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def delete(self, domain_name): def delete(self, domain_name):
domain = Domain.objects.get(name=domain_name) domain = Domain.objects.get(name=domain_name)
url = self.live_server_url + '/admin/domains/domain/%d/delete/' % domain.pk delete = reverse('admin:domains_domain_delete', args=(domain.pk,))
url = self.live_server_url + delete
self.selenium.get(url) self.selenium.get(url)
form = self.selenium.find_element_by_name('post') form = self.selenium.find_element_by_name('post')
form.submit() form.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def update(self, domain_name, records): def update(self, domain_name, records):
domain = Domain.objects.get(name=domain_name) domain = Domain.objects.get(name=domain_name)
url = self.live_server_url + '/admin/domains/domain/%d/' % domain.pk change = reverse('admin:domains_domain_change', args=(domain.pk,))
url = self.live_server_url + change
self.selenium.get(url) self.selenium.get(url)
value_input = self._add_records(records) value_input = self._add_records(records)
value_input.submit() value_input.submit()
@ -284,10 +308,10 @@ class Bind9BackendMixin(object):
) )
def add_route(self): def add_route(self):
master = Server.objects.create(name=self.MASTER_ADDR) master = Server.objects.create(name=self.MASTER_SERVER, address=self.MASTER_SERVER_ADDR)
backend = backends.Bind9MasterDomainBackend.get_name() backend = backends.Bind9MasterDomainBackend.get_name()
Route.objects.create(backend=backend, match=True, host=master) Route.objects.create(backend=backend, match=True, host=master)
slave = Server.objects.create(name=self.SLAVE_ADDR) slave = Server.objects.create(name=self.SLAVE_SERVER, address=self.SLAVE_SERVER_ADDR)
backend = backends.Bind9SlaveDomainBackend.get_name() backend = backends.Bind9SlaveDomainBackend.get_name()
Route.objects.create(backend=backend, match=True, host=slave) Route.objects.create(backend=backend, match=True, host=slave)
@ -296,5 +320,5 @@ class RESTBind9BackendDomainTest(Bind9BackendMixin, RESTDomainMixin, BaseLiveSer
pass pass
class AdminBind9BackendDomainest(Bind9BackendMixin, AdminDomainMixin, BaseLiveServerTestCase): class AdminBind9BackendDomainTest(Bind9BackendMixin, AdminDomainMixin, BaseLiveServerTestCase):
pass pass

View File

@ -1,7 +1,9 @@
from django.contrib import messages from django.contrib import messages
from django.core.mail import mail_admins from django.core.mail import mail_admins
from django.core.urlresolvers import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe
from django.utils.translation import ungettext, ugettext_lazy as _
def send_report(method, args, log): def send_report(method, args, log):
@ -32,15 +34,27 @@ def send_report(method, args, log):
def message_user(request, logs): def message_user(request, logs):
total = len(logs) total, successes = 0, 0
successes = [ log for log in logs if log.state == log.SUCCESS ] ids = []
successes = len(successes) for log in logs:
total += 1
ids.append(log.pk)
if log.state == log.SUCCESS:
successes += 1
errors = total-successes errors = total-successes
if errors: if total > 1:
msg = 'backends have' if errors > 1 else 'backend has' url = reverse('admin:orchestration_backendlog_changelist')
msg = _("%d out of %d {0} fail to execute".format(msg)) url += '?id__in=%s' ','.join(map(str, ids))
messages.warning(request, msg % (errors, total))
else: else:
msg = 'backends have' if successes > 1 else 'backend has' url = reverse('admin:orchestration_backendlog_change', args=ids)
msg = _("%d {0} been successfully executed".format(msg)) if errors:
messages.success(request, msg % successes) msg = ungettext(
_('{errors} out of {total} <a href="{url}">banckends</a> has fail to execute.'),
_('{errors} out of {total} <a href="{url}">banckends</a> have fail to execute.'),
errors)
else:
msg = ungettext(
_('{total} <a href="{url}">banckend</a> has been executed.'),
_('{total} <a href="{url}">banckends</a> have been executed.'),
total)
messages.warning(request, mark_safe(msg.format(errors=errors, total=total, url=url)))

View File

@ -1,6 +1,7 @@
import copy import copy
from threading import local from threading import local
from django.core.urlresolvers import resolve
from django.db.models.signals import pre_delete, post_save from django.db.models.signals import pre_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.http.response import HttpResponseServerError from django.http.response import HttpResponseServerError
@ -92,6 +93,6 @@ class OperationsMiddleware(object):
operations = type(self).get_pending_operations() operations = type(self).get_pending_operations()
if operations: if operations:
logs = Operation.execute(operations) logs = Operation.execute(operations)
if logs: if logs and resolve(request.path).app_name == 'admin':
message_user(request, logs) message_user(request, logs)
return response return response

View File

@ -132,4 +132,3 @@ class FTPTraffic(ServiceMonitor):
'object_id': user.pk, 'object_id': user.pk,
'username': user.username, 'username': user.username,
} }

View File

@ -1,5 +1,7 @@
import ftplib import ftplib
import os
import re import re
import socket
from functools import partial from functools import partial
import paramiko import paramiko
@ -10,19 +12,19 @@ from selenium.webdriver.support.select import Select
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
from orchestra.apps.orchestration.models import Server, Route from orchestra.apps.orchestration.models import Server, Route
from orchestra.utils.system import run from orchestra.utils.system import run, sshrun
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot_on_error
from ... import backends, settings from ... import backends, settings
from ...models import SystemUser from ...models import SystemUser
r = partial(run, silent=True, display=False) r = partial(run, silent=True, display=False)
sshr = partial(sshrun, silent=True, display=False)
class SystemUserMixin(object): class SystemUserMixin(object):
MASTER_ADDR = 'localhost' MASTER_SERVER = os.environ.get('ORCHESTRA_MASTER_SERVER', 'localhost')
ACCOUNT_USERNAME = '%s_account' % random_ascii(10)
DEPENDENCIES = ( DEPENDENCIES = (
'orchestra.apps.orchestration', 'orchestra.apps.orchestration',
'orcgestra.apps.systemusers', 'orcgestra.apps.systemusers',
@ -34,7 +36,7 @@ class SystemUserMixin(object):
djsettings.DEBUG = True djsettings.DEBUG = True
def add_route(self): def add_route(self):
master = Server.objects.create(name=self.MASTER_ADDR) master = Server.objects.create(name=self.MASTER_SERVER)
backend = backends.SystemUserBackend.get_name() backend = backends.SystemUserBackend.get_name()
Route.objects.create(backend=backend, match=True, host=master) Route.objects.create(backend=backend, match=True, host=master)
@ -57,7 +59,7 @@ class SystemUserMixin(object):
raise NotImplementedError raise NotImplementedError
def validate_user(self, username): def validate_user(self, username):
idcmd = r("id %s" % username) idcmd = sshr(self.MASTER_SERVER, "id %s" % username)
self.assertEqual(0, idcmd.return_code) self.assertEqual(0, idcmd.return_code)
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
groups = list(user.groups.values_list('username', flat=True)) groups = list(user.groups.values_list('username', flat=True))
@ -68,18 +70,22 @@ class SystemUserMixin(object):
def validate_delete(self, username): def validate_delete(self, username):
self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username) self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username)
self.assertRaises(CommandError, run, 'id %s' % username, display=False) self.assertRaises(CommandError,
self.assertRaises(CommandError, run, 'grep "^%s:" /etc/groups' % username, display=False) sshrun, self.MASTER_SERVER,'id %s' % username, display=False)
self.assertRaises(CommandError, run, 'grep "^%s:" /etc/passwd' % username, display=False) self.assertRaises(CommandError,
self.assertRaises(CommandError, run, 'grep "^%s:" /etc/shadow' % username, display=False) 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 validate_ftp(self, username, password): def validate_ftp(self, username, password):
connection = ftplib.FTP(self.MASTER_ADDR) connection = ftplib.FTP(self.MASTER_SERVER)
connection.login(user=username, passwd=password) connection.login(user=username, passwd=password)
connection.close() connection.close()
def validate_sftp(self, username, password): def validate_sftp(self, username, password):
transport = paramiko.Transport((self.MASTER_ADDR, 22)) transport = paramiko.Transport((self.MASTER_SERVER, 22))
transport.connect(username=username, password=password) transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport) sftp = paramiko.SFTPClient.from_transport(transport)
sftp.listdir() sftp.listdir()
@ -88,14 +94,14 @@ class SystemUserMixin(object):
def validate_ssh(self, username, password): def validate_ssh(self, username, password):
ssh = paramiko.SSHClient() ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(self.MASTER_ADDR, username=username, password=password) ssh.connect(self.MASTER_SERVER, username=username, password=password)
transport = ssh.get_transport() transport = ssh.get_transport()
channel = transport.open_session() channel = transport.open_session()
channel.exec_command('ls') channel.exec_command('ls')
self.assertEqual(0, channel.recv_exit_status()) self.assertEqual(0, channel.recv_exit_status())
channel.close() channel.close()
def test_create_systemuser(self): def test_create(self):
username = '%s_systemuser' % random_ascii(10) username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5) password = '@!?%spppP001' % random_ascii(5)
self.add(username, password) self.add(username, password)
@ -125,7 +131,7 @@ class SystemUserMixin(object):
self.addCleanup(partial(self.delete, username)) self.addCleanup(partial(self.delete, username))
self.validate_ssh(username, password) self.validate_ssh(username, password)
def test_delete_systemuser(self): def test_delete(self):
username = '%s_systemuser' % random_ascii(10) username = '%s_systemuser' % random_ascii(10)
password = '@!?%sppppP001' % random_ascii(5) password = '@!?%sppppP001' % random_ascii(5)
self.add(username, password) self.add(username, password)
@ -133,7 +139,7 @@ class SystemUserMixin(object):
self.delete(username) self.delete(username)
self.validate_delete(username) self.validate_delete(username)
def test_add_group_systemuser(self): def test_add_group(self):
username = '%s_systemuser' % random_ascii(10) username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5) password = '@!?%spppP001' % random_ascii(5)
self.add(username, password) self.add(username, password)
@ -150,7 +156,7 @@ class SystemUserMixin(object):
self.assertIn(username2, groups) self.assertIn(username2, groups)
self.validate_user(username) self.validate_user(username)
def test_disable_systemuser(self): def test_disable(self):
username = '%s_systemuser' % random_ascii(10) username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5) password = '@!?%spppP001' % random_ascii(5)
self.add(username, password, shell='/dev/null') self.add(username, password, shell='/dev/null')
@ -159,6 +165,10 @@ class SystemUserMixin(object):
self.disable(username) self.disable(username)
self.validate_user(username) self.validate_user(username)
self.assertRaises(ftplib.error_perm, self.validate_ftp, username, password) self.assertRaises(ftplib.error_perm, self.validate_ftp, username, password)
def test_change_password(self):
pass
# TODO
class RESTSystemUserMixin(SystemUserMixin): class RESTSystemUserMixin(SystemUserMixin):
@ -200,6 +210,7 @@ class AdminSystemUserMixin(SystemUserMixin):
self.save(self.account.username) self.save(self.account.username)
self.addCleanup(partial(self.delete, self.account.username)) self.addCleanup(partial(self.delete, self.account.username))
@snapshot_on_error
def add(self, username, password, shell='/dev/null'): def add(self, username, password, shell='/dev/null'):
url = self.live_server_url + reverse('admin:systemusers_systemuser_add') url = self.live_server_url + reverse('admin:systemusers_systemuser_add')
self.selenium.get(url) self.selenium.get(url)
@ -223,6 +234,7 @@ class AdminSystemUserMixin(SystemUserMixin):
username_field.submit() username_field.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def delete(self, username): def delete(self, username):
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
delete = reverse('admin:systemusers_systemuser_delete', args=(user.pk,)) delete = reverse('admin:systemusers_systemuser_delete', args=(user.pk,))
@ -232,6 +244,7 @@ class AdminSystemUserMixin(SystemUserMixin):
confirmation.submit() confirmation.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def disable(self, username): def disable(self, username):
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,)) change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
@ -243,6 +256,7 @@ class AdminSystemUserMixin(SystemUserMixin):
save.submit() save.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def add_group(self, username, groupname): def add_group(self, username, groupname):
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,)) change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
@ -254,6 +268,7 @@ class AdminSystemUserMixin(SystemUserMixin):
save.submit() save.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error
def save(self, username): def save(self, username):
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,)) change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
@ -269,6 +284,7 @@ class RESTSystemUserTest(RESTSystemUserMixin, BaseLiveServerTestCase):
class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase): class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
@snapshot_on_error
def test_create_account(self): def test_create_account(self):
url = self.live_server_url + reverse('admin:accounts_account_add') url = self.live_server_url + reverse('admin:accounts_account_add')
self.selenium.get(url) self.selenium.get(url)
@ -298,8 +314,9 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
account = Account.objects.get(username=account_username) account = Account.objects.get(username=account_username)
self.addCleanup(account.delete) self.addCleanup(account.delete)
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
self.assertEqual(0, r("id %s" % account.username).return_code) self.assertEqual(0, sshr(self.MASTER_SERVER, "id %s" % account.username).return_code)
@snapshot_on_error
def test_delete_account(self): def test_delete_account(self):
home = self.account.systemusers.get(is_main=True).get_home() home = self.account.systemusers.get(is_main=True).get_home()

View File

@ -158,7 +158,8 @@ function install_requirements () {
PIP="${PIP} \ PIP="${PIP} \
selenium \ selenium \
xvfbwrapper \ xvfbwrapper \
freezegun" freezegun \
coverage"
fi fi
# Make sure locales are in place before installing postgres # Make sure locales are in place before installing postgres

View File

@ -105,6 +105,11 @@ def run(command, display=True, error_codes=[0], silent=False, stdin=''):
return out return out
def sshrun(addr, command, *args, **kwargs):
cmd = "ssh -o stricthostkeychecking=no root@%s -C '%s'" % (addr, command)
return run(cmd, *args, **kwargs)
def get_default_celeryd_username(): def get_default_celeryd_username():
""" Introspect celeryd defaults file in order to get its username """ """ Introspect celeryd defaults file in order to get its username """
user = None user = None

View File

@ -1,5 +1,7 @@
import datetime
import string import string
import random import random
from functools import wraps
from django.conf import settings from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model
@ -105,3 +107,17 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
def rest_login(self): def rest_login(self):
self.rest.login(username=self.account.username, password=self.account_password) self.rest.login(username=self.account.username, password=self.account_password)
def snapshot_on_error(test):
@wraps(test)
def inner(*args, **kwargs):
try:
test(*args, **kwargs)
except:
self = args[0]
timestamp = datetime.datetime.now().isoformat().replace(':', '')
filename = '/tmp/screenshot_%s_%s.png' % (self.id(), timestamp)
self.selenium.save_screenshot(filename)
raise
return inner

24
scripts/services/bind9.md Normal file
View File

@ -0,0 +1,24 @@
Bind9 Master and Slave
======================
1. Install bind9 service as well as some convinient utilities on master and slave servers
```bash
apt-get update
apt-get install bind9 dnsutils
```
2. create the zone directory on the master server
```bash
mkdir /etc/bind/master
chown bind.bind /etc/bind/master
```
2. Allow zone transfer on master by adding the following line to `named.conf.options`
```bash
allow-transfer { slave-ip; };
```
3. Addlow notifications on the slave server by adding the following line to `named.conf.options`
```bash
allow-notify { master-ip; };
```

View File

@ -1,9 +0,0 @@
#!/bin/bash
# Installs and confingures bind9 to work with Orchestra
apt-get update
apt-get install bind9
echo "nameserver 127.0.0.1" > /etc/resolv.conf

View File

@ -9,6 +9,15 @@ apt-get install postfix
apt-get install dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sieve apt-get install dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sieve
sed -i "s#^mail_location = mbox.*#mail_location = maildir:~/Maildir#" /etc/dovecot/conf.d/10-mail.conf
echo 'auth_username_format = %n' >> /etc/dovecot/conf.d/10-auth.conf
echo 'service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
group = postfix
mode = 0600
user = postfix
}
}' >> /etc/dovecot/conf.d/10-master.conf
cat > /etc/apt/sources.list.d/mailscanner.list << 'EOF' cat > /etc/apt/sources.list.d/mailscanner.list << 'EOF'
@ -18,16 +27,17 @@ EOF
wget -O - http://apt.baruwa.org/baruwa-apt-keys.gpg | apt-key add - wget -O - http://apt.baruwa.org/baruwa-apt-keys.gpg | apt-key add -
apt-get update apt-get update
apt-get install mailscanner apt-get install mailscanner
apt-get install dovecot-core dovecot-imapd dovecot-pop3d dovecot-sieve
apt-get install postfix apt-get install postfix
echo 'home_mailbox = Maildir/' >> /etc/postfix/main.cf
echo 'mailbox_transport = lmtp:unix:private/dovecot-lmtp' >> /etc/postfix/main.cf
mail_location = maildir:~/Maildir
/etc/init.d/dovecot restart
/etc/init.d/postfix restart

View File

@ -12,6 +12,6 @@ Restricted Shell for SCP and Rsync
2. Enable the shell 2. Enable the shell
```bash ```bash
ln -s /usr/local/bin/rssh /bin/rssh ln -s /usr/bin/rssh /bin/rssh
echo /bin/rssh >> /etc/shells echo /bin/rssh >> /etc/shells
``` ```

View File

@ -12,9 +12,9 @@ VsFTPd with System Users
```bash ```bash
sed -i "s/anonymous_enable=YES/anonymous_enable=NO/" /etc/vsftpd.conf sed -i "s/anonymous_enable=YES/anonymous_enable=NO/" /etc/vsftpd.conf
sed -i "s/#local_enable=YES/local_enable=YES/" /etc/vsftpd.conf sed -i "s/#local_enable=YES/local_enable=YES/" /etc/vsftpd.conf
sed -i "s/#write_enable=YES/write_enable=YES" /etc/vsftpd.conf sed -i "s/#write_enable=YES/write_enable=YES/" /etc/vsftpd.conf
sed -i "s/#chroot_local_user=YES/chroot_local_user=YES/" /etc/vsftpd.conf # sed -i "s/#chroot_local_user=YES/chroot_local_user=YES/" /etc/vsftpd.conf
echo '/dev/null' >> /etc/shells echo '/dev/null' >> /etc/shells
``` ```