Merge branch 'master' into features/relay_domains

This commit is contained in:
Antoine Nguyen
2013-11-08 11:03:50 +01:00
19 changed files with 168 additions and 78 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
include README.rst COPYING requirements.txt
recursive-include modoboa *.html *.js *.css *.png *.po *.mo *.json *.gif
recursive-include modoboa/core/templates *.tpl
recursive-include doc *
recursive-include modoboa/core/commands/templates *.tpl
recursive-include doc *
+26 -1
View File
@@ -94,7 +94,7 @@ Deployment
`modoboa-admin.py`, a command line tool, let you deploy a
*ready-to-use* Modoboa site using only one instruction::
$ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis]
$ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url]
Just answer the few questions and you're done. You can now go to the
:ref:`first_use` section.
@@ -104,6 +104,31 @@ Just answer the few questions and you're done. You can now go to the
The `--with-amavis` option must be set only if you intend to use
the :ref:`amavis_frontend`.
In case you need a **silent installation**, e.g. if you're using Salt-Stack.
It's possible to supply the database credentials as commandline arguments.
.. note::
`--dburl database-url` for the modoboa database credentials
`--amavis_dburl database-url` for the amavis database credentials
Your database-url should meet the following syntax:
``scheme://[user:pass@][host:port]/dbname``
**or**
``sqlite:////full/path/to/your/database/file.sqlite``
Available `schemes` are:
* postgres
* postgresql
* postgis
* mysql
* mysql2
* sqlite
.. note::
If you plan to serve Modoboa using a URL prefix, you must change the
+10 -8
View File
@@ -25,6 +25,8 @@ variable::
Then, edit the ``inbox`` namespace and add the following lines::
inbox = yes
mailbox Drafts {
auto = create
special_use = \Drafts
@@ -111,7 +113,7 @@ content inside::
# Path for SQL configuration file, see example-config/dovecot-sql.conf.ext
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb sql {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
@@ -149,7 +151,7 @@ PostgreSQL users
::
driver = postgresql
driver = pgsql
connect = host=<postgres socket> dbname=<database> user=<user> password=<password>
@@ -213,7 +215,7 @@ proper permissions::
unix_listener dict {
mode = 0600
user = <user owning mailboxes>
#group =
#group =
}
}
@@ -392,12 +394,12 @@ Finally, configure the ``sieve`` plugin by editing the
:file:`conf.d/90-sieve.conf` file. Put the follwing caontent inside::
plugin {
# Location of the active script. When ManageSieve is used this is actually
# Location of the active script. When ManageSieve is used this is actually
# a symlink pointing to the active script in the sieve storage directory.
sieve = ~/.dovecot.sieve
#
# The path to the directory where the personal Sieve scripts are stored. For
# The path to the directory where the personal Sieve scripts are stored. For
# ManageSieve this is where the uploaded scripts are stored.
sieve_dir = ~/sieve
}
@@ -440,12 +442,12 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file
virtual_transport = dovecot
dovecot_destination_recipient_limit = 1
virtual_minimum_uid = <vmail user id>
virtual_minimum_uid = <vmail user id>
virtual_gid_maps = static:<vmail group id>
virtual_uid_maps = static:<vmail user id>
virtual_mailbox_base = /var/vmail
relay_domains =
relay_domains =
virtual_mailbox_domains = <driver>:/etc/postfix/sql-domains.cf
virtual_alias_domains = <driver>:/etc/postfix/sql-domain-aliases.cf
virtual_mailbox_maps = <driver>:/etc/postfix/sql-mailboxes.cf
@@ -455,7 +457,7 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file
smtpd_recipient_restrictions =
...
check_recipient_access <driver>:/etc/postfix/maps/sql-maintain.cf
check_recipient_access <driver>:/etc/postfix/sql-maintain.cf
permit_mynetworks
...
+1 -1
View File
@@ -60,4 +60,4 @@ def unset_default_topredirection(extension):
"""
topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION")
if topredirection == extension.name:
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs")
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "core")
+15 -4
View File
@@ -17,12 +17,16 @@ def enabled_applications():
:return: a list
"""
from modoboa.core.models import Extension
from modoboa.core.extensions import exts_pool
from modoboa.lib.dbutils import db_table_exists
result = [("core", "core")]
result = [("user", "user")]
if db_table_exists("core_extension"):
exts = Extension.objects.filter(enabled=True)
result += [(ext.name, ext.name) for ext in exts]
for ext in exts:
extclass = exts_pool.get_extension(ext.name)
if extclass.available_for_topredirection:
result.append((ext.name, ext.name))
return sorted(result, key=lambda e: e[0])
@@ -98,7 +102,7 @@ class GeneralParametersForm(parameters.AdminParametersForm):
label=ugettext_lazy("Bind password"),
initial='',
help_text=ugettext_lazy("The password to use when binding to the LDAP server (with 'Bind DN')"),
widget=forms.PasswordInput,
widget=forms.PasswordInput(render_value=True),
required=False
)
@@ -152,7 +156,7 @@ class GeneralParametersForm(parameters.AdminParametersForm):
default_top_redirection = forms.ChoiceField(
label=ugettext_lazy("Default top redirection"),
choices=[],
initial="core",
initial="user",
help_text=ugettext_lazy("The default redirection used when no application is specified")
)
@@ -176,6 +180,13 @@ class GeneralParametersForm(parameters.AdminParametersForm):
super(GeneralParametersForm, self).__init__(*args, **kwargs)
self.fields["default_top_redirection"].choices = enabled_applications()
def clean_secret_key(self):
if len(self.cleaned_data["secret_key"]) not in [16, 24, 32]:
raise forms.ValidationError(
_("Key must be either 16, 24, or 32 bytes long")
)
return self.cleaned_data["secret_key"]
def clean_ldap_user_dn_template(self):
tpl = self.cleaned_data["ldap_user_dn_template"]
try:
+1 -1
View File
@@ -19,7 +19,7 @@ class Command(object):
self._verbose = verbose
if not settings.configured:
settings.configure()
self._templates_dir = "%s/commands/templates" % os.path.dirname(__file__)
self._templates_dir = "%s/templates" % os.path.dirname(__file__)
def _render_template(self, tplfile, env):
fp = open(tplfile)
+78 -31
View File
@@ -4,19 +4,21 @@ import os
import shutil
import getpass
import subprocess
import dj_database_url
from django.core import management
from django.template import Context, Template
from modoboa.lib.sysutils import exec_cmd
from . import Command
from modoboa.core.commands import Command
dbconn_tpl = """
'{{ conn_name }}': {
'ENGINE': 'django.db.backends.{{ dbtype }}', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
'NAME': '{{ dbname }}', # Or path to database file if using sqlite3.
'USER': '{{ username }}', # Not used with sqlite3.
'PASSWORD': '{{ password }}', # Not used with sqlite3.
'HOST': '{{ dbhost }}', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.{% if dbtype == 'mysql' %}
'ENGINE': '{{ ENGINE }}',
'NAME': '{{ NAME }}', # Or path to database file if using sqlite3.
'USER': '{% if USER %}{{ USER }}{% endif %}', # Not used with sqlite3.
'PASSWORD': '{% if PASSWORD %}{{ PASSWORD }}{% endif %}', # Not used with sqlite3.
'HOST': '{% if HOST %}{{ HOST }}{% endif %}', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '{% if PORT %}{{ PORT }}{% endif %}', # Set to empty string for default. Not used with sqlite3.{% if ENGINE == 'django.db.backends.mysql' %}
'OPTIONS' : {
"init_command" : 'SET foreign_key_checks = 0;',
},{% endif %}
@@ -43,6 +45,16 @@ class DeployCommand(Command):
'--collectstatic', action='store_true', default=False,
help='Run django collectstatic command'
)
self._parser.add_argument(
'--dburl', type=str, nargs=1, default=None,
help='The database-url for your modoboa instance')
self._parser.add_argument(
'--amavis_dburl', type=str, nargs=1, default=None,
help='The database-url for your amavis instance')
self._parser.add_argument(
'--domain', type=str, nargs=1, default=None,
help='The domain under which you want to deploy modoboa')
def _exec_django_command(self, name, cwd, *args):
"""Run a django command for the freshly created project
@@ -76,22 +88,29 @@ class DeployCommand(Command):
"""
print "Configuring database connection: %s" % name
info = {'conn_name': name,
'dbtype': raw_input('Database type (mysql, postgres or sqlite3): ')}
if info['dbtype'] not in ['mysql', 'postgres', 'sqlite3']:
info['dbtype'] = 'mysql'
if info['dbtype'] == 'postgres':
info['dbtype'] = 'postgresql_psycopg2'
if info['dbtype'] == 'sqlite3':
info['dbname'] = '%s.db' % name
'ENGINE': raw_input('Database type (mysql, postgres or sqlite3): ')}
if info['ENGINE'] not in ['mysql', 'postgres', 'sqlite3']:
raise RuntimeError('Unsupported database engine')
if info['ENGINE'] == 'sqlite3':
info['ENGINE'] = 'django.db.backends.sqlite3'
info['NAME'] = '%s.db' % name
return info
if info['ENGINE'] == 'postgres':
info['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
default_port = 5432
else:
default_host = 'localhost' if info['dbtype'] == 'mysql' else ''
info['dbhost'] = \
raw_input("Database host (default: '%s'): " % default_host)
if info['dbhost'] == '':
info['dbhost'] = default_host
info['dbname'] = raw_input('Database name: ')
info['username'] = raw_input('Username: ')
info['password'] = getpass.getpass('Password: ')
info['ENGINE'] = 'django.db.backends.mysql'
default_port = 3306
info['HOST'] = raw_input("Database host (default: 'localhost'): ")
info['PORT'] = raw_input("Database port (default: '%s'): " % default_port)
# leave port setting empty, if default value is supplied and
# leave it to django
if info['PORT'] == default_port:
info['PORT'] = ''
info['NAME'] = raw_input('Database name: ')
info['USER'] = raw_input('Username: ')
info['PASSWORD'] = getpass.getpass('Password: ')
return info
def handle(self, parsed_args):
@@ -109,13 +128,41 @@ class DeployCommand(Command):
django14 = False
t = Template(dbconn_tpl)
default_conn = t.render(Context(self.ask_db_info()))
amavis_conn = t.render(Context(self.ask_db_info('amavis'))) \
if parsed_args.with_amavis else None
allowed_host = raw_input(
'Under which domain do you want to deploy modoboa? '
)
if parsed_args.dburl:
info = dj_database_url.config(default=parsed_args.dburl[0])
# In case the user fails to supply a valid database url, fallback to manual mode
if not info:
print "There was a problem with your database-url. \n"
info = self.ask_db_info()
#If we set this earlier, our fallback method will never be triggered
info['conn_name'] = 'default'
else:
info = self.ask_db_info()
default_conn = t.render(Context(info))
if parsed_args.with_amavis:
if parsed_args.amavis_dburl:
amavis_info = dj_database_url.config(default=parsed_args.amavis_dburl[0])
# In case the user fails to supply a valid database url, fallback to manual mode
if not amavis_info:
amavis_info = self.ask_db_info('amavis')
#If we set this earlier, our fallback method will never be triggered
amavis_info['conn_name'] = 'amavis'
else:
amavis_info = self.ask_db_info('amavis')
amavis_conn = t.render(Context(amavis_info))
else:
amavis_conn = None
if parsed_args.domain:
allowed_host = parsed_args.domain[0]
else:
allowed_host = raw_input(
'Under which domain do you want to deploy modoboa? '
)
mod = __import__(parsed_args.name, globals(), locals(), ['settings'])
tpl = self._render_template(
@@ -126,15 +173,15 @@ class DeployCommand(Command):
'allowed_host': allowed_host
}
)
fp = open("%s/settings.py" % path, "w")
fp.write(tpl)
fp.close()
with open("%s/settings.py" % path, "w") as fp:
fp.write(tpl)
shutil.copyfile(
"%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path
)
os.mkdir("%s/media" % path)
if parsed_args.syncdb:
os.unlink("%s/settings.pyc" % path)
self._exec_django_command(
"syncdb", parsed_args.name, '--noinput'
)
@@ -159,7 +159,6 @@ MODOBOA_APPS = (
'modoboa.extensions.webmail',
'modoboa.extensions.stats',
'modoboa.extensions.sievefilters',
'modoboa.extensions.stats',
{% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis',
)
@@ -167,6 +166,8 @@ INSTALLED_APPS += MODOBOA_APPS
AUTH_USER_MODEL = 'core.User'
SOUTH_TESTS_MIGRATE = False
#MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
AUTHENTICATION_BACKENDS = (
+1
View File
@@ -15,6 +15,7 @@ class ModoExtension(object):
url = None
needs_media = False
always_active = False
available_for_topredirection = False
def infos(self):
return dict(
+1 -1
View File
@@ -35,7 +35,7 @@ def dologin(request):
nextlocation = request.POST.get("next", None)
if nextlocation is None or nextlocation == "None":
if user.group != "SimpleUsers":
if user.group == "SimpleUsers":
nextlocation = reverse("modoboa.lib.webutils.topredirection")
else:
nextlocation = reverse("domains")
+1
View File
@@ -20,6 +20,7 @@ class Amavis(ModoExtension):
version = "1.0"
description = ugettext_lazy("Simple amavis management frontend")
url = "quarantine"
available_for_topredirection = True
def init(self):
"""Init function
@@ -11,6 +11,7 @@ class SieveFilters(ModoExtension):
version = "1.0"
description = ugettext_lazy("Plugin to easily create server-side filters")
url = "sfilters"
available_for_topredirection = True
def load(self):
from .app_settings import ParametersForm, UserSettings
+1
View File
@@ -12,6 +12,7 @@ class Webmail(ModoExtension):
version = "1.0"
description = ugettext_lazy("Simple IMAP webmail")
needs_media = True
available_for_topredirection = True
def load(self):
from .app_settings import ParametersForm, UserSettings
+1 -1
View File
@@ -20,7 +20,7 @@
"model": "lib.parameter",
"fields": {
"name": "general.DEFAULT_TOP_REDIRECTION",
"value": "userprefs"
"value": "user"
}
},
{
+5 -11
View File
@@ -138,27 +138,21 @@ def size2integer(value):
@login_required
def topredirection(request):
"""Simple view to redirect the request when no application is specified
"""Simple view to redirect the request when no application is specified.
The default "top redirection" can be specified in the *Admin >
Settings* panel. It is the application that will be launched by
default. Users that are not allowed to access this application
will be redirected to the "User preferences" application.
This feature only applies to simple users.
:param request: a Request object
"""
from modoboa.lib import parameters
from modoboa.core.extensions import exts_pool
topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="core")
if not topredir in ["core"]:
infos = exts_pool.get_extension_infos(topredir)
path = infos["url"] if infos["url"] else infos["name"]
else:
path = "admin" # topredir
if topredir in ["core", "stats"] and \
request.user.belongs_to_group('SimpleUsers'):
path = "userprefs"
infos = exts_pool.get_extension_infos(topredir)
path = infos["url"] if infos["url"] else infos["name"]
return HttpResponseRedirect(path)
+3 -2
View File
@@ -42,7 +42,8 @@ function modalbox(e, css, defhref, defcb, defclosecb) {
$div.modal({show: false});
$div.one('shown', function() {
$(".help").popover({
container: "#modalbox"
container: "#modalbox",
trigger: "hover"
}).click(function(e) {e.preventDefault();});
if (modalcb != undefined) {
if (typeof modalcb === "function") modalcb(); else eval(modalcb + '()');
@@ -308,5 +309,5 @@ $(document).ready(function() {
$(document).on('click', 'a[data-toggle="ajaxmodal"]', modalbox);
$(document).on('click', 'a[data-toggle="ajaxmodal-autowidth"]', modalbox_autowidth);
$(document).on('click', '.activator', activate_widget);
$(".help").popover().click(function(e) {e.preventDefault();});
$(".help").popover({trigger: 'hover'}).click(function(e) {e.preventDefault();});
});
+1 -1
View File
@@ -45,7 +45,7 @@ TwocolsNav.prototype = {
$('#' + this.options.divid + ' input[type=radio]').click($.proxy(this.radio_clicked, this));
$(".help").popover({
placement: 'bottom',
trigger: 'click'
trigger: 'hover'
}).click(function(e) {e.preventDefault();});
$('#' + this.options.divid + ' select').change();
$('#' + this.options.divid + ' input[type=radio]:checked').click();
+1
View File
@@ -1,5 +1,6 @@
django
django-reversion
dj-database-url
south
lxml
pycrypto
+17 -14
View File
@@ -1,19 +1,11 @@
# coding: utf-8
import os
import sys
import subprocess
import tempfile
import pexpect
import shutil
import unittest
def runcmd(cmd, **kwargs):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=True, **kwargs)
output = p.communicate()[0]
return p.returncode, output
from modoboa.lib.sysutils import exec_cmd
class DeployTest(unittest.TestCase):
@@ -25,19 +17,23 @@ class DeployTest(unittest.TestCase):
def setUp(self):
cmd = "mysqladmin -u %s -p%s create %s" % (self.dbuser, self.dbpassword, self.projname)
code, out = runcmd(cmd)
code, out = exec_cmd(cmd)
self.assertEqual(code, 0)
self.workdir = tempfile.mkdtemp()
def tearDown(self):
path = os.path.join(self.workdir, self.projname)
code, output = exec_cmd("python manage.py test admin", cwd=path)
self.assertEqual(code, 0)
if hasattr(self, "workdir"):
shutil.rmtree(self.workdir)
shutil.rmtree(self.workdir)
child = pexpect.spawn("mysqladmin -u root -p%s drop %s" % (self.dbpassword, self.projname))
child.expect("\[y/N\]")
child.sendline("y")
child.expect('Database "%s" dropped' % self.projname)
def test(self):
def test_standard(self):
timeout = 2
cmd = "modoboa-admin.py deploy --syncdb --collectstatic %s" % self.projname
child = pexpect.spawn(cmd, cwd=self.workdir)
@@ -47,6 +43,8 @@ class DeployTest(unittest.TestCase):
child.sendline(self.dbtype)
child.expect("Database host \(default: 'localhost'\):", timeout=timeout)
child.sendline(self.dbhost)
child.expect("Database port \(default: '3306'\):", timeout=timeout)
child.sendline('3306')
child.expect("Database name:", timeout=timeout)
child.sendline(self.projname)
child.expect("Username:", timeout=timeout)
@@ -59,8 +57,13 @@ class DeployTest(unittest.TestCase):
fout.close()
self.assertEqual(child.exitstatus, 0)
path = os.path.join(self.workdir, self.projname)
code, output = runcmd("python manage.py test admin", cwd=path)
def test_silent(self):
dburl = "%s://%s:%s@%s/%s" \
% (self.dbtype, self.dbuser, self.dbpassword,
self.dbhost, self.projname)
cmd = "modoboa-admin.py deploy --syncdb --collectstatic --dburl %s --domain %s %s" \
% (dburl, 'localhost', self.projname)
code, output = exec_cmd(cmd, cwd=self.workdir)
self.assertEqual(code, 0)