Merge pull request #99 from modoboa/feature/automx

Added automx support.
This commit is contained in:
Antoine Nguyen
2017-03-17 09:12:35 +01:00
committed by GitHub
15 changed files with 287 additions and 58 deletions

View File

@@ -25,7 +25,7 @@ Usage::
$ git clone https://github.com/modoboa/modoboa-installer $ git clone https://github.com/modoboa/modoboa-installer
$ cd modoboa-installer $ cd modoboa-installer
$ sudo ./run.py <mail server hostname> $ sudo ./run.py <your domain>
To customize the installation, look at the ``installer.cfg`` file. To customize the installation, look at the ``installer.cfg`` file.
@@ -35,7 +35,7 @@ By default, the following components are installed:
* Nginx and uWSGI * Nginx and uWSGI
* Postfix * Postfix
* Dovecot * Dovecot
* Amavis (with SpamAssassin and ClamAV) * Amavis (with SpamAssassin and ClamAV)
If you want more information about the installation process, add the If you want more information about the installation process, add the
``--debug`` option to your command line. ``--debug`` option to your command line.

View File

@@ -1,3 +1,7 @@
[general]
# %(domain)s is the value specified when launching the installer
hostname = mail.%(domain)s
[certificate] [certificate]
generate = true generate = true
# Choose between self-signed or letsencrypt # Choose between self-signed or letsencrypt
@@ -39,6 +43,14 @@ extensions = modoboa-amavis modoboa-pdfcredentials modoboa-postfix-autoreply mod
# Deploy Modoboa and enable development mode # Deploy Modoboa and enable development mode
devmode = false devmode = false
[automx]
enabled = true
user = automx
config_dir = /etc
home_dir = /srv/automx
venv_path = %(home_dir)s/env
instance_path = %(home_dir)s/instance
[amavis] [amavis]
enabled = true enabled = true
user = amavis user = amavis
@@ -86,6 +98,6 @@ dbuser = spamassassin
dbpassword = password dbpassword = password
[uwsgi] [uwsgi]
enabled = true enabled = true
config_dir = /etc/uwsgi config_dir = /etc/uwsgi
nb_processes = 2 nb_processes = 2

View File

@@ -6,6 +6,14 @@ from . import package
from . import utils from . import utils
def get_path(cmd, venv=None):
"""Return path to cmd."""
path = cmd
if venv:
path = os.path.join(venv, "bin", path)
return path
def get_pip_path(venv): def get_pip_path(venv):
"""Return the full path to pip command.""" """Return the full path to pip command."""
binpath = "pip" binpath = "pip"

View File

