From d8130392706dc1c2ff1fd938629ae3d7db839942 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 31 Mar 2018 14:12:53 +0200 Subject: [PATCH] Added Radicale setup. (#194) * Added Radicale setup. see #193 * Fixed setup on CentOS. --- modoboa_installer/config_dict_template.py | 33 +++- modoboa_installer/python.py | 31 +++- modoboa_installer/scripts/dovecot.py | 3 +- .../files/dovecot/conf.d/10-master.conf.tpl | 7 + .../scripts/files/radicale/config.tpl | 161 ++++++++++++++++++ .../scripts/files/radicale/supervisor.tpl | 8 + modoboa_installer/scripts/modoboa.py | 9 + modoboa_installer/scripts/nginx.py | 9 + modoboa_installer/scripts/radicale.py | 79 +++++++++ run.py | 1 + 10 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 modoboa_installer/scripts/files/radicale/config.tpl create mode 100644 modoboa_installer/scripts/files/radicale/supervisor.tpl create mode 100644 modoboa_installer/scripts/radicale.py diff --git a/modoboa_installer/config_dict_template.py b/modoboa_installer/config_dict_template.py index 6f39dc9..7e15a74 100644 --- a/modoboa_installer/config_dict_template.py +++ b/modoboa_installer/config_dict_template.py @@ -158,7 +158,9 @@ ConfigDictTemplate = [ "default": ( "modoboa-amavis modoboa-pdfcredentials " "modoboa-postfix-autoreply modoboa-sievefilters " - "modoboa-stats modoboa-webmail modoboa-contacts"), + "modoboa-stats modoboa-webmail modoboa-contacts " + "modoboa-radicale" + ), }, { "option": "devmode", @@ -270,6 +272,10 @@ ConfigDictTemplate = [ "option": "postmaster_address", "default": "postmaster@%(domain)s", }, + { + "option": "radicale_auth_socket_path", + "default": "/var/run/dovecot/auth-radicale" + }, ] }, { @@ -372,4 +378,29 @@ ConfigDictTemplate = [ }, ] }, + { + "name": "radicale", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "user", + "default": "radicale", + }, + { + "option": "config_dir", + "default": "/etc/radicale", + }, + { + "option": "home_dir", + "default": "/srv/radicale", + }, + { + "option": "venv_path", + "default": "%(home_dir)s/env", + } + ] + }, ] diff --git a/modoboa_installer/python.py b/modoboa_installer/python.py index 87edd93..3c07f19 100644 --- a/modoboa_installer/python.py +++ b/modoboa_installer/python.py @@ -36,14 +36,35 @@ def install_packages(names, venv=None, upgrade=False, **kwargs): utils.exec_cmd(cmd, **kwargs) -def setup_virtualenv(path, sudo_user=None): +def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs): + """Install a Python package from its repository.""" + if vcs == "git": + package.backend.install("git") + cmd = "{} install -e {}+{}#egg={}".format( + get_pip_path(venv), vcs, url, name) + utils.exec_cmd(cmd, **kwargs) + + +def setup_virtualenv(path, sudo_user=None, python_version=2): """Install a virtualenv if needed.""" if os.path.exists(path): return - packages = ["python-virtualenv"] - if utils.dist_name() == "debian": - packages.append("virtualenv") + if python_version == 2: + python_binary = "python" + packages = ["python-virtualenv"] + if utils.dist_name() == "debian": + packages.append("virtualenv") + else: + if utils.dist_name().startswith("centos"): + python_binary = "python36" + packages = ["python36"] + else: + python_binary = "python3" + packages = ["python3-venv"] package.backend.install_many(packages) with utils.settings(sudo_user=sudo_user): - utils.exec_cmd("virtualenv {}".format(path)) + if python_version == 2: + utils.exec_cmd("virtualenv {}".format(path)) + else: + utils.exec_cmd("{} -m venv {}".format(python_binary, path)) install_package("pip", venv=path, upgrade=True) diff --git a/modoboa_installer/scripts/dovecot.py b/modoboa_installer/scripts/dovecot.py index 04ce6b4..093fbcf 100644 --- a/modoboa_installer/scripts/dovecot.py +++ b/modoboa_installer/scripts/dovecot.py @@ -77,7 +77,8 @@ class Dovecot(base.Installer): "modoboa_dbuser": self.config.get("modoboa", "dbuser"), "modoboa_dbpassword": self.config.get("modoboa", "dbpassword"), "protocols": protocols, - "ssl_protocols": ssl_protocols + "ssl_protocols": ssl_protocols, + "radicale_user": self.config.get("radicale", "user") }) return context diff --git a/modoboa_installer/scripts/files/dovecot/conf.d/10-master.conf.tpl b/modoboa_installer/scripts/files/dovecot/conf.d/10-master.conf.tpl index 740f169..6c25b8d 100644 --- a/modoboa_installer/scripts/files/dovecot/conf.d/10-master.conf.tpl +++ b/modoboa_installer/scripts/files/dovecot/conf.d/10-master.conf.tpl @@ -116,6 +116,13 @@ service auth { group = postfix } + # Radicale auth + %{radicale_enabled}unix_listener %{radicale_auth_socket_path} { + %{radicale_enabled} mode = 0666 + %{radicale_enabled} user = %{radicale_user} + %{radicale_enabled} group = %{radicale_user} + %{radicale_enabled}} + # Auth process is run as this user. #user = $default_internal_user } diff --git a/modoboa_installer/scripts/files/radicale/config.tpl b/modoboa_installer/scripts/files/radicale/config.tpl new file mode 100644 index 0000000..a4285a4 --- /dev/null +++ b/modoboa_installer/scripts/files/radicale/config.tpl @@ -0,0 +1,161 @@ +# -*- mode: conf -*- +# vim:ft=cfg + +# Config file for Radicale - A simple calendar server +# +# Place it into /etc/radicale/config (global) +# or ~/.config/radicale/config (user) +# +# The current values are the default ones + + +[server] + +# CalDAV server hostnames separated by a comma +# IPv4 syntax: address:port +# IPv6 syntax: [address]:port +# For example: 0.0.0.0:9999, [::]:9999 +#hosts = 127.0.0.1:5232 + +# Daemon flag +#daemon = False + +# File storing the PID in daemon mode +#pid = + +# Max parallel connections +#max_connections = 20 + +# Max size of request body (bytes) +#max_content_length = 10000000 + +# Socket timeout (seconds) +#timeout = 10 + +# SSL flag, enable HTTPS protocol +#ssl = False + +# SSL certificate path +#certificate = /etc/ssl/radicale.cert.pem + +# SSL private key +#key = /etc/ssl/radicale.key.pem + +# CA certificate for validating clients. This can be used to secure +# TCP traffic between Radicale and a reverse proxy +#certificate_authority = + +# SSL Protocol used. See python's ssl module for available values +#protocol = PROTOCOL_TLSv1_2 + +# Available ciphers. See python's ssl module for available ciphers +#ciphers = + +# Reverse DNS to resolve client address in logs +#dns_lookup = True + +# Message displayed in the client when a password is needed +#realm = Radicale - Password Required + + +[encoding] + +# Encoding for responding requests +#request = utf-8 + +# Encoding for storing local collections +#stock = utf-8 + + +[auth] + +# Authentication method +# Value: none | htpasswd | remote_user | http_x_remote_user +type = radicale_dovecot_auth + +# Htpasswd filename +# htpasswd_filename = users + +# Htpasswd encryption method +# Value: plain | sha1 | ssha | crypt | bcrypt | md5 +# Only bcrypt can be considered secure. +# bcrypt and md5 require the passlib library to be installed. +# htpasswd_encryption = plain + +# Incorrect authentication delay (seconds) +#delay = 1 + +auth_socket = %{radicale_auth_socket_path} + + +[rights] + +# Rights backend +# Value: none | authenticated | owner_only | owner_write | from_file +type = from_file + +# File for rights management from_file +file = %{config_dir}/rights + + +[storage] + +# Storage backend +# Value: multifilesystem +type = radicale_storage_by_index +radicale_storage_by_index_fields = dtstart, dtend, uid, summary + +# Folder for storing local collections, created if not present +filesystem_folder = %{home_dir}/collections + +# Lock the storage. Never start multiple instances of Radicale or edit the +# storage externally while Radicale is running if disabled. +#filesystem_locking = True + +# Sync all changes to disk during requests. (This can impair performance.) +# Disabling it increases the risk of data loss, when the system crashes or +# power fails! +#filesystem_fsync = True + +# Delete sync token that are older (seconds) +#max_sync_token_age = 2592000 + +# Close the lock file when no more clients are waiting. +# This option is not very useful in general, but on Windows files that are +# opened cannot be deleted. +#filesystem_close_lock_file = False + +# Command that is run after changes to storage +# Example: ([ -d .git ] || git init) && git add -A && (git diff --cached --quiet || git commit -m "Changes by "%%(user)s) +#hook = + + +[web] + +# Web interface backend +# Value: none | internal +type = none + + +[logging] + +# Logging configuration file +# If no config is given, simple information is printed on the standard output +# For more information about the syntax of the configuration file, see: +# http://docs.python.org/library/logging.config.html +#config = /etc/radicale/logging + +# Set the default logging level to debug +debug = False + +# Store all environment variables (including those set in the shell) +#full_environment = False + +# Don't include passwords in logs +#mask_passwords = True + + +[headers] + +# Additional HTTP headers +#Access-Control-Allow-Origin = * diff --git a/modoboa_installer/scripts/files/radicale/supervisor.tpl b/modoboa_installer/scripts/files/radicale/supervisor.tpl new file mode 100644 index 0000000..644f337 --- /dev/null +++ b/modoboa_installer/scripts/files/radicale/supervisor.tpl @@ -0,0 +1,8 @@ +[program:radicale] +autostart=true +autorestart=true +command=%{venv_path}/bin/radicale -C %{config_dir}/config +directory=%{home_dir} +redirect_stderr=true +user=%{user} +numprocs=1 diff --git a/modoboa_installer/scripts/modoboa.py b/modoboa_installer/scripts/modoboa.py index e7d933f..97a7a74 100644 --- a/modoboa_installer/scripts/modoboa.py +++ b/modoboa_installer/scripts/modoboa.py @@ -50,6 +50,9 @@ class Modoboa(base.Installer): self.amavis_enabled = True else: self.extensions.remove("modoboa-amavis") + if "modoboa-radicale" in self.extensions: + if not self.config.getboolean("radicale", "enabled"): + self.extensions.remove("modoboa-radicale") def is_extension_ok_for_version(self, extension, version): """Check if extension can be installed with this modo version.""" @@ -200,6 +203,12 @@ class Modoboa(base.Installer): }, "modoboa_pdfcredentials": { "storage_dir": pdf_storage_dir + }, + "modoboa_radicale": { + "server_location": "https://{}/radicale/".format( + self.config.get("general", "hostname")), + "rights_file_path": "{}/rights".format( + self.config.get("radicale", "config_dir")) } } for path in ["/var/log/maillog", "/var/log/mail.log"]: diff --git a/modoboa_installer/scripts/nginx.py b/modoboa_installer/scripts/nginx.py index def4d78..bbff33a 100644 --- a/modoboa_installer/scripts/nginx.py +++ b/modoboa_installer/scripts/nginx.py @@ -71,6 +71,15 @@ class Nginx(base.Installer): include uwsgi_params; uwsgi_pass automx; } +""" + if self.config.get("radicale", "enabled"): + extra_modoboa_config += """ + location /radicale/ { + proxy_pass http://localhost:5232/; # The / is important! + proxy_set_header X-Script-Name /radicale; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_header Authorization; + } """ self._setup_config( "modoboa", extra_config=extra_modoboa_config) diff --git a/modoboa_installer/scripts/radicale.py b/modoboa_installer/scripts/radicale.py new file mode 100644 index 0000000..5b4fc75 --- /dev/null +++ b/modoboa_installer/scripts/radicale.py @@ -0,0 +1,79 @@ +"""Radicale related tasks.""" + +import os +import stat + +from .. import package +from .. import python +from .. import utils + +from . import base + + +class Radicale(base.Installer): + """Radicale installation.""" + + appname = "radicale" + config_files = ["config"] + no_daemon = True + packages = { + "deb": ["supervisor"], + "rpm": ["supervisor"] + } + with_user = True + + def __init__(self, config): + """Get configuration.""" + super(Radicale, self).__init__(config) + self.venv_path = config.get("radicale", "venv_path") + + def _setup_venv(self): + """Prepare a dedicated virtualenv.""" + python.setup_virtualenv( + self.venv_path, sudo_user=self.user, python_version=3) + packages = ["Radicale", "radicale-dovecot-auth", "pytz"] + python.install_packages(packages, self.venv_path, sudo_user=self.user) + python.install_package_from_repository( + "radicale-storage-by-index", + "https://github.com/tonioo/RadicaleStorageByIndex", + venv=self.venv_path, sudo_user=self.user) + + def get_template_context(self): + """Additional variables.""" + context = super(Radicale, self).get_template_context() + radicale_auth_socket_path = self.config.get( + "dovecot", "radicale_auth_socket_path") + context.update({ + "radicale_auth_socket_path": radicale_auth_socket_path + }) + return context + + def get_config_files(self): + """Return appropriate path.""" + config_files = super(Radicale, self).get_config_files() + if package.backend.FORMAT == "deb": + path = "supervisor=/etc/supervisor/conf.d/radicale.conf" + else: + path = "supervisor=/etc/supervisord.d/radicale.ini" + config_files.append(path) + return config_files + + def install_config_files(self): + """Make sure config directory exists.""" + if not os.path.exists(self.config_dir): + utils.mkdir( + self.config_dir, + stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH, + 0, 0 + ) + super(Radicale, self).install_config_files() + + def post_run(self): + """Additional tasks.""" + self._setup_venv() + daemon_name = ( + "supervisor" if package.backend.FORMAT == "deb" else "supervisord" + ) + utils.exec_cmd("service {} stop".format(daemon_name)) + utils.exec_cmd("service {} start".format(daemon_name)) diff --git a/run.py b/run.py index 80c586f..d796bba 100755 --- a/run.py +++ b/run.py @@ -93,6 +93,7 @@ def main(input_args): scripts.install("amavis", config) scripts.install("modoboa", config) scripts.install("automx", config) + scripts.install("radicale", config) scripts.install("uwsgi", config) scripts.install("nginx", config) scripts.install("postfix", config)