@@ -0,0 +1,94 @@
"""Automx related tasks."""
import os
import pwd
import shutil
import stat
from .. import python
from .. import system
from .. import utils
from . import base
class Automx(base.Installer):
"""Automx installation."""
appname = "automx"
config_files = ["automx.conf"]
no_daemon = True
packages = {
"deb": ["memcached", "unzip"],
"rpm": ["memcached", "unzip"]
}
with_user = True
def __init__(self, config):
"""Get configuration."""
super(Automx, self).__init__(config)
self.venv_path = config.get("automx", "venv_path")
self.instance_path = config.get("automx", "instance_path")
def get_template_context(self):
"""Additional variables."""
context = super(Automx, self).get_template_context()
sql_dsn = "{}://{}:{}@{}/{}".format(
self.dbengine,
self.config.get("modoboa", "dbuser"),
self.config.get("modoboa", "dbpassword"),
self.dbhost,
self.config.get("modoboa", "dbname"))
if self.db_driver == "pgsql":
sql_query = (
"SELECT first_name || ' ' || last_name AS display_name, email "
"FROM core_user WHERE email='%s' AND is_active")
else:
sql_query = (
"SELECT concat(first_name, ' ', last_name) AS display_name, "
"email FROM core_user WHERE email='%s' AND is_active=1"
)
context.update({"sql_dsn": sql_dsn, "sql_query": sql_query})
return context
def _setup_venv(self):
"""Prepare a python virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user)
packages = [
"future", "lxml", "ipaddress", "sqlalchemy", "python-memcached",
"python-dateutil", "configparser"
]
python.install_packages(packages, self.venv_path, sudo_user=self.user)
target = "{}/master.zip".format(self.home_dir)
if os.path.exists(target):
os.unlink(target)
utils.exec_cmd(
"wget https://github.com/sys4/automx/archive/master.zip",
sudo_user=self.user, cwd=self.home_dir)
self.repo_dir = "{}/automx-master".format(self.home_dir)
if os.path.exists(self.repo_dir):
shutil.rmtree(self.repo_dir)
utils.exec_cmd(
"unzip master.zip", sudo_user=self.user, cwd=self.home_dir)
utils.exec_cmd(
"{} setup.py install".format(
python.get_path("python", self.venv_path)),
cwd=self.repo_dir)
def _deploy_instance(self):
"""Copy files to instance dir."""
if not os.path.exists(self.instance_path):
pw = pwd.getpwnam(self.user)
mode = (
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
utils.mkdir(self.instance_path, mode, pw[2], pw[3])
path = "{}/src/automx_wsgi.py".format(self.repo_dir)
utils.exec_cmd("cp {} {}".format(path, self.instance_path),
sudo_user=self.user, cwd=self.home_dir)
def post_run(self):
"""Additional tasks."""
self._setup_venv()
self._deploy_instance()
system.enable_and_start_service("memcached")

View File

@@ -9,7 +9,6 @@ from .. import utils
class Installer(object): class Installer(object):
"""Simple installer for one application.""" """Simple installer for one application."""
appname = None appname = None
@@ -137,10 +136,7 @@ class Installer(object):
if self.no_daemon: if self.no_daemon:
return return
name = self.get_daemon_name() name = self.get_daemon_name()
system.enable_service(name) system.enable_and_start_service(name)
code, output = utils.exec_cmd("service {} status".format(name))
action = "start" if code else "restart"
utils.exec_cmd("service {} {}".format(name, action))
def run(self): def run(self):
"""Run the installer.""" """Run the installer."""

View File

@@ -0,0 +1,41 @@
[automx]
provider = %domain
domains = *
# Protect against DoS
memcache = 127.0.0.1:11211
memcache_ttl = 600
client_error_limit = 20
rate_limit_exception_networks = 127.0.0.0/8, ::1/128
[global]
backend = sql
actions = settings
account_type = email
host = %sql_dsn
query = %sql_query
result_attrs = display_name, email
smtp = yes
smtp_server = %hostname
smtp_port = 587
smtp_encryption = starttls
smtp_auth = plaintext
smtp_auth_identity = ${email}
smtp_refresh_ttl = 6
smtp_default = yes
imap = yes
imap_server = %hostname
imap_port = 143
imap_encryption = starttls
imap_auth = plaintext
imap_auth_identity = ${email}
imap_refresh_ttl = 6
pop = yes
pop_server = %hostname
pop_port = 110
pop_encryption = starttls
pop_auth = plaintext
pop_auth_identity = ${email}

View File

@@ -0,0 +1,16 @@
upstream automx {
server unix:%uwsgi_socket_path fail_timeout=0;
}
server {
listen 80;
server_name %hostname;
access_log /var/log/nginx/%{hostname}-access.log;
error_log /var/log/nginx/%{hostname}-error.log;
location /mail/config-v1.1.xml {
include uwsgi_params;
uwsgi_pass automx;
}
}

View File

@@ -11,7 +11,7 @@ server {
server { server {
listen 443 ssl; listen 443 ssl;
server_name %hostname; server_name %hostname;
root %modoboa_instance_path; root %app_instance_path;
ssl_certificate %tls_cert_file; ssl_certificate %tls_cert_file;
ssl_certificate_key %tls_key_file; ssl_certificate_key %tls_key_file;

View File

@@ -0,0 +1,14 @@
[uwsgi]
uid = %app_user
gid = %app_user
plugins = python
home = %app_venv_path
chdir = %app_instance_path
module = automx_wsgi
master = true
vhost = true
harakiri = 60
processes = %nb_processes
socket = %uwsgi_socket_path
chmod-socket = 660
vacuum = true

View File

@@ -1,9 +1,9 @@
[uwsgi] [uwsgi]
uid = %modoboa_user uid = %app_user
gid = %modoboa_user gid = %app_user
plugins = python plugins = python
home = %modoboa_venv_path home = %app_venv_path
chdir = %modoboa_instance_path chdir = %app_instance_path
module = instance.wsgi:application module = instance.wsgi:application
master = true master = true
processes = %nb_processes processes = %nb_processes

View File

@@ -12,7 +12,6 @@ from .. import python
from .. import utils from .. import utils
from . import base from . import base
from . import install
class Modoboa(base.Installer): class Modoboa(base.Installer):
@@ -52,7 +51,7 @@ class Modoboa(base.Installer):
self.extensions.remove("modoboa-amavis") self.extensions.remove("modoboa-amavis")
def _setup_venv(self): def _setup_venv(self):
"""Prepare a dedicated virtuelenv.""" """Prepare a dedicated virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user) python.setup_virtualenv(self.venv_path, sudo_user=self.user)
packages = ["modoboa", "rrdtool"] packages = ["modoboa", "rrdtool"]
if self.dbengine == "postgres": if self.dbengine == "postgres":
@@ -187,5 +186,3 @@ class Modoboa(base.Installer):
self._setup_venv() self._setup_venv()
self._deploy_instance() self._deploy_instance()
self.apply_settings() self.apply_settings()
install("uwsgi", self.config)
install("nginx", self.config)

View File

@@ -11,7 +11,6 @@ from .uwsgi import Uwsgi
class Nginx(base.Installer): class Nginx(base.Installer):
"""Nginx installer.""" """Nginx installer."""
appname = "nginx" appname = "nginx"
@@ -20,21 +19,23 @@ class Nginx(base.Installer):
"rpm": ["nginx"] "rpm": ["nginx"]
} }
def get_template_context(self): def get_template_context(self, app):
"""Additionnal variables.""" """Additionnal variables."""
context = super(Nginx, self).get_template_context() context = super(Nginx, self).get_template_context()
context.update({ context.update({
"modoboa_instance_path": ( "app_instance_path": (
self.config.get("modoboa", "instance_path")), self.config.get(app, "instance_path")),
"uwsgi_socket_path": Uwsgi(self.config).socket_path "uwsgi_socket_path": Uwsgi(self.config).get_socket_path(app)
}) })
return context return context
def post_run(self): def _setup_config(self, app, hostname=None):
"""Additionnal tasks.""" """Custom app configuration."""
hostname = self.config.get("general", "hostname") if hostname is None:
context = self.get_template_context() hostname = self.config.get("general", "hostname")
src = self.get_file_path("nginx.conf.tpl") context = self.get_template_context(app)
context.update({"hostname": hostname})
src = self.get_file_path("{}.conf.tpl".format(app))
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
dst = os.path.join( dst = os.path.join(
self.config_dir, "sites-available", "{}.conf".format(hostname)) self.config_dir, "sites-available", "{}.conf".format(hostname))
@@ -44,7 +45,7 @@ class Nginx(base.Installer):
if os.path.exists(link): if os.path.exists(link):
return return
os.symlink(dst, link) os.symlink(dst, link)
group = self.config.get("modoboa", "user") group = self.config.get(app, "user")
user = "www-data" user = "www-data"
else: else:
dst = os.path.join( dst = os.path.join(
@@ -54,6 +55,13 @@ class Nginx(base.Installer):
user = "nginx" user = "nginx"
system.add_user_to_group(user, group) system.add_user_to_group(user, group)
def post_run(self):
"""Additionnal tasks."""
self._setup_config("modoboa")
if self.config.getboolean("automx", "enabled"):
hostname = "autoconfig.{}".format(
self.config.get("general", "domain"))
self._setup_config("automx", hostname)
if not os.path.exists("{}/dhparam.pem".format(self.config_dir)): if not os.path.exists("{}/dhparam.pem".format(self.config_dir)):
cmd = "openssl dhparam -dsaparam -out dhparam.pem 4096" cmd = "openssl dhparam -dsaparam -out dhparam.pem 4096"
utils.exec_cmd(cmd, cwd=self.config_dir) utils.exec_cmd(cmd, cwd=self.config_dir)

View File

@@ -10,7 +10,6 @@ from . import base
class Uwsgi(base.Installer): class Uwsgi(base.Installer):
"""uWSGI installer.""" """uWSGI installer."""
appname = "uwsgi" appname = "uwsgi"
@@ -19,22 +18,21 @@ class Uwsgi(base.Installer):
"rpm": ["uwsgi", "uwsgi-plugin-python"], "rpm": ["uwsgi", "uwsgi-plugin-python"],
} }
@property def get_socket_path(self, app):
def socket_path(self):
"""Return socket path.""" """Return socket path."""
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
return "/run/uwsgi/app/modoboa_instance/socket" return "/run/uwsgi/app/{}_instance/socket".format(app)
return "/run/uwsgi/modoboa_instance.sock" return "/run/uwsgi/{}_instance.sock".format(app)
def get_template_context(self): def get_template_context(self, app):
"""Additionnal variables.""" """Additionnal variables."""
context = super(Uwsgi, self).get_template_context() context = super(Uwsgi, self).get_template_context()
context.update({ context.update({
"modoboa_user": self.config.get("modoboa", "user"), "app_user": self.config.get(app, "user"),
"modoboa_venv_path": self.config.get("modoboa", "venv_path"), "app_venv_path": self.config.get(app, "venv_path"),
"modoboa_instance_path": ( "app_instance_path": (
self.config.get("modoboa", "instance_path")), self.config.get(app, "instance_path")),
"uwsgi_socket_path": self.socket_path, "uwsgi_socket_path": self.get_socket_path(app),
}) })
return context return context
@@ -44,18 +42,28 @@ class Uwsgi(base.Installer):
return os.path.join(self.config_dir, "apps-available") return os.path.join(self.config_dir, "apps-available")
return "{}.d".format(self.config_dir) return "{}.d".format(self.config_dir)
def post_run(self): def _enable_config_debian(self, dst):
"""Additionnal tasks.""" """Enable config file."""
context = self.get_template_context() link = os.path.join(
src = self.get_file_path("uwsgi.ini.tpl") self.config_dir, "apps-enabled", os.path.basename(dst))
dst = os.path.join(self.get_config_dir(), "modoboa_instance.ini") if os.path.exists(link):
return
os.symlink(dst, link)
def _setup_config(self, app):
"""Common setup code."""
context = self.get_template_context(app)
src = self.get_file_path("{}.ini.tpl".format(app))
dst = os.path.join(
self.get_config_dir(), "{}_instance.ini".format(app))
utils.copy_from_template(src, dst, context) utils.copy_from_template(src, dst, context)
return dst
def _setup_modoboa_config(self):
"""Custom modoboa configuration."""
dst = self._setup_config("modoboa")
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
link = os.path.join( self._enable_config_debian(dst)
self.config_dir, "apps-enabled", os.path.basename(dst))
if os.path.exists(link):
return
os.symlink(dst, link)
else: else:
system.add_user_to_group( system.add_user_to_group(
"uwsgi", self.config.get("modoboa", "user")) "uwsgi", self.config.get("modoboa", "user"))
@@ -68,8 +76,32 @@ class Uwsgi(base.Installer):
utils.exec_cmd( utils.exec_cmd(
"perl -pi -e '{}' /etc/uwsgi.ini".format(pattern)) "perl -pi -e '{}' /etc/uwsgi.ini".format(pattern))
def _setup_automx_config(self):
"""Custom automx configuration."""
dst = self._setup_config("automx")
if package.backend.FORMAT == "deb":
self._enable_config_debian(dst)
else:
system.add_user_to_group(
"uwsgi", self.config.get("automx", "user"))
pattern = (
"s/emperor-tyrant = true/emperor-tyrant = false/")
utils.exec_cmd(
"perl -pi -e '{}' /etc/uwsgi.ini".format(pattern))
def post_run(self):
"""Additionnal tasks."""
self._setup_modoboa_config()
if self.config.getboolean("automx", "enabled"):
self._setup_automx_config()
def restart_daemon(self): def restart_daemon(self):
"""Restart daemon process.""" """Restart daemon process."""
code, output = utils.exec_cmd("service uwsgi status modoboa_instance") instances = ["modoboa_instance"]
action = "start" if code else "restart" if self.config.getboolean("automx", "enabled"):
utils.exec_cmd("service uwsgi {}".format(action)) instances.append("automx_instance")
for instance in instances:
code, output = utils.exec_cmd("service uwsgi status {}".format(
instance))
action = "start" if code else "restart"
utils.exec_cmd("service uwsgi {}".format(action))

View File

@@ -49,3 +49,11 @@ def add_user_to_group(user, group):
def enable_service(name): def enable_service(name):
"""Enable a service at startup.""" """Enable a service at startup."""
utils.exec_cmd("systemctl enable {}".format(name)) utils.exec_cmd("systemctl enable {}".format(name))
def enable_and_start_service(name):
"""Enable a start a service."""
enable_service(name)
code, output = utils.exec_cmd("service {} status".format(name))
action = "start" if code else "restart"
utils.exec_cmd("service {} {}".format(name, action))

15
run.py
View File

@@ -21,8 +21,8 @@ def main():
help="Enable debug output") help="Enable debug output")
parser.add_argument("--force", action="store_true", default=False, parser.add_argument("--force", action="store_true", default=False,
help="Force installation") help="Force installation")
parser.add_argument("hostname", type=str, parser.add_argument("domain", type=str,
help="The hostname of your future mail server") help="The main domain of your future mail server")
args = parser.parse_args() args = parser.parse_args()
if args.debug: if args.debug:
@@ -33,10 +33,10 @@ def main():
config.readfp(fp) config.readfp(fp)
if not config.has_section("general"): if not config.has_section("general"):
config.add_section("general") config.add_section("general")
config.set("general", "hostname", args.hostname) config.set("general", "domain", args.domain)
utils.printcolor( utils.printcolor(
"Your mail server {} will be installed with the following components:" "Your mail server will be installed with the following components:",
.format(args.hostname), utils.BLUE) utils.BLUE)
components = [] components = []
for section in config.sections(): for section in config.sections():
if section in ["general", "database", "mysql", "postgres", if section in ["general", "database", "mysql", "postgres",
@@ -62,11 +62,14 @@ def main():
ssl_backend.create() ssl_backend.create()
scripts.install("amavis", config) scripts.install("amavis", config)
scripts.install("modoboa", config) scripts.install("modoboa", config)
scripts.install("automx", config)
scripts.install("uwsgi", config)
scripts.install("nginx", config)
scripts.install("postfix", config) scripts.install("postfix", config)
scripts.install("dovecot", config) scripts.install("dovecot", config)
utils.printcolor( utils.printcolor(
"Congratulations! You can enjoy Modoboa at https://{} (admin:password)" "Congratulations! You can enjoy Modoboa at https://{} (admin:password)"
.format(args.hostname), .format(config.get("general", "hostname")),
utils.GREEN) utils.GREEN)
if __name__ == "__main__": if __name__ == "__main__":