Compare commits
37 Commits
dovecot-24
...
improve-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8045f41718 | ||
|
|
24ca07f762 | ||
|
|
39e78649aa | ||
|
|
a81cd4196c | ||
|
|
d6c70fba1d | ||
|
|
9c0052c274 | ||
|
|
3e5b9ab310 | ||
|
|
30b9393877 | ||
|
|
5887274ba8 | ||
|
|
0e64b92199 | ||
|
|
1d701353d9 | ||
|
|
3acec87ab5 | ||
|
|
dd9c7457c8 | ||
|
|
73feb967fe | ||
|
|
ad3f9f8cef | ||
|
|
765b56d48a | ||
|
|
a9a8f8888c | ||
|
|
355e8d9b98 | ||
|
|
212fa9b5c1 | ||
|
|
65961209c8 | ||
|
|
5a6f5e7d71 | ||
|
|
eed7603ba1 | ||
|
|
731df935e5 | ||
|
|
6f1e717fc4 | ||
|
|
c7f24218aa | ||
|
|
3d80bc3131 | ||
|
|
67971ee981 | ||
|
|
34bf98452d | ||
|
|
6e9fc2e7c2 | ||
|
|
90d4aac001 | ||
|
|
22cbc2e278 | ||
|
|
2dbec9d2a5 | ||
|
|
9d91a25293 | ||
|
|
246419407a | ||
|
|
fc81c04220 | ||
|
|
ff485f0d25 | ||
|
|
56be1be372 |
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
|workflow| |codecov|
|
|workflow| |codecov|
|
||||||
|
|
||||||
An installer which deploy a complete mail server based on Modoboa.
|
An installer which deploys a complete mail server based on Modoboa.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
This tool is still in beta stage, it has been tested on:
|
This tool is still in beta, it has been tested on:
|
||||||
|
|
||||||
* Debian 12 and upper
|
* Debian 12 and upper
|
||||||
* Ubuntu Focal Fossa (20.04) and upper
|
* Ubuntu Focal Fossa (20.04) and upper
|
||||||
@@ -44,7 +44,6 @@ The following components are installed by the installer:
|
|||||||
* Postfix
|
* Postfix
|
||||||
* Dovecot
|
* Dovecot
|
||||||
* Amavis (with SpamAssassin and ClamAV) or Rspamd
|
* Amavis (with SpamAssassin and ClamAV) or Rspamd
|
||||||
* automx (autoconfiguration service)
|
|
||||||
* OpenDKIM
|
* OpenDKIM
|
||||||
* Radicale (CalDAV and CardDAV server)
|
* Radicale (CalDAV and CardDAV server)
|
||||||
|
|
||||||
|
|||||||
@@ -222,35 +222,6 @@ ConfigDictTemplate = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "automx",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"option": "enabled",
|
|
||||||
"default": "true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"option": "user",
|
|
||||||
"default": "automx",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"option": "config_dir",
|
|
||||||
"default": "/etc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"option": "home_dir",
|
|
||||||
"default": "/srv/automx",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"option": "venv_path",
|
|
||||||
"default": "%(home_dir)s/env",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"option": "instance_path",
|
|
||||||
"default": "%(home_dir)s/instance",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "rspamd",
|
"name": "rspamd",
|
||||||
"if": ["antispam.enabled=true", "antispam.type=rspamd"],
|
"if": ["antispam.enabled=true", "antispam.type=rspamd"],
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
import stat
|
import stat
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from . import package
|
from . import package
|
||||||
from . import system
|
from . import system
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
class Database(object):
|
class Database:
|
||||||
|
|
||||||
"""Common database backend."""
|
"""Common database backend."""
|
||||||
|
|
||||||
default_port = None
|
default_port: Optional[int] = None
|
||||||
packages = None
|
packages: Optional[dict[str, list[str]]] = None
|
||||||
service = None
|
service: Optional[str] = None
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Install if necessary."""
|
"""Install if necessary."""
|
||||||
@@ -36,7 +36,6 @@ class Database(object):
|
|||||||
|
|
||||||
|
|
||||||
class PostgreSQL(Database):
|
class PostgreSQL(Database):
|
||||||
|
|
||||||
"""Postgres."""
|
"""Postgres."""
|
||||||
|
|
||||||
default_port = 5432
|
default_port = 5432
|
||||||
@@ -157,7 +156,6 @@ class PostgreSQL(Database):
|
|||||||
|
|
||||||
|
|
||||||
class MySQL(Database):
|
class MySQL(Database):
|
||||||
|
|
||||||
"""MySQL backend."""
|
"""MySQL backend."""
|
||||||
|
|
||||||
default_port = 3306
|
default_port = 3306
|
||||||
@@ -178,7 +176,7 @@ class MySQL(Database):
|
|||||||
if name.startswith("debian"):
|
if name.startswith("debian"):
|
||||||
if version.startswith("8"):
|
if version.startswith("8"):
|
||||||
self.packages["deb"].append("libmysqlclient-dev")
|
self.packages["deb"].append("libmysqlclient-dev")
|
||||||
elif version.startswith("11") or version.startswith("12"):
|
elif int(version[:2]) >= 11:
|
||||||
self.packages["deb"].append("libmariadb-dev")
|
self.packages["deb"].append("libmariadb-dev")
|
||||||
else:
|
else:
|
||||||
self.packages["deb"].append("libmariadbclient-dev")
|
self.packages["deb"].append("libmariadbclient-dev")
|
||||||
@@ -188,7 +186,7 @@ class MySQL(Database):
|
|||||||
self.packages["deb"].append("libmariadb-dev")
|
self.packages["deb"].append("libmariadb-dev")
|
||||||
else:
|
else:
|
||||||
self.packages["deb"].append("libmysqlclient-dev")
|
self.packages["deb"].append("libmysqlclient-dev")
|
||||||
super(MySQL, self).install_package()
|
super().install_package()
|
||||||
queries = []
|
queries = []
|
||||||
if name.startswith("debian"):
|
if name.startswith("debian"):
|
||||||
if version.startswith("8"):
|
if version.startswith("8"):
|
||||||
@@ -200,7 +198,7 @@ class MySQL(Database):
|
|||||||
self.dbpassword)
|
self.dbpassword)
|
||||||
return
|
return
|
||||||
if (
|
if (
|
||||||
(name.startswith("debian") and (version.startswith("11") or version.startswith("12"))) or
|
(name.startswith("debian") and int(version[:2]) >= 11) or
|
||||||
(name.startswith("ubuntu") and int(version[:2]) >= 22)
|
(name.startswith("ubuntu") and int(version[:2]) >= 22)
|
||||||
):
|
):
|
||||||
queries = [
|
queries = [
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from .. import utils
|
|||||||
def load_app_script(appname):
|
def load_app_script(appname):
|
||||||
"""Load module corresponding to the given appname."""
|
"""Load module corresponding to the given appname."""
|
||||||
try:
|
try:
|
||||||
script = importlib.import_module(
|
script = importlib.import_module("modoboa_installer.scripts.{}".format(appname))
|
||||||
"modoboa_installer.scripts.{}".format(appname))
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Unknown application {}".format(appname))
|
print("Unknown application {}".format(appname))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -19,8 +18,9 @@ def load_app_script(appname):
|
|||||||
|
|
||||||
def install(appname: str, config, upgrade: bool, archive_path: str):
|
def install(appname: str, config, upgrade: bool, archive_path: str):
|
||||||
"""Install an application."""
|
"""Install an application."""
|
||||||
if (config.has_option(appname, "enabled") and
|
if config.has_option(appname, "enabled") and not config.getboolean(
|
||||||
not config.getboolean(appname, "enabled")):
|
appname, "enabled"
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
utils.printcolor("Installing {}".format(appname), utils.MAGENTA)
|
utils.printcolor("Installing {}".format(appname), utils.MAGENTA)
|
||||||
@@ -34,8 +34,9 @@ def install(appname: str, config, upgrade: bool, archive_path: str):
|
|||||||
|
|
||||||
def backup(appname, config, path):
|
def backup(appname, config, path):
|
||||||
"""Backup an application."""
|
"""Backup an application."""
|
||||||
if (config.has_option(appname, "enabled") and
|
if config.has_option(appname, "enabled") and not config.getboolean(
|
||||||
not config.getboolean(appname, "enabled")):
|
appname, "enabled"
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
utils.printcolor("Backing up {}".format(appname), utils.MAGENTA)
|
utils.printcolor("Backing up {}".format(appname), utils.MAGENTA)
|
||||||
@@ -49,6 +50,5 @@ def backup(appname, config, path):
|
|||||||
|
|
||||||
def restore_prep(restore):
|
def restore_prep(restore):
|
||||||
"""Restore instance"""
|
"""Restore instance"""
|
||||||
script = importlib.import_module(
|
script = importlib.import_module("modoboa_installer.scripts.restore")
|
||||||
"modoboa_installer.scripts.restore")
|
|
||||||
getattr(script, "Restore")(restore)
|
getattr(script, "Restore")(restore)
|
||||||
|
|||||||
@@ -10,18 +10,29 @@ from . import backup, install
|
|||||||
|
|
||||||
|
|
||||||
class Amavis(base.Installer):
|
class Amavis(base.Installer):
|
||||||
|
|
||||||
"""Amavis installer."""
|
"""Amavis installer."""
|
||||||
|
|
||||||
appname = "amavis"
|
appname = "amavis"
|
||||||
packages = {
|
packages = {
|
||||||
"deb": [
|
"deb": [
|
||||||
"libdbi-perl", "amavisd-new", "arc", "arj", "cabextract",
|
"libdbi-perl",
|
||||||
"liblz4-tool", "lrzip", "lzop", "p7zip-full", "rpm2cpio",
|
"amavisd-new",
|
||||||
|
"arc",
|
||||||
|
"arj",
|
||||||
|
"cabextract",
|
||||||
|
"liblz4-tool",
|
||||||
|
"lrzip",
|
||||||
|
"lzop",
|
||||||
|
"p7zip-full",
|
||||||
|
"rpm2cpio",
|
||||||
"unrar-free",
|
"unrar-free",
|
||||||
],
|
],
|
||||||
"rpm": [
|
"rpm": [
|
||||||
"amavisd-new", "arj", "lz4", "lzop", "p7zip",
|
"amavisd-new",
|
||||||
|
"arj",
|
||||||
|
"lz4",
|
||||||
|
"lzop",
|
||||||
|
"p7zip",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
with_db = True
|
with_db = True
|
||||||
@@ -43,8 +54,10 @@ class Amavis(base.Installer):
|
|||||||
"""Return appropriate config files."""
|
"""Return appropriate config files."""
|
||||||
if package.backend.FORMAT == "deb":
|
if package.backend.FORMAT == "deb":
|
||||||
return [
|
return [
|
||||||
"conf.d/05-node_id", "conf.d/15-content_filter_mode",
|
"conf.d/05-node_id",
|
||||||
"conf.d/50-user"]
|
"conf.d/15-content_filter_mode",
|
||||||
|
"conf.d/50-user",
|
||||||
|
]
|
||||||
return ["amavisd.conf"]
|
return ["amavisd.conf"]
|
||||||
|
|
||||||
def get_packages(self):
|
def get_packages(self):
|
||||||
@@ -52,7 +65,17 @@ class Amavis(base.Installer):
|
|||||||
packages = super(Amavis, self).get_packages()
|
packages = super(Amavis, self).get_packages()
|
||||||
if package.backend.FORMAT == "deb":
|
if package.backend.FORMAT == "deb":
|
||||||
db_driver = "pg" if self.db_driver == "pgsql" else self.db_driver
|
db_driver = "pg" if self.db_driver == "pgsql" else self.db_driver
|
||||||
return packages + ["libdbd-{}-perl".format(db_driver)]
|
packages += ["libdbd-{}-perl".format(db_driver)]
|
||||||
|
|
||||||
|
name, version = utils.dist_info()
|
||||||
|
try:
|
||||||
|
major_version = int(version.split(".")[0])
|
||||||
|
except ValueError:
|
||||||
|
major_version = 0
|
||||||
|
if major_version >= 13:
|
||||||
|
packages = [p if p != "liblz4-tool" else "lz4" for p in packages]
|
||||||
|
return packages
|
||||||
|
|
||||||
if self.db_driver == "pgsql":
|
if self.db_driver == "pgsql":
|
||||||
db_driver = "Pg"
|
db_driver = "Pg"
|
||||||
elif self.db_driver == "mysql":
|
elif self.db_driver == "mysql":
|
||||||
@@ -61,9 +84,9 @@ class Amavis(base.Installer):
|
|||||||
raise NotImplementedError("DB driver not supported")
|
raise NotImplementedError("DB driver not supported")
|
||||||
packages += ["perl-DBD-{}".format(db_driver)]
|
packages += ["perl-DBD-{}".format(db_driver)]
|
||||||
name, version = utils.dist_info()
|
name, version = utils.dist_info()
|
||||||
if version.startswith('7'):
|
if version.startswith("7"):
|
||||||
packages += ["cabextract", "lrzip", "unar", "unzoo"]
|
packages += ["cabextract", "lrzip", "unar", "unzoo"]
|
||||||
elif version.startswith('8'):
|
elif version.startswith("8"):
|
||||||
packages += ["perl-IO-stringy"]
|
packages += ["perl-IO-stringy"]
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
@@ -75,12 +98,10 @@ class Amavis(base.Installer):
|
|||||||
version = package.backend.get_installed_version("amavis")
|
version = package.backend.get_installed_version("amavis")
|
||||||
if version is None:
|
if version is None:
|
||||||
raise utils.FatalError("Amavis is not installed")
|
raise utils.FatalError("Amavis is not installed")
|
||||||
path = self.get_file_path(
|
path = self.get_file_path("amavis_{}_{}.sql".format(self.dbengine, version))
|
||||||
"amavis_{}_{}.sql".format(self.dbengine, version))
|
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
version = ".".join(version.split(".")[:-1]) + ".X"
|
version = ".".join(version.split(".")[:-1]) + ".X"
|
||||||
path = self.get_file_path(
|
path = self.get_file_path("amavis_{}_{}.sql".format(self.dbengine, version))
|
||||||
"amavis_{}_{}.sql".format(self.dbengine, version))
|
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise utils.FatalError("Failed to find amavis database schema")
|
raise utils.FatalError("Failed to find amavis database schema")
|
||||||
return path
|
return path
|
||||||
@@ -97,20 +118,21 @@ class Amavis(base.Installer):
|
|||||||
|
|
||||||
def custom_backup(self, path):
|
def custom_backup(self, path):
|
||||||
"""Backup custom configuration if any."""
|
"""Backup custom configuration if any."""
|
||||||
if package.backend.FORMAT == "deb":
|
|
||||||
amavis_custom = f"{self.config_dir}/conf.d/99-custom"
|
amavis_custom = f"{self.config_dir}/conf.d/99-custom"
|
||||||
if os.path.isfile(amavis_custom):
|
if os.path.isfile(amavis_custom):
|
||||||
utils.copy_file(amavis_custom, path)
|
utils.copy_file(amavis_custom, path)
|
||||||
utils.success("Amavis custom configuration saved!")
|
utils.success("Amavis custom configuration saved!")
|
||||||
backup("spamassassin", self.config, os.path.dirname(path))
|
backup("spamassassin", self.config, self.base_backup_path)
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
"""Restore custom config files."""
|
"""Restore custom config files."""
|
||||||
if package.backend.FORMAT != "deb":
|
if package.backend.FORMAT != "deb":
|
||||||
return
|
return
|
||||||
amavis_custom_configuration = os.path.join(
|
amavis_custom_configuration = os.path.join(
|
||||||
self.archive_path, "custom/99-custom")
|
self.archive_path, "custom/amavis/99-custom"
|
||||||
|
)
|
||||||
if os.path.isfile(amavis_custom_configuration):
|
if os.path.isfile(amavis_custom_configuration):
|
||||||
utils.copy_file(amavis_custom_configuration, os.path.join(
|
utils.copy_file(
|
||||||
self.config_dir, "conf.d"))
|
amavis_custom_configuration, os.path.join(self.config_dir, "conf.d")
|
||||||
|
)
|
||||||
utils.success("Custom amavis configuration restored.")
|
utils.success("Custom amavis configuration restored.")
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
"""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, *args, **kwargs):
|
|
||||||
"""Get configuration."""
|
|
||||||
super(Automx, self).__init__(*args, **kwargs)
|
|
||||||
self.venv_path = self.config.get("automx", "venv_path")
|
|
||||||
self.instance_path = self.config.get("automx", "instance_path")
|
|
||||||
|
|
||||||
def get_template_context(self):
|
|
||||||
"""Additional variables."""
|
|
||||||
context = super(Automx, self).get_template_context()
|
|
||||||
sql_dsn = "{}://{}:{}@{}:{}/{}".format(
|
|
||||||
"postgresql" if self.dbengine == "postgres" else self.dbengine,
|
|
||||||
self.config.get("modoboa", "dbuser"),
|
|
||||||
self.config.get("modoboa", "dbpassword"),
|
|
||||||
self.dbhost,
|
|
||||||
self.dbport,
|
|
||||||
self.config.get("modoboa", "dbname"))
|
|
||||||
if self.db_driver == "pgsql":
|
|
||||||
sql_query = (
|
|
||||||
"SELECT first_name || ' ' || last_name AS display_name, email"
|
|
||||||
", SPLIT_PART(email, '@', 2) AS domain "
|
|
||||||
"FROM core_user WHERE email='%s' AND is_active;")
|
|
||||||
else:
|
|
||||||
sql_query = (
|
|
||||||
"SELECT concat(first_name, ' ', last_name) AS display_name, "
|
|
||||||
"email, SUBSTRING_INDEX(email, '@', -1) AS domain "
|
|
||||||
"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 < 2.0", "python-memcached",
|
|
||||||
"python-dateutil", "configparser"
|
|
||||||
]
|
|
||||||
if self.dbengine == "postgres":
|
|
||||||
packages.append("psycopg2-binary")
|
|
||||||
else:
|
|
||||||
packages.append("mysqlclient")
|
|
||||||
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")
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
"""Backup script for pre-installed instance."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pwd
|
|
||||||
import shutil
|
|
||||||
import stat
|
|
||||||
import sys
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from .. import database
|
|
||||||
from .. import utils
|
|
||||||
from ..constants import DEFAULT_BACKUP_DIRECTORY
|
|
||||||
|
|
||||||
|
|
||||||
class Backup:
|
|
||||||
"""
|
|
||||||
Backup structure ( {optional} ):
|
|
||||||
{{backup_directory}}
|
|
||||||
||
|
|
||||||
||--> installer.cfg
|
|
||||||
||--> custom
|
|
||||||
|--> { (copy of) /etc/amavis/conf.d/99-custom }
|
|
||||||
|--> { (copy of) /etc/postfix/custom_whitelist.cidr }
|
|
||||||
|--> { (copy of) dkim directory }
|
|
||||||
|--> {dkim.pem}...
|
|
||||||
|--> { (copy of) radicale home_dir }
|
|
||||||
||--> databases
|
|
||||||
|--> modoboa.sql
|
|
||||||
|--> { amavis.sql }
|
|
||||||
|--> { spamassassin.sql }
|
|
||||||
||--> mails
|
|
||||||
|--> vmails
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config, silent_backup, backup_path, nomail):
|
|
||||||
self.config = config
|
|
||||||
self.backup_path = backup_path
|
|
||||||
self.nomail = nomail
|
|
||||||
self.silent_backup = silent_backup
|
|
||||||
|
|
||||||
def validate_path(self, path):
|
|
||||||
"""Check basic condition for backup directory."""
|
|
||||||
|
|
||||||
path_exists = os.path.exists(path)
|
|
||||||
|
|
||||||
if path_exists and os.path.isfile(path):
|
|
||||||
utils.error("Error, you provided a file instead of a directory!")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not path_exists:
|
|
||||||
if not self.silent_backup:
|
|
||||||
create_dir = input(
|
|
||||||
f"\"{path}\" doesn't exist, would you like to create it? [Y/n]\n").lower()
|
|
||||||
|
|
||||||
if self.silent_backup or (not self.silent_backup and create_dir.startswith("y")):
|
|
||||||
pw = pwd.getpwnam("root")
|
|
||||||
utils.mkdir_safe(path, stat.S_IRWXU |
|
|
||||||
stat.S_IRWXG, pw[2], pw[3])
|
|
||||||
else:
|
|
||||||
utils.error("Error, backup directory not present.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if len(os.listdir(path)) != 0:
|
|
||||||
if not self.silent_backup:
|
|
||||||
delete_dir = input(
|
|
||||||
"Warning: backup directory is not empty, it will be purged if you continue... [Y/n]\n").lower()
|
|
||||||
|
|
||||||
if self.silent_backup or (not self.silent_backup and delete_dir.startswith("y")):
|
|
||||||
try:
|
|
||||||
os.remove(os.path.join(path, "installer.cfg"))
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
shutil.rmtree(os.path.join(path, "custom"),
|
|
||||||
ignore_errors=False)
|
|
||||||
shutil.rmtree(os.path.join(path, "mails"), ignore_errors=False)
|
|
||||||
shutil.rmtree(os.path.join(path, "databases"),
|
|
||||||
ignore_errors=False)
|
|
||||||
else:
|
|
||||||
utils.error("Error: backup directory not clean.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.backup_path = path
|
|
||||||
|
|
||||||
pw = pwd.getpwnam("root")
|
|
||||||
for dir in ["custom/", "databases/"]:
|
|
||||||
utils.mkdir_safe(os.path.join(self.backup_path, dir),
|
|
||||||
stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set_path(self):
|
|
||||||
"""Setup backup directory."""
|
|
||||||
if self.silent_backup:
|
|
||||||
if self.backup_path is None:
|
|
||||||
if self.config.has_option("backup", "default_path"):
|
|
||||||
path = self.config.get("backup", "default_path")
|
|
||||||
else:
|
|
||||||
path = DEFAULT_BACKUP_DIRECTORY
|
|
||||||
date = datetime.datetime.now().strftime("%m_%d_%Y_%H_%M")
|
|
||||||
path = os.path.join(path, f"backup_{date}")
|
|
||||||
self.validate_path(path)
|
|
||||||
else:
|
|
||||||
if not self.validate_path(self.backup_path):
|
|
||||||
utils.printcolor(
|
|
||||||
f"Path provided: {self.backup_path}", utils.BLUE)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
user_value = None
|
|
||||||
while user_value == "" or user_value is None or not self.validate_path(user_value):
|
|
||||||
utils.printcolor(
|
|
||||||
"Enter backup path (it must be an empty directory)", utils.MAGENTA)
|
|
||||||
utils.printcolor("CTRL+C to cancel", utils.MAGENTA)
|
|
||||||
user_value = utils.user_input("-> ")
|
|
||||||
|
|
||||||
def config_file_backup(self):
|
|
||||||
utils.copy_file("installer.cfg", self.backup_path)
|
|
||||||
|
|
||||||
def mail_backup(self):
|
|
||||||
if self.nomail:
|
|
||||||
utils.printcolor(
|
|
||||||
"Skipping mail backup, no-mail argument provided", utils.MAGENTA)
|
|
||||||
return
|
|
||||||
|
|
||||||
utils.printcolor("Backing up mails", utils.MAGENTA)
|
|
||||||
|
|
||||||
home_path = self.config.get("dovecot", "home_dir")
|
|
||||||
|
|
||||||
if not os.path.exists(home_path) or os.path.isfile(home_path):
|
|
||||||
utils.error("Error backing up Email, provided path "
|
|
||||||
f" ({home_path}) seems not right...")
|
|
||||||
|
|
||||||
else:
|
|
||||||
dst = os.path.join(self.backup_path, "mails/")
|
|
||||||
|
|
||||||
if os.path.exists(dst):
|
|
||||||
shutil.rmtree(dst)
|
|
||||||
|
|
||||||
shutil.copytree(home_path, dst)
|
|
||||||
utils.printcolor("Mail backup complete!", utils.GREEN)
|
|
||||||
|
|
||||||
def custom_config_backup(self):
|
|
||||||
"""
|
|
||||||
Custom config :
|
|
||||||
- DKIM keys: {{keys_storage_dir}}
|
|
||||||
- Radicale collection (calendars, contacts): {{home_dir}}
|
|
||||||
- Amavis : /etc/amavis/conf.d/99-custom
|
|
||||||
- Postwhite : /etc/postwhite.conf
|
|
||||||
Feel free to suggest to add others!
|
|
||||||
"""
|
|
||||||
utils.printcolor(
|
|
||||||
"Backing up some custom configuration...", utils.MAGENTA)
|
|
||||||
|
|
||||||
custom_path = os.path.join(
|
|
||||||
self.backup_path, "custom")
|
|
||||||
|
|
||||||
# DKIM Key
|
|
||||||
if (self.config.has_option("opendkim", "enabled") and
|
|
||||||
self.config.getboolean("opendkim", "enabled")):
|
|
||||||
dkim_keys = self.config.get(
|
|
||||||
"opendkim", "keys_storage_dir", fallback="/var/lib/dkim")
|
|
||||||
if os.path.isdir(dkim_keys):
|
|
||||||
shutil.copytree(dkim_keys, os.path.join(custom_path, "dkim"))
|
|
||||||
utils.printcolor(
|
|
||||||
"DKIM keys saved!", utils.GREEN)
|
|
||||||
|
|
||||||
# Radicale Collections
|
|
||||||
if (self.config.has_option("radicale", "enabled") and
|
|
||||||
self.config.getboolean("radicale", "enabled")):
|
|
||||||
radicale_backup = os.path.join(self.config.get(
|
|
||||||
"radicale", "home_dir", fallback="/srv/radicale"), "collections")
|
|
||||||
if os.path.isdir(radicale_backup):
|
|
||||||
shutil.copytree(radicale_backup, os.path.join(
|
|
||||||
custom_path, "radicale"))
|
|
||||||
utils.printcolor("Radicale files saved", utils.GREEN)
|
|
||||||
|
|
||||||
# AMAVIS
|
|
||||||
if (self.config.has_option("amavis", "enabled") and
|
|
||||||
self.config.getboolean("amavis", "enabled")):
|
|
||||||
amavis_custom = "/etc/amavis/conf.d/99-custom"
|
|
||||||
if os.path.isfile(amavis_custom):
|
|
||||||
utils.copy_file(amavis_custom, custom_path)
|
|
||||||
utils.printcolor(
|
|
||||||
"Amavis custom configuration saved!", utils.GREEN)
|
|
||||||
|
|
||||||
# POSTWHITE
|
|
||||||
if (self.config.has_option("postwhite", "enabled") and
|
|
||||||
self.config.getboolean("postwhite", "enabled")):
|
|
||||||
postswhite_custom = "/etc/postwhite.conf"
|
|
||||||
if os.path.isfile(postswhite_custom):
|
|
||||||
utils.copy_file(postswhite_custom, custom_path)
|
|
||||||
utils.printcolor(
|
|
||||||
"Postwhite configuration saved!", utils.GREEN)
|
|
||||||
|
|
||||||
def database_backup(self):
|
|
||||||
"""Backing up databases"""
|
|
||||||
|
|
||||||
utils.printcolor("Backing up databases...", utils.MAGENTA)
|
|
||||||
|
|
||||||
self.database_dump("modoboa")
|
|
||||||
self.database_dump("amavis")
|
|
||||||
self.database_dump("spamassassin")
|
|
||||||
|
|
||||||
def database_dump(self, app_name):
|
|
||||||
|
|
||||||
dump_path = os.path.join(self.backup_path, "databases")
|
|
||||||
backend = database.get_backend(self.config)
|
|
||||||
|
|
||||||
if app_name == "modoboa" or (self.config.has_option(app_name, "enabled") and
|
|
||||||
self.config.getboolean(app_name, "enabled")):
|
|
||||||
dbname = self.config.get(app_name, "dbname")
|
|
||||||
dbuser = self.config.get(app_name, "dbuser")
|
|
||||||
dbpasswd = self.config.get(app_name, "dbpassword")
|
|
||||||
backend.dump_database(dbname, dbuser, dbpasswd,
|
|
||||||
os.path.join(dump_path, f"{app_name}.sql"))
|
|
||||||
|
|
||||||
def backup_completed(self):
|
|
||||||
utils.printcolor("Backup process done, your backup is available here:"
|
|
||||||
f"--> {self.backup_path}", utils.GREEN)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.set_path()
|
|
||||||
self.config_file_backup()
|
|
||||||
self.mail_backup()
|
|
||||||
self.custom_config_backup()
|
|
||||||
self.database_backup()
|
|
||||||
self.backup_completed()
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .. import database
|
from .. import database
|
||||||
from .. import package
|
from .. import package
|
||||||
@@ -13,15 +14,15 @@ from .. import utils
|
|||||||
class Installer:
|
class Installer:
|
||||||
"""Simple installer for one application."""
|
"""Simple installer for one application."""
|
||||||
|
|
||||||
appname = None
|
appname: str
|
||||||
no_daemon = False
|
no_daemon: bool = False
|
||||||
daemon_name = None
|
daemon_name: Optional[str] = None
|
||||||
packages = {}
|
packages: dict[str, list[str]] = {}
|
||||||
with_user = False
|
with_user: bool = False
|
||||||
with_db = False
|
with_db: bool = False
|
||||||
config_files = []
|
config_files: list[str] = []
|
||||||
|
|
||||||
def __init__(self, config, upgrade: bool, archive_path: str):
|
def __init__(self, config, upgrade: bool, archive_path: str) -> None:
|
||||||
"""Get configuration."""
|
"""Get configuration."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.upgrade = upgrade
|
self.upgrade = upgrade
|
||||||
@@ -30,12 +31,12 @@ class Installer:
|
|||||||
self.app_config = dict(self.config.items(self.appname))
|
self.app_config = dict(self.config.items(self.appname))
|
||||||
self.dbengine = self.config.get("database", "engine")
|
self.dbengine = self.config.get("database", "engine")
|
||||||
# Used to install system packages
|
# Used to install system packages
|
||||||
self.db_driver = (
|
self.db_driver = "pgsql" if self.dbengine == "postgres" else self.dbengine
|
||||||
"pgsql" if self.dbengine == "postgres" else self.dbengine)
|
|
||||||
self.backend = database.get_backend(self.config)
|
self.backend = database.get_backend(self.config)
|
||||||
self.dbhost = self.config.get("database", "host")
|
self.dbhost = self.config.get("database", "host")
|
||||||
self.dbport = self.config.get(
|
self.dbport = self.config.get(
|
||||||
"database", "port", fallback=self.backend.default_port)
|
"database", "port", fallback=self.backend.default_port
|
||||||
|
)
|
||||||
self._config_dir = None
|
self._config_dir = None
|
||||||
if not self.with_db:
|
if not self.with_db:
|
||||||
return
|
return
|
||||||
@@ -44,24 +45,24 @@ class Installer:
|
|||||||
self.dbpasswd = self.config.get(self.appname, "dbpassword")
|
self.dbpasswd = self.config.get(self.appname, "dbpassword")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modoboa_2_2_or_greater(self):
|
def modoboa_2_2_or_greater(self) -> bool:
|
||||||
# Check if modoboa version > 2.2
|
# Check if modoboa version > 2.2
|
||||||
modoboa_version = python.get_package_version(
|
modoboa_version = python.get_package_version(
|
||||||
"modoboa",
|
"modoboa",
|
||||||
self.config.get("modoboa", "venv_path"),
|
self.config.get("modoboa", "venv_path"),
|
||||||
sudo_user=self.config.get("modoboa", "user")
|
sudo_user=self.config.get("modoboa", "user"),
|
||||||
)
|
)
|
||||||
condition = (
|
condition = (
|
||||||
(int(modoboa_version[0]) == 2 and int(modoboa_version[1]) >= 2) or
|
int(modoboa_version[0]) == 2 and int(modoboa_version[1]) >= 2
|
||||||
int(modoboa_version[0]) > 2
|
) or int(modoboa_version[0]) > 2
|
||||||
)
|
|
||||||
return condition
|
return condition
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_dir(self):
|
def config_dir(self):
|
||||||
"""Return main configuration directory."""
|
"""Return main configuration directory."""
|
||||||
if self._config_dir is None and self.config.has_option(
|
if self._config_dir is None and self.config.has_option(
|
||||||
self.appname, "config_dir"):
|
self.appname, "config_dir"
|
||||||
|
):
|
||||||
self._config_dir = self.config.get(self.appname, "config_dir")
|
self._config_dir = self.config.get(self.appname, "config_dir")
|
||||||
return self._config_dir
|
return self._config_dir
|
||||||
|
|
||||||
@@ -72,11 +73,11 @@ class Installer:
|
|||||||
def get_sql_schema_from_backup(self):
|
def get_sql_schema_from_backup(self):
|
||||||
"""Retrieve a dump path from a previous backup."""
|
"""Retrieve a dump path from a previous backup."""
|
||||||
utils.printcolor(
|
utils.printcolor(
|
||||||
f"Trying to restore {self.appname} database from backup.",
|
f"Trying to restore {self.appname} database from backup.", utils.MAGENTA
|
||||||
utils.MAGENTA
|
|
||||||
)
|
)
|
||||||
database_backup_path = os.path.join(
|
database_backup_path = os.path.join(
|
||||||
self.archive_path, f"databases/{self.appname}.sql")
|
self.archive_path, f"databases/{self.appname}.sql"
|
||||||
|
)
|
||||||
if os.path.isfile(database_backup_path):
|
if os.path.isfile(database_backup_path):
|
||||||
utils.success(f"SQL dump found in backup for {self.appname}!")
|
utils.success(f"SQL dump found in backup for {self.appname}!")
|
||||||
return database_backup_path
|
return database_backup_path
|
||||||
@@ -85,8 +86,7 @@ class Installer:
|
|||||||
def get_file_path(self, fname):
|
def get_file_path(self, fname):
|
||||||
"""Return the absolute path of this file."""
|
"""Return the absolute path of this file."""
|
||||||
return os.path.abspath(
|
return os.path.abspath(
|
||||||
os.path.join(
|
os.path.join(os.path.dirname(__file__), "files", self.appname, fname)
|
||||||
os.path.dirname(__file__), "files", self.appname, fname)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def setup_database(self):
|
def setup_database(self):
|
||||||
@@ -101,8 +101,7 @@ class Installer:
|
|||||||
if not schema:
|
if not schema:
|
||||||
schema = self.get_sql_schema_path()
|
schema = self.get_sql_schema_path()
|
||||||
if schema:
|
if schema:
|
||||||
self.backend.load_sql_file(
|
self.backend.load_sql_file(self.dbname, self.dbuser, self.dbpasswd, schema)
|
||||||
self.dbname, self.dbuser, self.dbpasswd, schema)
|
|
||||||
|
|
||||||
def setup_user(self):
|
def setup_user(self):
|
||||||
"""Setup a system user."""
|
"""Setup a system user."""
|
||||||
@@ -118,8 +117,7 @@ class Installer:
|
|||||||
def get_template_context(self):
|
def get_template_context(self):
|
||||||
"""Return context used for template rendering."""
|
"""Return context used for template rendering."""
|
||||||
context = {
|
context = {
|
||||||
"dbengine": (
|
"dbengine": ("Pg" if self.dbengine == "postgres" else self.dbengine),
|
||||||
"Pg" if self.dbengine == "postgres" else self.dbengine),
|
|
||||||
"dbhost": self.dbhost,
|
"dbhost": self.dbhost,
|
||||||
"dbport": self.dbport,
|
"dbport": self.dbport,
|
||||||
}
|
}
|
||||||
@@ -171,9 +169,11 @@ class Installer:
|
|||||||
utils.copy_from_template(src, dst, context)
|
utils.copy_from_template(src, dst, context)
|
||||||
|
|
||||||
def backup(self, path):
|
def backup(self, path):
|
||||||
|
self.base_backup_path = path
|
||||||
if self.with_db:
|
if self.with_db:
|
||||||
self._dump_database(path)
|
self._dump_database(path)
|
||||||
custom_backup_path = os.path.join(path, "custom")
|
custom_backup_path = os.path.join(path, "custom", self.appname)
|
||||||
|
utils.mkdir_safe(custom_backup_path)
|
||||||
self.custom_backup(custom_backup_path)
|
self.custom_backup(custom_backup_path)
|
||||||
|
|
||||||
def custom_backup(self, path):
|
def custom_backup(self, path):
|
||||||
@@ -212,8 +212,7 @@ class Installer:
|
|||||||
"""Create a new database dump for this app."""
|
"""Create a new database dump for this app."""
|
||||||
target_dir = os.path.join(backup_path, "databases")
|
target_dir = os.path.join(backup_path, "databases")
|
||||||
target_file = os.path.join(target_dir, f"{self.appname}.sql")
|
target_file = os.path.join(target_dir, f"{self.appname}.sql")
|
||||||
self.backend.dump_database(
|
self.backend.dump_database(self.dbname, self.dbuser, self.dbpasswd, target_file)
|
||||||
self.dbname, self.dbuser, self.dbpasswd, target_file)
|
|
||||||
|
|
||||||
def pre_run(self):
|
def pre_run(self):
|
||||||
"""Tasks to execute before the installer starts."""
|
"""Tasks to execute before the installer starts."""
|
||||||
|
|||||||
@@ -23,10 +23,9 @@ class Dovecot(base.Installer):
|
|||||||
"dovecot-imapd",
|
"dovecot-imapd",
|
||||||
"dovecot-lmtpd",
|
"dovecot-lmtpd",
|
||||||
"dovecot-managesieved",
|
"dovecot-managesieved",
|
||||||
"dovecot-sieve"
|
"dovecot-sieve",
|
||||||
],
|
],
|
||||||
"rpm": [
|
"rpm": ["dovecot", "dovecot-pigeonhole"],
|
||||||
"dovecot", "dovecot-pigeonhole"]
|
|
||||||
}
|
}
|
||||||
per_version_config_files = {
|
per_version_config_files = {
|
||||||
"2.3": [
|
"2.3": [
|
||||||
@@ -45,9 +44,8 @@ class Dovecot(base.Installer):
|
|||||||
"conf.d/10-ssl.conf",
|
"conf.d/10-ssl.conf",
|
||||||
"conf.d/10-ssl-keys.try",
|
"conf.d/10-ssl-keys.try",
|
||||||
"conf.d/20-lmtp.conf",
|
"conf.d/20-lmtp.conf",
|
||||||
"conf.d/30-dict-server.conf",
|
|
||||||
"conf.d/auth-oauth2.conf.ext",
|
"conf.d/auth-oauth2.conf.ext",
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
with_user = True
|
with_user = True
|
||||||
|
|
||||||
@@ -69,11 +67,12 @@ class Dovecot(base.Installer):
|
|||||||
files += [
|
files += [
|
||||||
f"conf.d/auth-sql-{self.dbengine}.conf.ext=conf.d/auth-sql.conf.ext",
|
f"conf.d/auth-sql-{self.dbengine}.conf.ext=conf.d/auth-sql.conf.ext",
|
||||||
f"conf.d/auth-master-{self.dbengine}.conf.ext=conf.d/auth-master.conf.ext",
|
f"conf.d/auth-master-{self.dbengine}.conf.ext=conf.d/auth-master.conf.ext",
|
||||||
|
f"conf.d/30-dict-server-{self.dbengine}.conf=conf.d/30-dict-server.conf",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
files += [
|
files += [
|
||||||
f"dovecot-sql-{self.dbengine}.conf.ext=dovecot-sql.conf.ext",
|
f"dovecot-sql-{self.dbengine}.conf.ext=dovecot-sql.conf.ext",
|
||||||
f"dovecot-sql-master-{self.dbengine}.conf.ext=dovecot-sql-master.conf.ext"
|
f"dovecot-sql-master-{self.dbengine}.conf.ext=dovecot-sql-master.conf.ext",
|
||||||
]
|
]
|
||||||
result = []
|
result = []
|
||||||
for path in files:
|
for path in files:
|
||||||
@@ -107,7 +106,9 @@ class Dovecot(base.Installer):
|
|||||||
packages += super().get_packages()
|
packages += super().get_packages()
|
||||||
backports_codename = getattr(self, "backports_codename", None)
|
backports_codename = getattr(self, "backports_codename", None)
|
||||||
if backports_codename:
|
if backports_codename:
|
||||||
packages = [f"{package}/{backports_codename}-backports" for package in packages]
|
packages = [
|
||||||
|
f"{package}/{backports_codename}-backports" for package in packages
|
||||||
|
]
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
def install_packages(self):
|
def install_packages(self):
|
||||||
@@ -118,7 +119,8 @@ class Dovecot(base.Installer):
|
|||||||
package.backend.enable_backports("bookworm")
|
package.backend.enable_backports("bookworm")
|
||||||
self.backports_codename = "bookworm"
|
self.backports_codename = "bookworm"
|
||||||
package.backend.preconfigure(
|
package.backend.preconfigure(
|
||||||
"dovecot-core", "create-ssl-cert", "boolean", "false")
|
"dovecot-core", "create-ssl-cert", "boolean", "false"
|
||||||
|
)
|
||||||
super().install_packages()
|
super().install_packages()
|
||||||
|
|
||||||
def get_template_context(self):
|
def get_template_context(self):
|
||||||
@@ -127,11 +129,17 @@ class Dovecot(base.Installer):
|
|||||||
pw_mailbox = pwd.getpwnam(self.mailboxes_owner)
|
pw_mailbox = pwd.getpwnam(self.mailboxes_owner)
|
||||||
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
|
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
|
||||||
ssl_protocol_parameter = "ssl_protocols"
|
ssl_protocol_parameter = "ssl_protocols"
|
||||||
if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3":
|
if (
|
||||||
|
package.backend.get_installed_version(
|
||||||
|
dovecot_package[package.backend.FORMAT]
|
||||||
|
)
|
||||||
|
> "2.3"
|
||||||
|
):
|
||||||
ssl_protocol_parameter = "ssl_min_protocol"
|
ssl_protocol_parameter = "ssl_min_protocol"
|
||||||
ssl_protocols = "!SSLv2 !SSLv3"
|
ssl_protocols = "!SSLv2 !SSLv3"
|
||||||
if package.backend.get_installed_version("openssl").startswith("1.1") \
|
if package.backend.get_installed_version("openssl").startswith(
|
||||||
or package.backend.get_installed_version("openssl").startswith("3"):
|
"1.1"
|
||||||
|
) or package.backend.get_installed_version("openssl").startswith("3"):
|
||||||
ssl_protocols = "!SSLv3"
|
ssl_protocols = "!SSLv3"
|
||||||
if ssl_protocol_parameter == "ssl_min_protocol":
|
if ssl_protocol_parameter == "ssl_min_protocol":
|
||||||
ssl_protocols = "TLSv1.2"
|
ssl_protocols = "TLSv1.2"
|
||||||
@@ -145,14 +153,16 @@ class Dovecot(base.Installer):
|
|||||||
protocols = ""
|
protocols = ""
|
||||||
|
|
||||||
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
||||||
"Dovecot", "dovecot", self.config)
|
"Dovecot", "dovecot", self.config
|
||||||
|
)
|
||||||
hostname = self.config.get("general", "hostname")
|
hostname = self.config.get("general", "hostname")
|
||||||
oauth2_introspection_url = (
|
oauth2_introspection_url = (
|
||||||
f"https://{oauth2_client_id}:{oauth2_client_secret}"
|
f"https://{oauth2_client_id}:{oauth2_client_secret}"
|
||||||
f"@{hostname}/api/o/introspect/"
|
f"@{hostname}/api/o/introspect/"
|
||||||
)
|
)
|
||||||
|
|
||||||
context.update({
|
context.update(
|
||||||
|
{
|
||||||
"db_driver": self.db_driver,
|
"db_driver": self.db_driver,
|
||||||
"mailboxes_owner_uid": pw_mailbox[2],
|
"mailboxes_owner_uid": pw_mailbox[2],
|
||||||
"mailboxes_owner_gid": pw_mailbox[3],
|
"mailboxes_owner_gid": pw_mailbox[3],
|
||||||
@@ -165,11 +175,16 @@ class Dovecot(base.Installer):
|
|||||||
"ssl_protocols": ssl_protocols,
|
"ssl_protocols": ssl_protocols,
|
||||||
"ssl_protocol_parameter": ssl_protocol_parameter,
|
"ssl_protocol_parameter": ssl_protocol_parameter,
|
||||||
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
|
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
|
||||||
"not_modoboa_2_2_or_greater": "" if not self.modoboa_2_2_or_greater else "#",
|
"not_modoboa_2_2_or_greater": (
|
||||||
"do_move_spam_to_junk": "" if self.app_config["move_spam_to_junk"] else "#",
|
"" if not self.modoboa_2_2_or_greater else "#"
|
||||||
|
),
|
||||||
|
"do_move_spam_to_junk": (
|
||||||
|
"" if self.app_config["move_spam_to_junk"] else "#"
|
||||||
|
),
|
||||||
"oauth2_introspection_url": oauth2_introspection_url,
|
"oauth2_introspection_url": oauth2_introspection_url,
|
||||||
"radicale_user": self.config.get("radicale", "user"),
|
"radicale_user": self.config.get("radicale", "user"),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def install_config_files(self):
|
def install_config_files(self):
|
||||||
@@ -177,9 +192,13 @@ class Dovecot(base.Installer):
|
|||||||
if self.app_config["move_spam_to_junk"]:
|
if self.app_config["move_spam_to_junk"]:
|
||||||
utils.mkdir_safe(
|
utils.mkdir_safe(
|
||||||
f"{self.config_dir}/conf.d/custom_after_sieve",
|
f"{self.config_dir}/conf.d/custom_after_sieve",
|
||||||
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
|
stat.S_IRWXU
|
||||||
stat.S_IROTH | stat.S_IXOTH,
|
| stat.S_IRGRP
|
||||||
0, 0
|
| stat.S_IXGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
| stat.S_IXOTH,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
super().install_config_files()
|
super().install_config_files()
|
||||||
|
|
||||||
@@ -191,14 +210,18 @@ class Dovecot(base.Installer):
|
|||||||
dbpassword = self.config.get("modoboa", "dbpassword")
|
dbpassword = self.config.get("modoboa", "dbpassword")
|
||||||
backend = database.get_backend(self.config)
|
backend = database.get_backend(self.config)
|
||||||
backend.load_sql_file(
|
backend.load_sql_file(
|
||||||
dbname, dbuser, dbpassword,
|
dbname,
|
||||||
self.get_file_path("install_modoboa_postgres_trigger.sql")
|
dbuser,
|
||||||
|
dbpassword,
|
||||||
|
self.get_file_path("install_modoboa_postgres_trigger.sql"),
|
||||||
)
|
)
|
||||||
backend.load_sql_file(
|
backend.load_sql_file(
|
||||||
dbname, dbuser, dbpassword,
|
dbname,
|
||||||
self.get_file_path("fix_modoboa_postgres_schema.sql")
|
dbuser,
|
||||||
|
dbpassword,
|
||||||
|
self.get_file_path("fix_modoboa_postgres_schema.sql"),
|
||||||
)
|
)
|
||||||
for f in glob.glob(f"{self.version}/{self.get_file_path('conf.d')}/*"):
|
for f in glob.glob(f"{self.get_file_path(f'{self.version}/conf.d')}/*"):
|
||||||
if os.path.isfile(f):
|
if os.path.isfile(f):
|
||||||
utils.copy_file(f, "{}/conf.d".format(self.config_dir))
|
utils.copy_file(f, "{}/conf.d".format(self.config_dir))
|
||||||
# Make postlogin script executable
|
# Make postlogin script executable
|
||||||
@@ -210,9 +233,11 @@ class Dovecot(base.Installer):
|
|||||||
# See https://github.com/modoboa/modoboa/issues/2157.
|
# See https://github.com/modoboa/modoboa/issues/2157.
|
||||||
if self.app_config["move_spam_to_junk"]:
|
if self.app_config["move_spam_to_junk"]:
|
||||||
# Compile sieve script
|
# Compile sieve script
|
||||||
sieve_file = f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve"
|
sieve_file = (
|
||||||
|
f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve"
|
||||||
|
)
|
||||||
utils.exec_cmd(f"/usr/bin/sievec {sieve_file}")
|
utils.exec_cmd(f"/usr/bin/sievec {sieve_file}")
|
||||||
system.add_user_to_group(self.mailboxes_owner, 'dovecot')
|
system.add_user_to_group(self.mailboxes_owner, "dovecot")
|
||||||
|
|
||||||
def restart_daemon(self):
|
def restart_daemon(self):
|
||||||
"""Restart daemon process.
|
"""Restart daemon process.
|
||||||
@@ -226,7 +251,8 @@ class Dovecot(base.Installer):
|
|||||||
action = "start" if code else "restart"
|
action = "start" if code else "restart"
|
||||||
utils.exec_cmd(
|
utils.exec_cmd(
|
||||||
"service {} {} > /dev/null 2>&1".format(self.appname, action),
|
"service {} {} > /dev/null 2>&1".format(self.appname, action),
|
||||||
capture_output=False)
|
capture_output=False,
|
||||||
|
)
|
||||||
system.enable_service(self.get_daemon_name())
|
system.enable_service(self.get_daemon_name())
|
||||||
|
|
||||||
def backup(self, path):
|
def backup(self, path):
|
||||||
@@ -234,14 +260,16 @@ class Dovecot(base.Installer):
|
|||||||
home_dir = self.config.get("dovecot", "home_dir")
|
home_dir = self.config.get("dovecot", "home_dir")
|
||||||
utils.printcolor("Backing up mails", utils.MAGENTA)
|
utils.printcolor("Backing up mails", utils.MAGENTA)
|
||||||
if not os.path.exists(home_dir) or os.path.isfile(home_dir):
|
if not os.path.exists(home_dir) or os.path.isfile(home_dir):
|
||||||
utils.error("Error backing up emails, provided path "
|
utils.error(
|
||||||
f" ({home_dir}) seems not right...")
|
"Error backing up emails, provided path "
|
||||||
|
f" ({home_dir}) seems not right..."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
dst = os.path.join(path, "mails/")
|
dst = os.path.join(path, "mails/")
|
||||||
if os.path.exists(dst):
|
if os.path.exists(dst):
|
||||||
shutil.rmtree(dst)
|
shutil.rmtree(dst)
|
||||||
shutil.copytree(home_dir, dst)
|
shutil.copytree(home_dir, dst, dirs_exist_ok=True)
|
||||||
utils.success("Mail backup complete!")
|
utils.success("Mail backup complete!")
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
@@ -257,10 +285,13 @@ class Dovecot(base.Installer):
|
|||||||
for dirpath, dirnames, filenames in os.walk(home_dir):
|
for dirpath, dirnames, filenames in os.walk(home_dir):
|
||||||
shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner)
|
shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner)
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
shutil.chown(os.path.join(dirpath, filename),
|
shutil.chown(
|
||||||
self.mailboxes_owner, self.mailboxes_owner)
|
os.path.join(dirpath, filename),
|
||||||
|
self.mailboxes_owner,
|
||||||
|
self.mailboxes_owner,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
utils.printcolor(
|
utils.printcolor(
|
||||||
"It seems that emails were not backed up, skipping restoration.",
|
"It seems that emails were not backed up, skipping restoration.",
|
||||||
utils.MAGENTA
|
utils.MAGENTA,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
[automx]
|
|
||||||
provider = %domain
|
|
||||||
domains = *
|
|
||||||
|
|
||||||
#debug=yes
|
|
||||||
#logfile = /srv/automx/automx.log
|
|
||||||
|
|
||||||
# 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
|
|
||||||
action = settings
|
|
||||||
account_type = email
|
|
||||||
host = %sql_dsn
|
|
||||||
query = %sql_query
|
|
||||||
result_attrs = display_name, email
|
|
||||||
|
|
||||||
display_name = ${display_name}
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
##
|
||||||
|
## Dictionary server settings
|
||||||
|
##
|
||||||
|
|
||||||
|
# Dictionary can be used to store key=value lists. This is used by several
|
||||||
|
# plugins. The dictionary can be accessed either directly or though a
|
||||||
|
# dictionary server. The following dict block maps dictionary names to URIs
|
||||||
|
# when the server is used. These can then be referenced using URIs in format
|
||||||
|
# "proxy::<name>".
|
||||||
|
|
||||||
|
dict_server {
|
||||||
|
mysql %dbhost {
|
||||||
|
port = %dbport
|
||||||
|
dbname = %modoboa_dbname
|
||||||
|
user = %modoboa_dbuser
|
||||||
|
password = %modoboa_dbpassword
|
||||||
|
}
|
||||||
|
dict quota {
|
||||||
|
driver = sql
|
||||||
|
sql_driver = %db_driver
|
||||||
|
hostname = %dbhost
|
||||||
|
|
||||||
|
dict_map priv/quota/storage {
|
||||||
|
sql_table = admin_quota
|
||||||
|
username_field = username
|
||||||
|
value_field bytes {
|
||||||
|
type = uint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dict_map priv/quota/messages {
|
||||||
|
sql_table = admin_quota
|
||||||
|
username_field = username
|
||||||
|
value_field messages {
|
||||||
|
type = uint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quota_clone {
|
||||||
|
dict proxy {
|
||||||
|
name = quota
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,21 +104,21 @@ mysql %dbhost {
|
|||||||
#
|
#
|
||||||
# Commonly used available substitutions (see https://doc.dovecot.org/latest/core/settings/variables.html
|
# Commonly used available substitutions (see https://doc.dovecot.org/latest/core/settings/variables.html
|
||||||
# for full list):
|
# for full list):
|
||||||
# %{user} = entire user@domain
|
# %%{user} = entire user@domain
|
||||||
# %{user|username} = user part of user@domain
|
# %%{user|username} = user part of user@domain
|
||||||
# %{user|domain} = domain part of user@domain
|
# %%{user|domain} = domain part of user@domain
|
||||||
#
|
#
|
||||||
# Note that these can be used only as input to SQL query. If the query outputs
|
# Note that these can be used only as input to SQL query. If the query outputs
|
||||||
# any of these substitutions, they're not touched. Otherwise it would be
|
# any of these substitutions, they're not touched. Otherwise it would be
|
||||||
# difficult to have eg. usernames containing '%' characters.
|
# difficult to have eg. usernames containing '%%' characters.
|
||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
# query = SELECT userid AS user, pw AS password \
|
# query = SELECT userid AS user, pw AS password \
|
||||||
# FROM users WHERE userid = '%u' AND active = 'Y'
|
# FROM users WHERE userid = '%%u' AND active = 'Y'
|
||||||
#
|
#
|
||||||
# query = \
|
# query = \
|
||||||
# SELECT userid as username, domain, password \
|
# SELECT userid as username, domain, password \
|
||||||
# FROM users WHERE userid = '%{user|username}' AND domain = '%{user|domain}'
|
# FROM users WHERE userid = '%%{user|username}' AND domain = '%%{user|domain}'
|
||||||
#}
|
#}
|
||||||
|
|
||||||
passdb sql {
|
passdb sql {
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
upstream automx {
|
|
||||||
server unix:%uwsgi_socket_path fail_timeout=0;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name %hostname;
|
server_name %hostname;
|
||||||
root /srv/automx/instance;
|
root %app_instance_path;
|
||||||
|
|
||||||
access_log /var/log/nginx/%{hostname}-access.log;
|
access_log /var/log/nginx/%{hostname}-access.log;
|
||||||
error_log /var/log/nginx/%{hostname}-error.log;
|
error_log /var/log/nginx/%{hostname}-error.log;
|
||||||
|
|
||||||
location /mail/config-v1.1.xml {
|
location ~ ^/(mail/config-v1.1.xml|mobileconfig) {
|
||||||
include uwsgi_params;
|
include uwsgi_params;
|
||||||
uwsgi_pass automx;
|
uwsgi_pass modoboa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,8 +102,6 @@ file = %{config_dir}/rights
|
|||||||
|
|
||||||
# Storage backend
|
# Storage backend
|
||||||
# Value: multifilesystem
|
# 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
|
# Folder for storing local collections, created if not present
|
||||||
filesystem_folder = %{home_dir}/collections
|
filesystem_folder = %{home_dir}/collections
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
[uwsgi]
|
|
||||||
uid = %app_user
|
|
||||||
gid = %app_user
|
|
||||||
plugins = %uwsgi_plugin
|
|
||||||
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
|
|
||||||
@@ -19,14 +19,15 @@ class Nginx(base.Installer):
|
|||||||
"rpm": ["nginx"]
|
"rpm": ["nginx"]
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_template_context(self, app):
|
def get_template_context(self):
|
||||||
"""Additionnal variables."""
|
"""Additionnal variables."""
|
||||||
context = super().get_template_context()
|
context = super().get_template_context()
|
||||||
context.update({
|
context.update({
|
||||||
"app_instance_path": (
|
"app_instance_path": (
|
||||||
self.config.get(app, "instance_path")),
|
self.config.get("modoboa", "instance_path")),
|
||||||
"uwsgi_socket_path": (
|
"uwsgi_socket_path": (
|
||||||
Uwsgi(self.config, self.upgrade, self.restore).get_socket_path(app))
|
Uwsgi(self.config, self.upgrade, self.restore).get_socket_path("modoboa")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -34,9 +35,10 @@ class Nginx(base.Installer):
|
|||||||
"""Custom app configuration."""
|
"""Custom app configuration."""
|
||||||
if hostname is None:
|
if hostname is None:
|
||||||
hostname = self.config.get("general", "hostname")
|
hostname = self.config.get("general", "hostname")
|
||||||
context = self.get_template_context(app)
|
context = self.get_template_context()
|
||||||
context.update({"hostname": hostname, "extra_config": extra_config})
|
context.update({"hostname": hostname, "extra_config": extra_config})
|
||||||
src = self.get_file_path("{}.conf.tpl".format(app))
|
src = self.get_file_path("{}.conf.tpl".format(app))
|
||||||
|
group = None
|
||||||
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))
|
||||||
@@ -46,6 +48,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)
|
||||||
|
if self.config.has_section(app):
|
||||||
group = self.config.get(app, "user")
|
group = self.config.get(app, "user")
|
||||||
user = "www-data"
|
user = "www-data"
|
||||||
else:
|
else:
|
||||||
@@ -54,25 +57,17 @@ class Nginx(base.Installer):
|
|||||||
utils.copy_from_template(src, dst, context)
|
utils.copy_from_template(src, dst, context)
|
||||||
group = "uwsgi"
|
group = "uwsgi"
|
||||||
user = "nginx"
|
user = "nginx"
|
||||||
|
if user and group:
|
||||||
system.add_user_to_group(user, group)
|
system.add_user_to_group(user, group)
|
||||||
|
|
||||||
def post_run(self):
|
def post_run(self):
|
||||||
"""Additionnal tasks."""
|
"""Additionnal tasks."""
|
||||||
extra_modoboa_config = ""
|
extra_modoboa_config = ""
|
||||||
if self.config.getboolean("automx", "enabled"):
|
|
||||||
hostname = "autoconfig.{}".format(
|
hostname = "autoconfig.{}".format(
|
||||||
self.config.get("general", "domain"))
|
self.config.get("general", "domain"))
|
||||||
self._setup_config("automx", hostname)
|
self._setup_config("autoconfig", hostname)
|
||||||
extra_modoboa_config = """
|
|
||||||
location ~* ^/autodiscover/autodiscover.xml {
|
|
||||||
include uwsgi_params;
|
|
||||||
uwsgi_pass automx;
|
|
||||||
}
|
|
||||||
location /mobileconfig {
|
|
||||||
include uwsgi_params;
|
|
||||||
uwsgi_pass automx;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
if self.config.get("radicale", "enabled"):
|
if self.config.get("radicale", "enabled"):
|
||||||
extra_modoboa_config += """
|
extra_modoboa_config += """
|
||||||
location /radicale/ {
|
location /radicale/ {
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ class Opendkim(base.Installer):
|
|||||||
"""OpenDKIM installer."""
|
"""OpenDKIM installer."""
|
||||||
|
|
||||||
appname = "opendkim"
|
appname = "opendkim"
|
||||||
packages = {
|
packages = {"deb": ["opendkim"], "rpm": ["opendkim"]}
|
||||||
"deb": ["opendkim"],
|
|
||||||
"rpm": ["opendkim"]
|
|
||||||
}
|
|
||||||
config_files = ["opendkim.conf", "opendkim.hosts"]
|
config_files = ["opendkim.conf", "opendkim.hosts"]
|
||||||
|
|
||||||
def get_packages(self):
|
def get_packages(self):
|
||||||
@@ -36,30 +33,34 @@ class Opendkim(base.Installer):
|
|||||||
"""Make sure config directory exists."""
|
"""Make sure config directory exists."""
|
||||||
user = self.config.get("opendkim", "user")
|
user = self.config.get("opendkim", "user")
|
||||||
pw = pwd.getpwnam(user)
|
pw = pwd.getpwnam(user)
|
||||||
targets = [
|
targets = [[self.app_config["keys_storage_dir"], pw[2], pw[3]]]
|
||||||
[self.app_config["keys_storage_dir"], pw[2], pw[3]]
|
|
||||||
]
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
if not os.path.exists(target[0]):
|
if not os.path.exists(target[0]):
|
||||||
utils.mkdir(
|
utils.mkdir(
|
||||||
target[0],
|
target[0],
|
||||||
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
|
stat.S_IRWXU
|
||||||
stat.S_IROTH | stat.S_IXOTH,
|
| stat.S_IRGRP
|
||||||
target[1], target[2]
|
| stat.S_IXGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
| stat.S_IXOTH,
|
||||||
|
target[1],
|
||||||
|
target[2],
|
||||||
)
|
)
|
||||||
super().install_config_files()
|
super().install_config_files()
|
||||||
|
|
||||||
def get_template_context(self):
|
def get_template_context(self):
|
||||||
"""Additional variables."""
|
"""Additional variables."""
|
||||||
context = super(Opendkim, self).get_template_context()
|
context = super(Opendkim, self).get_template_context()
|
||||||
context.update({
|
context.update(
|
||||||
|
{
|
||||||
"db_driver": self.db_driver,
|
"db_driver": self.db_driver,
|
||||||
"db_name": self.config.get("modoboa", "dbname"),
|
"db_name": self.config.get("modoboa", "dbname"),
|
||||||
"db_user": self.app_config["dbuser"],
|
"db_user": self.app_config["dbuser"],
|
||||||
"db_password": self.app_config["dbpassword"],
|
"db_password": self.app_config["dbpassword"],
|
||||||
"port": self.app_config["port"],
|
"port": self.app_config["port"],
|
||||||
"user": self.app_config["user"]
|
"user": self.app_config["user"],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def setup_database(self):
|
def setup_database(self):
|
||||||
@@ -72,11 +73,14 @@ class Opendkim(base.Installer):
|
|||||||
dbuser = self.config.get("modoboa", "dbuser")
|
dbuser = self.config.get("modoboa", "dbuser")
|
||||||
dbpassword = self.config.get("modoboa", "dbpassword")
|
dbpassword = self.config.get("modoboa", "dbpassword")
|
||||||
self.backend.load_sql_file(
|
self.backend.load_sql_file(
|
||||||
dbname, dbuser, dbpassword,
|
dbname,
|
||||||
self.get_file_path("dkim_view_{}.sql".format(self.dbengine))
|
dbuser,
|
||||||
|
dbpassword,
|
||||||
|
self.get_file_path("dkim_view_{}.sql".format(self.dbengine)),
|
||||||
)
|
)
|
||||||
self.backend.grant_right_on_table(
|
self.backend.grant_right_on_table(
|
||||||
dbname, "dkim", self.app_config["dbuser"], "SELECT")
|
dbname, "dkim", self.app_config["dbuser"], "SELECT"
|
||||||
|
)
|
||||||
|
|
||||||
def post_run(self):
|
def post_run(self):
|
||||||
"""Additional tasks.
|
"""Additional tasks.
|
||||||
@@ -90,31 +94,33 @@ class Opendkim(base.Installer):
|
|||||||
else:
|
else:
|
||||||
params_file = "/etc/opendkim.conf"
|
params_file = "/etc/opendkim.conf"
|
||||||
pattern = r"s/^(SOCKET=.*)/#\1/"
|
pattern = r"s/^(SOCKET=.*)/#\1/"
|
||||||
utils.exec_cmd(
|
utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, params_file))
|
||||||
"perl -pi -e '{}' {}".format(pattern, params_file))
|
|
||||||
with open(params_file, "a") as f:
|
with open(params_file, "a") as f:
|
||||||
f.write('\n'.join([
|
f.write(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
"",
|
"",
|
||||||
'SOCKET="inet:12345@localhost"',
|
'SOCKET="inet:12345@localhost"',
|
||||||
]))
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Make sure opendkim is started after postgresql and mysql,
|
# Make sure opendkim is started after postgresql and mysql,
|
||||||
# respectively.
|
# respectively.
|
||||||
if (self.dbengine != "postgres" and package.backend.FORMAT == "deb"):
|
if self.dbengine != "postgres" and package.backend.FORMAT == "deb":
|
||||||
dbservice = "mysql.service"
|
dbservice = "mysql.service"
|
||||||
elif (self.dbengine != "postgres" and package.backend.FORMAT != "deb"):
|
elif self.dbengine != "postgres" and package.backend.FORMAT != "deb":
|
||||||
dbservice = "mysqld.service"
|
dbservice = "mysqld.service"
|
||||||
else:
|
else:
|
||||||
dbservice = "postgresql.service"
|
dbservice = "postgresql.service"
|
||||||
pattern = (
|
pattern = "s/^After=(.*)$/After=$1 {}/".format(dbservice)
|
||||||
"s/^After=(.*)$/After=$1 {}/".format(dbservice))
|
|
||||||
utils.exec_cmd(
|
utils.exec_cmd(
|
||||||
"perl -pi -e '{}' /lib/systemd/system/opendkim.service".format(pattern))
|
"perl -pi -e '{}' /lib/systemd/system/opendkim.service".format(pattern)
|
||||||
|
)
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
"""Restore keys."""
|
"""Restore keys."""
|
||||||
dkim_keys_backup = os.path.join(
|
dkim_keys_backup = os.path.join(self.archive_path, "custom/opendkim")
|
||||||
self.archive_path, "custom/dkim")
|
|
||||||
keys_storage_dir = self.app_config["keys_storage_dir"]
|
keys_storage_dir = self.app_config["keys_storage_dir"]
|
||||||
if os.path.isdir(dkim_keys_backup):
|
if os.path.isdir(dkim_keys_backup):
|
||||||
for file in os.listdir(dkim_keys_backup):
|
for file in os.listdir(dkim_keys_backup):
|
||||||
@@ -129,6 +135,7 @@ class Opendkim(base.Installer):
|
|||||||
def custom_backup(self, path):
|
def custom_backup(self, path):
|
||||||
"""Backup DKIM keys."""
|
"""Backup DKIM keys."""
|
||||||
if os.path.isdir(self.app_config["keys_storage_dir"]):
|
if os.path.isdir(self.app_config["keys_storage_dir"]):
|
||||||
shutil.copytree(self.app_config["keys_storage_dir"], os.path.join(path, "dkim"))
|
shutil.copytree(
|
||||||
utils.printcolor(
|
self.app_config["keys_storage_dir"], path, dirs_exist_ok=True
|
||||||
"DKIM keys saved!", utils.GREEN)
|
)
|
||||||
|
utils.printcolor("DKIM keys saved!", utils.GREEN)
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ class Postwhite(base.Installer):
|
|||||||
"crontab=/etc/cron.d/postwhite",
|
"crontab=/etc/cron.d/postwhite",
|
||||||
]
|
]
|
||||||
no_daemon = True
|
no_daemon = True
|
||||||
packages = {
|
packages = {"deb": ["bind9-host", "unzip"], "rpm": ["bind-utils", "unzip"]}
|
||||||
"deb": ["bind9-host"],
|
|
||||||
"rpm": ["bind-utils"]
|
|
||||||
}
|
|
||||||
|
|
||||||
def install_from_archive(self, repository, target_dir):
|
def install_from_archive(self, repository, target_dir):
|
||||||
"""Install from an archive."""
|
"""Install from an archive."""
|
||||||
@@ -36,8 +33,7 @@ class Postwhite(base.Installer):
|
|||||||
if os.path.exists(archive_dir):
|
if os.path.exists(archive_dir):
|
||||||
shutil.rmtree(archive_dir)
|
shutil.rmtree(archive_dir)
|
||||||
utils.exec_cmd("unzip master.zip", cwd=target_dir)
|
utils.exec_cmd("unzip master.zip", cwd=target_dir)
|
||||||
utils.exec_cmd(
|
utils.exec_cmd("mv {name}-master {name}".format(name=app_name), cwd=target_dir)
|
||||||
"mv {name}-master {name}".format(name=app_name), cwd=target_dir)
|
|
||||||
os.unlink(target)
|
os.unlink(target)
|
||||||
return archive_dir
|
return archive_dir
|
||||||
|
|
||||||
@@ -45,10 +41,8 @@ class Postwhite(base.Installer):
|
|||||||
"""Additionnal tasks."""
|
"""Additionnal tasks."""
|
||||||
install_dir = "/usr/local/bin"
|
install_dir = "/usr/local/bin"
|
||||||
self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir)
|
self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir)
|
||||||
self.postw_dir = self.install_from_archive(
|
self.postw_dir = self.install_from_archive(POSTWHITE_REPOSITORY, install_dir)
|
||||||
POSTWHITE_REPOSITORY, install_dir)
|
utils.copy_file(os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir)
|
||||||
utils.copy_file(
|
|
||||||
os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir)
|
|
||||||
self.postw_bin = os.path.join(self.postw_dir, "postwhite")
|
self.postw_bin = os.path.join(self.postw_dir, "postwhite")
|
||||||
utils.exec_cmd("{} /etc/postwhite.conf".format(self.postw_bin))
|
utils.exec_cmd("{} /etc/postwhite.conf".format(self.postw_bin))
|
||||||
|
|
||||||
@@ -57,13 +51,13 @@ class Postwhite(base.Installer):
|
|||||||
postswhite_custom = "/etc/postwhite.conf"
|
postswhite_custom = "/etc/postwhite.conf"
|
||||||
if os.path.isfile(postswhite_custom):
|
if os.path.isfile(postswhite_custom):
|
||||||
utils.copy_file(postswhite_custom, path)
|
utils.copy_file(postswhite_custom, path)
|
||||||
utils.printcolor(
|
utils.printcolor("Postwhite configuration saved!", utils.GREEN)
|
||||||
"Postwhite configuration saved!", utils.GREEN)
|
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
"""Restore config files."""
|
"""Restore config files."""
|
||||||
postwhite_backup_configuration = os.path.join(
|
postwhite_backup_configuration = os.path.join(
|
||||||
self.archive_path, "custom/postwhite.conf")
|
self.archive_path, "custom/postwhite/postwhite.conf"
|
||||||
|
)
|
||||||
if os.path.isfile(postwhite_backup_configuration):
|
if os.path.isfile(postwhite_backup_configuration):
|
||||||
utils.copy_file(postwhite_backup_configuration, self.config_dir)
|
utils.copy_file(postwhite_backup_configuration, self.config_dir)
|
||||||
utils.success("postwhite.conf restored from backup")
|
utils.success("postwhite.conf restored from backup")
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ class Radicale(base.Installer):
|
|||||||
appname = "radicale"
|
appname = "radicale"
|
||||||
config_files = ["config"]
|
config_files = ["config"]
|
||||||
no_daemon = True
|
no_daemon = True
|
||||||
packages = {
|
packages = {"deb": ["supervisor"], "rpm": ["supervisor"]}
|
||||||
"deb": ["supervisor"],
|
|
||||||
"rpm": ["supervisor"]
|
|
||||||
}
|
|
||||||
with_user = True
|
with_user = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -32,28 +29,25 @@ class Radicale(base.Installer):
|
|||||||
def _setup_venv(self):
|
def _setup_venv(self):
|
||||||
"""Prepare a dedicated virtualenv."""
|
"""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 = [
|
packages = ["Radicale", "pytz", "radicale-modoboa-auth-oauth2"]
|
||||||
"Radicale", "pytz", "radicale-modoboa-auth-oauth2"
|
|
||||||
]
|
|
||||||
python.install_packages(packages, self.venv_path, sudo_user=self.user)
|
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):
|
def get_template_context(self):
|
||||||
"""Additional variables."""
|
"""Additional variables."""
|
||||||
context = super().get_template_context()
|
context = super().get_template_context()
|
||||||
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
||||||
"Radicale", "radicale", self.config)
|
"Radicale", "radicale", self.config
|
||||||
|
)
|
||||||
hostname = self.config.get("general", "hostname")
|
hostname = self.config.get("general", "hostname")
|
||||||
oauth2_introspection_url = (
|
oauth2_introspection_url = (
|
||||||
f"https://{oauth2_client_id}:{oauth2_client_secret}"
|
f"https://{oauth2_client_id}:{oauth2_client_secret}"
|
||||||
f"@{hostname}/api/o/introspect/"
|
f"@{hostname}/api/o/introspect/"
|
||||||
)
|
)
|
||||||
context.update({
|
context.update(
|
||||||
|
{
|
||||||
"oauth2_introspection_url": oauth2_introspection_url,
|
"oauth2_introspection_url": oauth2_introspection_url,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_config_files(self):
|
def get_config_files(self):
|
||||||
@@ -71,16 +65,19 @@ class Radicale(base.Installer):
|
|||||||
if not os.path.exists(self.config_dir):
|
if not os.path.exists(self.config_dir):
|
||||||
utils.mkdir(
|
utils.mkdir(
|
||||||
self.config_dir,
|
self.config_dir,
|
||||||
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
|
stat.S_IRWXU
|
||||||
stat.S_IROTH | stat.S_IXOTH,
|
| stat.S_IRGRP
|
||||||
0, 0
|
| stat.S_IXGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
| stat.S_IXOTH,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
super().install_config_files()
|
super().install_config_files()
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
"""Restore collections."""
|
"""Restore collections."""
|
||||||
radicale_backup = os.path.join(
|
radicale_backup = os.path.join(self.archive_path, "custom/radicale")
|
||||||
self.archive_path, "custom/radicale")
|
|
||||||
if os.path.isdir(radicale_backup):
|
if os.path.isdir(radicale_backup):
|
||||||
restore_target = os.path.join(self.home_dir, "collections")
|
restore_target = os.path.join(self.home_dir, "collections")
|
||||||
if os.path.isdir(restore_target):
|
if os.path.isdir(restore_target):
|
||||||
@@ -91,18 +88,17 @@ class Radicale(base.Installer):
|
|||||||
def post_run(self):
|
def post_run(self):
|
||||||
"""Additional tasks."""
|
"""Additional tasks."""
|
||||||
self._setup_venv()
|
self._setup_venv()
|
||||||
daemon_name = (
|
daemon_name = "supervisor" if package.backend.FORMAT == "deb" else "supervisord"
|
||||||
"supervisor" if package.backend.FORMAT == "deb" else "supervisord"
|
|
||||||
)
|
|
||||||
system.enable_service(daemon_name)
|
system.enable_service(daemon_name)
|
||||||
utils.exec_cmd("service {} stop".format(daemon_name))
|
utils.exec_cmd("service {} stop".format(daemon_name))
|
||||||
utils.exec_cmd("service {} start".format(daemon_name))
|
utils.exec_cmd("service {} start".format(daemon_name))
|
||||||
|
|
||||||
def custom_backup(self, path):
|
def custom_backup(self, path):
|
||||||
"""Backup collections."""
|
"""Backup collections."""
|
||||||
radicale_backup = os.path.join(self.config.get(
|
radicale_backup = os.path.join(
|
||||||
"radicale", "home_dir", fallback="/srv/radicale"), "collections")
|
self.config.get("radicale", "home_dir", fallback="/srv/radicale"),
|
||||||
|
"collections",
|
||||||
|
)
|
||||||
if os.path.isdir(radicale_backup):
|
if os.path.isdir(radicale_backup):
|
||||||
shutil.copytree(radicale_backup, os.path.join(
|
shutil.copytree(radicale_backup, path, dirs_exist_ok=True)
|
||||||
path, "radicale"))
|
|
||||||
utils.printcolor("Radicale files saved", utils.GREEN)
|
utils.printcolor("Radicale files saved", utils.GREEN)
|
||||||
|
|||||||
@@ -16,11 +16,7 @@ class Rspamd(base.Installer):
|
|||||||
"""Rspamd installer."""
|
"""Rspamd installer."""
|
||||||
|
|
||||||
appname = "rspamd"
|
appname = "rspamd"
|
||||||
packages = {
|
packages = {"deb": ["rspamd", "redis"]}
|
||||||
"deb": [
|
|
||||||
"rspamd", "redis"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
config_files = [
|
config_files = [
|
||||||
"local.d/arc.conf",
|
"local.d/arc.conf",
|
||||||
"local.d/dkim_signing.conf",
|
"local.d/dkim_signing.conf",
|
||||||
@@ -39,10 +35,9 @@ class Rspamd(base.Installer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.generate_password_condition = (
|
self.generate_password_condition = not self.upgrade or utils.user_input(
|
||||||
not self.upgrade or utils.user_input(
|
"Do you want to (re)generate rspamd password ? (y/N)"
|
||||||
"Do you want to (re)generate rspamd password ? (y/N)").lower().startswith("y")
|
).lower().startswith("y")
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_dir(self):
|
def config_dir(self):
|
||||||
@@ -54,16 +49,20 @@ class Rspamd(base.Installer):
|
|||||||
if debian_based_dist:
|
if debian_based_dist:
|
||||||
utils.mkdir_safe(
|
utils.mkdir_safe(
|
||||||
"/etc/apt/keyrings",
|
"/etc/apt/keyrings",
|
||||||
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
|
stat.S_IRWXU
|
||||||
stat.S_IROTH | stat.S_IXOTH,
|
| stat.S_IRGRP
|
||||||
0, 0
|
| stat.S_IXGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
| stat.S_IXOTH,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
package.backend.add_custom_repository(
|
package.backend.add_custom_repository(
|
||||||
"rspamd",
|
"rspamd",
|
||||||
"http://rspamd.com/apt-stable/",
|
"http://rspamd.com/apt-stable/",
|
||||||
"https://rspamd.com/apt-stable/gpg.key",
|
"https://rspamd.com/apt-stable/gpg.key",
|
||||||
codename
|
codename,
|
||||||
)
|
)
|
||||||
package.backend.update()
|
package.backend.update()
|
||||||
|
|
||||||
@@ -73,16 +72,18 @@ class Rspamd(base.Installer):
|
|||||||
"""Make sure config directory exists."""
|
"""Make sure config directory exists."""
|
||||||
user = self.config.get(self.appname, "user")
|
user = self.config.get(self.appname, "user")
|
||||||
pw = pwd.getpwnam(user)
|
pw = pwd.getpwnam(user)
|
||||||
targets = [
|
targets = [[self.app_config["dkim_keys_storage_dir"], pw[2], pw[3]]]
|
||||||
[self.app_config["dkim_keys_storage_dir"], pw[2], pw[3]]
|
|
||||||
]
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
if not os.path.exists(target[0]):
|
if not os.path.exists(target[0]):
|
||||||
utils.mkdir(
|
utils.mkdir(
|
||||||
target[0],
|
target[0],
|
||||||
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
|
stat.S_IRWXU
|
||||||
stat.S_IROTH | stat.S_IXOTH,
|
| stat.S_IRGRP
|
||||||
target[1], target[2]
|
| stat.S_IXGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
| stat.S_IXOTH,
|
||||||
|
target[1],
|
||||||
|
target[2],
|
||||||
)
|
)
|
||||||
super().install_config_files()
|
super().install_config_files()
|
||||||
|
|
||||||
@@ -101,16 +102,23 @@ class Rspamd(base.Installer):
|
|||||||
|
|
||||||
def get_template_context(self):
|
def get_template_context(self):
|
||||||
_context = super().get_template_context()
|
_context = super().get_template_context()
|
||||||
_context["greylisting_disabled"] = "" if not self.app_config["greylisting"].lower() == "true" else "#"
|
_context["greylisting_disabled"] = (
|
||||||
_context["whitelist_auth_enabled"] = "" if self.app_config["whitelist_auth"].lower() == "true" else "#"
|
"" if not self.app_config["greylisting"].lower() == "true" else "#"
|
||||||
|
)
|
||||||
|
_context["whitelist_auth_enabled"] = (
|
||||||
|
"" if self.app_config["whitelist_auth"].lower() == "true" else "#"
|
||||||
|
)
|
||||||
if self.generate_password_condition:
|
if self.generate_password_condition:
|
||||||
code, controller_password = utils.exec_cmd(
|
code, controller_password = utils.exec_cmd(
|
||||||
r"rspamadm pw -p {}".format(self.app_config["password"]))
|
r"rspamadm pw -p {}".format(self.app_config["password"])
|
||||||
|
)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
utils.error("Error setting rspamd password. "
|
utils.error(
|
||||||
|
"Error setting rspamd password. "
|
||||||
"Please make sure it is not 'q1' or 'q2'."
|
"Please make sure it is not 'q1' or 'q2'."
|
||||||
"Storing the password in plain. See"
|
"Storing the password in plain. See"
|
||||||
"https://rspamd.com/doc/quickstart.html#setting-the-controller-password")
|
"https://rspamd.com/doc/quickstart.html#setting-the-controller-password"
|
||||||
|
)
|
||||||
_context["controller_password"] = self.app_config["password"]
|
_context["controller_password"] = self.app_config["password"]
|
||||||
else:
|
else:
|
||||||
controller_password = controller_password.decode().replace("\n", "")
|
controller_password = controller_password.decode().replace("\n", "")
|
||||||
@@ -120,35 +128,59 @@ class Rspamd(base.Installer):
|
|||||||
def post_run(self):
|
def post_run(self):
|
||||||
"""Additional tasks."""
|
"""Additional tasks."""
|
||||||
user = self.config.get(self.appname, "user")
|
user = self.config.get(self.appname, "user")
|
||||||
system.add_user_to_group(
|
system.add_user_to_group(self.config.get("modoboa", "user"), user)
|
||||||
self.config.get("modoboa", "user"),
|
|
||||||
user
|
|
||||||
)
|
|
||||||
if self.config.getboolean("clamav", "enabled"):
|
if self.config.getboolean("clamav", "enabled"):
|
||||||
install("clamav", self.config, self.upgrade, self.archive_path)
|
install("clamav", self.config, self.upgrade, self.archive_path)
|
||||||
|
|
||||||
|
def backup(self, path):
|
||||||
|
self._dump_database(path)
|
||||||
|
super().backup(path)
|
||||||
|
|
||||||
|
def _dump_database(self, backup_path: str):
|
||||||
|
"""Copy the rrd file containing the redis db for rspamd.
|
||||||
|
Uses the default path on debian 13,
|
||||||
|
it may be somewhere else on other distros..."""
|
||||||
|
rspamd_redis_db = "/var/lib/rspamd/rspamd.rrd"
|
||||||
|
if not os.path.isfile(rspamd_redis_db):
|
||||||
|
return
|
||||||
|
target_dir = os.path.join(backup_path, "databases")
|
||||||
|
target_file = os.path.join(target_dir, "rspamd.rrd")
|
||||||
|
system.stop_service("redis")
|
||||||
|
utils.copy_file(rspamd_redis_db, target_file)
|
||||||
|
system.restart_service("redis")
|
||||||
|
|
||||||
def custom_backup(self, path):
|
def custom_backup(self, path):
|
||||||
"""Backup custom configuration if any."""
|
"""Backup custom configuration if any."""
|
||||||
custom_config_dir = os.path.join(self.config_dir,
|
custom_config_dir = os.path.join(self.config_dir, "local.d/")
|
||||||
"/local.d/")
|
local_files = [
|
||||||
custom_backup_dir = os.path.join(path, "/rspamd/")
|
os.path.join(custom_config_dir, f)
|
||||||
local_files = [f for f in os.listdir(custom_config_dir)
|
for f in os.listdir(custom_config_dir)
|
||||||
if os.path.isfile(custom_config_dir, f)
|
if os.path.isfile(os.path.join(custom_config_dir, f))
|
||||||
]
|
]
|
||||||
|
backup_locald_path = os.path.join(path, "local.d")
|
||||||
|
if local_files:
|
||||||
|
utils.mkdir_safe(backup_locald_path)
|
||||||
for file in local_files:
|
for file in local_files:
|
||||||
utils.copy_file(file, custom_backup_dir)
|
basename = os.path.basename(file)
|
||||||
if len(local_files) != 0:
|
utils.copy_file(file, os.path.join(backup_locald_path, basename))
|
||||||
|
if local_files:
|
||||||
utils.success("Rspamd custom configuration saved!")
|
utils.success("Rspamd custom configuration saved!")
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
"""Restore custom config files."""
|
"""Restore custom config files."""
|
||||||
custom_config_dir = os.path.join(self.config_dir,
|
custom_config_dir = os.path.join(self.config_dir, "/local.d/")
|
||||||
"/local.d/")
|
|
||||||
custom_backup_dir = os.path.join(self.archive_path, "/rspamd/")
|
custom_backup_dir = os.path.join(self.archive_path, "/rspamd/")
|
||||||
backed_up_files = [
|
backed_up_files = [
|
||||||
f for f in os.listdir(custom_backup_dir)
|
f
|
||||||
|
for f in os.listdir(custom_backup_dir)
|
||||||
if os.path.isfile(custom_backup_dir, f)
|
if os.path.isfile(custom_backup_dir, f)
|
||||||
]
|
]
|
||||||
for f in backed_up_files:
|
for f in backed_up_files:
|
||||||
utils.copy_file(f, custom_config_dir)
|
utils.copy_file(f, custom_config_dir)
|
||||||
|
rspamd_redis_db = "/var/lib/rspamd"
|
||||||
|
rspamd_redis_db_backup = os.path.join(self.archive_path, "databases/rspamd.rrd")
|
||||||
|
if os.path.isdir(rspamd_redis_db) and os.path.isfile(rspamd_redis_db_backup):
|
||||||
|
system.stop_service("redis")
|
||||||
|
utils.copy_file(rspamd_redis_db_backup, rspamd_redis_db)
|
||||||
|
system.restart_service("redis")
|
||||||
utils.success("Custom Rspamd configuration restored.")
|
utils.success("Custom Rspamd configuration restored.")
|
||||||
|
|||||||
@@ -83,24 +83,9 @@ 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):
|
def post_run(self):
|
||||||
"""Additionnal tasks."""
|
"""Additionnal tasks."""
|
||||||
self._setup_modoboa_config()
|
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."""
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ def create_user(name, home=None):
|
|||||||
else:
|
else:
|
||||||
extra_message = "."
|
extra_message = "."
|
||||||
if home:
|
if home:
|
||||||
extra_message = (
|
extra_message = " but please make sure the {} directory exists.".format(
|
||||||
" but please make sure the {} directory exists.".format(
|
home
|
||||||
home))
|
)
|
||||||
utils.printcolor(
|
utils.printcolor(
|
||||||
"User {} already exists, skipping creation{}".format(
|
"User {} already exists, skipping creation{}".format(name, extra_message),
|
||||||
name, extra_message), utils.YELLOW)
|
utils.YELLOW,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
cmd = "useradd -m "
|
cmd = "useradd -m "
|
||||||
if home:
|
if home:
|
||||||
@@ -62,3 +63,8 @@ def enable_and_start_service(name):
|
|||||||
def restart_service(name):
|
def restart_service(name):
|
||||||
"""Restart a service."""
|
"""Restart a service."""
|
||||||
utils.exec_cmd("service {} restart".format(name))
|
utils.exec_cmd("service {} restart".format(name))
|
||||||
|
|
||||||
|
|
||||||
|
def stop_service(name):
|
||||||
|
"""Stop a service."""
|
||||||
|
utils.exec_cmd(f"service {name} stop")
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
|||||||
|
|
||||||
|
|
||||||
class FatalError(Exception):
|
class FatalError(Exception):
|
||||||
|
|
||||||
"""A simple exception."""
|
"""A simple exception."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
@@ -77,16 +76,14 @@ def dist_info():
|
|||||||
info = {}
|
info = {}
|
||||||
with open(path) as fp:
|
with open(path) as fp:
|
||||||
for line in fp.readlines():
|
for line in fp.readlines():
|
||||||
if line == '\n':
|
if line == "\n":
|
||||||
continue
|
continue
|
||||||
key, value = line.split("=")
|
key, value = line.split("=")
|
||||||
value = value.rstrip('"\n')
|
value = value.rstrip('"\n')
|
||||||
value = value.strip('"')
|
value = value.strip('"')
|
||||||
info[key] = value
|
info[key] = value
|
||||||
return info["NAME"], info["VERSION_ID"]
|
return info["NAME"], info["VERSION_ID"]
|
||||||
printcolor(
|
printcolor("Failed to retrieve information about your system, aborting.", RED)
|
||||||
"Failed to retrieve information about your system, aborting.",
|
|
||||||
RED)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,14 +96,27 @@ def is_dist_debian_based() -> (bool, str):
|
|||||||
"""Check if current OS is Debian based or not."""
|
"""Check if current OS is Debian based or not."""
|
||||||
status, codename = exec_cmd("lsb_release -c -s")
|
status, codename = exec_cmd("lsb_release -c -s")
|
||||||
codename = codename.decode().strip().lower()
|
codename = codename.decode().strip().lower()
|
||||||
return codename in [
|
return (
|
||||||
"bionic", "bookworm", "bullseye", "buster",
|
codename
|
||||||
"focal", "jammy", "jessie", "sid", "stretch",
|
in [
|
||||||
"trusty", "wheezy", "xenial"
|
"bionic",
|
||||||
], codename
|
"bookworm",
|
||||||
|
"bullseye",
|
||||||
|
"buster",
|
||||||
|
"focal",
|
||||||
|
"jammy",
|
||||||
|
"jessie",
|
||||||
|
"sid",
|
||||||
|
"stretch",
|
||||||
|
"trusty",
|
||||||
|
"wheezy",
|
||||||
|
"xenial",
|
||||||
|
],
|
||||||
|
codename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def mkdir(path, mode, uid, gid):
|
def mkdir(path, mode=0o777, uid=os.getuid(), gid=os.getgid()):
|
||||||
"""Create a directory."""
|
"""Create a directory."""
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
os.mkdir(path, mode)
|
os.mkdir(path, mode)
|
||||||
@@ -115,7 +125,7 @@ def mkdir(path, mode, uid, gid):
|
|||||||
os.chown(path, uid, gid)
|
os.chown(path, uid, gid)
|
||||||
|
|
||||||
|
|
||||||
def mkdir_safe(path, mode, uid, gid):
|
def mkdir_safe(path, mode=0o777, uid=os.getuid(), gid=os.getgid()):
|
||||||
"""Create a directory. Safe way (-p)"""
|
"""Create a directory. Safe way (-p)"""
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
os.makedirs(os.path.abspath(path), mode)
|
os.makedirs(os.path.abspath(path), mode)
|
||||||
@@ -125,8 +135,9 @@ def mkdir_safe(path, mode, uid, gid):
|
|||||||
def make_password(length=16):
|
def make_password(length=16):
|
||||||
"""Create a random password."""
|
"""Create a random password."""
|
||||||
return "".join(
|
return "".join(
|
||||||
random.SystemRandom().choice(
|
random.SystemRandom().choice(string.ascii_letters + string.digits)
|
||||||
string.ascii_letters + string.digits) for _ in range(length))
|
for _ in range(length)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
@@ -149,8 +160,7 @@ def backup_file(fname):
|
|||||||
"""Create a backup of a given file."""
|
"""Create a backup of a given file."""
|
||||||
for f in glob.glob("{}.old.*".format(fname)):
|
for f in glob.glob("{}.old.*".format(fname)):
|
||||||
os.unlink(f)
|
os.unlink(f)
|
||||||
bak_name = "{}.old.{}".format(
|
bak_name = "{}.old.{}".format(fname, datetime.datetime.now().isoformat())
|
||||||
fname, datetime.datetime.now().isoformat())
|
|
||||||
shutil.copy(fname, bak_name)
|
shutil.copy(fname, bak_name)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,17 +181,13 @@ def copy_from_template(template, dest, context):
|
|||||||
if os.path.isfile(dest):
|
if os.path.isfile(dest):
|
||||||
backup_file(dest)
|
backup_file(dest)
|
||||||
with open(dest, "w") as fp:
|
with open(dest, "w") as fp:
|
||||||
fp.write(
|
fp.write("# This file was automatically installed on {}\n".format(now))
|
||||||
"# This file was automatically installed on {}\n"
|
|
||||||
.format(now))
|
|
||||||
fp.write(ConfigFileTemplate(buf).substitute(context))
|
fp.write(ConfigFileTemplate(buf).substitute(context))
|
||||||
|
|
||||||
|
|
||||||
def check_config_file(dest,
|
def check_config_file(
|
||||||
interactive=False,
|
dest, interactive=False, upgrade=False, backup=False, restore=False
|
||||||
upgrade=False,
|
):
|
||||||
backup=False,
|
|
||||||
restore=False):
|
|
||||||
"""Create a new installer config file if needed."""
|
"""Create a new installer config file if needed."""
|
||||||
is_present = True
|
is_present = True
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
@@ -189,22 +195,25 @@ def check_config_file(dest,
|
|||||||
if upgrade:
|
if upgrade:
|
||||||
error(
|
error(
|
||||||
"You cannot upgrade an existing installation without a "
|
"You cannot upgrade an existing installation without a "
|
||||||
"configuration file.")
|
"configuration file."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif backup:
|
elif backup:
|
||||||
is_present = False
|
is_present = False
|
||||||
error(
|
error(
|
||||||
"Your configuration file hasn't been found. A new one will be generated. "
|
"Your configuration file hasn't been found. A new one will be generated. "
|
||||||
"Please edit it with correct password for the databases !")
|
"Please edit it with correct password for the databases !"
|
||||||
|
)
|
||||||
elif restore:
|
elif restore:
|
||||||
error(
|
error(
|
||||||
"You cannot restore an existing installation without a "
|
"You cannot restore an existing installation without a "
|
||||||
f"configuration file. (file : {dest} has not been found...")
|
f"configuration file. (file : {dest} has not been found..."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
printcolor(
|
printcolor(
|
||||||
"Configuration file {} not found, creating new one."
|
"Configuration file {} not found, creating new one.".format(dest), YELLOW
|
||||||
.format(dest), YELLOW)
|
)
|
||||||
gen_config(dest, interactive)
|
gen_config(dest, interactive)
|
||||||
return is_present, None
|
return is_present, None
|
||||||
|
|
||||||
@@ -217,6 +226,7 @@ def has_colours(stream):
|
|||||||
return False # auto color only on TTYs
|
return False # auto color only on TTYs
|
||||||
try:
|
try:
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
curses.setupterm()
|
curses.setupterm()
|
||||||
return curses.tigetnum("colors") > 2
|
return curses.tigetnum("colors") > 2
|
||||||
except:
|
except:
|
||||||
@@ -251,8 +261,9 @@ def convert_version_to_int(version):
|
|||||||
numbers = [int(number_string) for number_string in version.split(".")]
|
numbers = [int(number_string) for number_string in version.split(".")]
|
||||||
if len(numbers) > len(number_bits):
|
if len(numbers) > len(number_bits):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"Versions with more than {0} decimal places are not supported"
|
"Versions with more than {0} decimal places are not supported".format(
|
||||||
.format(len(number_bits) - 1)
|
len(number_bits) - 1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# add 0s for missing numbers
|
# add 0s for missing numbers
|
||||||
numbers.extend([0] * (len(number_bits) - len(numbers)))
|
numbers.extend([0] * (len(number_bits) - len(numbers)))
|
||||||
@@ -263,8 +274,9 @@ def convert_version_to_int(version):
|
|||||||
max_num = (bits + 1) - 1
|
max_num = (bits + 1) - 1
|
||||||
if num >= 1 << max_num:
|
if num >= 1 << max_num:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Number {0} cannot be stored with only {1} bits. Max is {2}"
|
"Number {0} cannot be stored with only {1} bits. Max is {2}".format(
|
||||||
.format(num, bits, max_num)
|
num, bits, max_num
|
||||||
|
)
|
||||||
)
|
)
|
||||||
number += num << total_bits
|
number += num << total_bits
|
||||||
total_bits += bits
|
total_bits += bits
|
||||||
@@ -315,7 +327,9 @@ def validate(value, config_entry):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_entry_value(entry: dict, interactive: bool, config: configparser.ConfigParser) -> string:
|
def get_entry_value(
|
||||||
|
entry: dict, interactive: bool, config: configparser.ConfigParser
|
||||||
|
) -> string:
|
||||||
default_entry = entry["default"]
|
default_entry = entry["default"]
|
||||||
if type(default_entry) is type(list()):
|
if type(default_entry) is type(list()):
|
||||||
default_value = str(check_if_condition(config, default_entry)).lower()
|
default_value = str(check_if_condition(config, default_entry)).lower()
|
||||||
@@ -325,7 +339,7 @@ def get_entry_value(entry: dict, interactive: bool, config: configparser.ConfigP
|
|||||||
default_value = default_entry
|
default_value = default_entry
|
||||||
user_value = None
|
user_value = None
|
||||||
if entry.get("customizable") and interactive:
|
if entry.get("customizable") and interactive:
|
||||||
while (user_value != '' and not validate(user_value, entry)):
|
while user_value != "" and not validate(user_value, entry):
|
||||||
question = entry.get("question")
|
question = entry.get("question")
|
||||||
if entry.get("values"):
|
if entry.get("values"):
|
||||||
question += " from the list"
|
question += " from the list"
|
||||||
@@ -366,14 +380,10 @@ def load_config_template(interactive):
|
|||||||
config.add_section(section["name"])
|
config.add_section(section["name"])
|
||||||
for config_entry in section["values"]:
|
for config_entry in section["values"]:
|
||||||
if config_entry.get("if") is not None:
|
if config_entry.get("if") is not None:
|
||||||
interactive_section = (interactive_section and
|
interactive_section = interactive_section and check_if_condition(
|
||||||
check_if_condition(
|
|
||||||
config, config_entry["if"]
|
config, config_entry["if"]
|
||||||
)
|
)
|
||||||
)
|
value = get_entry_value(config_entry, interactive_section, config)
|
||||||
value = get_entry_value(config_entry,
|
|
||||||
interactive_section,
|
|
||||||
config)
|
|
||||||
config.set(section["name"], config_entry["option"], value)
|
config.set(section["name"], config_entry["option"], value)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -393,10 +403,11 @@ def update_config(path, apply_update=True):
|
|||||||
dropped_sections = list(set(old_sections) - set(new_sections))
|
dropped_sections = list(set(old_sections) - set(new_sections))
|
||||||
added_sections = list(set(new_sections) - set(old_sections))
|
added_sections = list(set(new_sections) - set(old_sections))
|
||||||
if len(dropped_sections) > 0 and apply_update:
|
if len(dropped_sections) > 0 and apply_update:
|
||||||
printcolor("Following section(s) will not be ported "
|
printcolor(
|
||||||
"due to being deleted or renamed: " +
|
"Following section(s) will not be ported "
|
||||||
', '.join(dropped_sections),
|
"due to being deleted or renamed: " + ", ".join(dropped_sections),
|
||||||
RED)
|
RED,
|
||||||
|
)
|
||||||
|
|
||||||
if len(dropped_sections) + len(added_sections) > 0:
|
if len(dropped_sections) + len(added_sections) > 0:
|
||||||
update = True
|
update = True
|
||||||
@@ -409,11 +420,12 @@ def update_config(path, apply_update=True):
|
|||||||
dropped_options = list(set(old_options) - set(new_options))
|
dropped_options = list(set(old_options) - set(new_options))
|
||||||
added_options = list(set(new_options) - set(old_options))
|
added_options = list(set(new_options) - set(old_options))
|
||||||
if len(dropped_options) > 0 and apply_update:
|
if len(dropped_options) > 0 and apply_update:
|
||||||
printcolor(f"Following option(s) from section: {section}, "
|
printcolor(
|
||||||
|
f"Following option(s) from section: {section}, "
|
||||||
"will not be ported due to being "
|
"will not be ported due to being "
|
||||||
"deleted or renamed: " +
|
"deleted or renamed: " + ", ".join(dropped_options),
|
||||||
', '.join(dropped_options),
|
RED,
|
||||||
RED)
|
)
|
||||||
if len(dropped_options) + len(added_options) > 0:
|
if len(dropped_options) + len(added_options) > 0:
|
||||||
update = True
|
update = True
|
||||||
|
|
||||||
@@ -466,29 +478,27 @@ def validate_backup_path(path: str, silent_mode: bool):
|
|||||||
"""Check if provided backup path is valid or not."""
|
"""Check if provided backup path is valid or not."""
|
||||||
path_exists = os.path.exists(path)
|
path_exists = os.path.exists(path)
|
||||||
if path_exists and os.path.isfile(path):
|
if path_exists and os.path.isfile(path):
|
||||||
printcolor(
|
printcolor("Error, you provided a file instead of a directory!", RED)
|
||||||
"Error, you provided a file instead of a directory!", RED)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not path_exists:
|
if not path_exists:
|
||||||
if not silent_mode:
|
if not silent_mode:
|
||||||
create_dir = input(
|
create_dir = input(
|
||||||
f"\"{path}\" doesn't exist, would you like to create it? [y/N]\n"
|
f'"{path}" doesn\'t exist, would you like to create it? [y/N]\n'
|
||||||
).lower()
|
).lower()
|
||||||
|
|
||||||
if silent_mode or (not silent_mode and create_dir.startswith("y")):
|
if silent_mode or (not silent_mode and create_dir.startswith("y")):
|
||||||
pw = pwd.getpwnam("root")
|
pw = pwd.getpwnam("root")
|
||||||
mkdir_safe(path, stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
|
mkdir_safe(path, stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
|
||||||
else:
|
else:
|
||||||
printcolor(
|
printcolor("Error, backup directory not present.", RED)
|
||||||
"Error, backup directory not present.", RED
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(os.listdir(path)) != 0:
|
if len(os.listdir(path)) != 0:
|
||||||
if not silent_mode:
|
if not silent_mode:
|
||||||
delete_dir = input(
|
delete_dir = input(
|
||||||
"Warning: backup directory is not empty, it will be purged if you continue... [y/N]\n").lower()
|
"Warning: backup directory is not empty, it will be purged if you continue... [y/N]\n"
|
||||||
|
).lower()
|
||||||
|
|
||||||
if silent_mode or (not silent_mode and delete_dir.startswith("y")):
|
if silent_mode or (not silent_mode and delete_dir.startswith("y")):
|
||||||
try:
|
try:
|
||||||
@@ -496,22 +506,19 @@ def validate_backup_path(path: str, silent_mode: bool):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
shutil.rmtree(os.path.join(path, "custom"),
|
shutil.rmtree(os.path.join(path, "custom"), ignore_errors=False)
|
||||||
ignore_errors=False)
|
|
||||||
shutil.rmtree(os.path.join(path, "mails"), ignore_errors=False)
|
shutil.rmtree(os.path.join(path, "mails"), ignore_errors=False)
|
||||||
shutil.rmtree(os.path.join(path, "databases"),
|
shutil.rmtree(os.path.join(path, "databases"), ignore_errors=False)
|
||||||
ignore_errors=False)
|
|
||||||
else:
|
else:
|
||||||
printcolor(
|
printcolor("Error: backup directory not clean.", RED)
|
||||||
"Error: backup directory not clean.", RED
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
backup_path = path
|
backup_path = path
|
||||||
pw = pwd.getpwnam("root")
|
pw = pwd.getpwnam("root")
|
||||||
for dir in ["custom/", "databases/"]:
|
for dir in ["custom/", "databases/"]:
|
||||||
mkdir_safe(os.path.join(backup_path, dir),
|
mkdir_safe(
|
||||||
stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
|
os.path.join(backup_path, dir), stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]
|
||||||
|
)
|
||||||
return backup_path
|
return backup_path
|
||||||
|
|
||||||
|
|
||||||
@@ -539,7 +546,9 @@ def check_app_compatibility(section, config):
|
|||||||
if section in APP_INCOMPATIBILITY.keys():
|
if section in APP_INCOMPATIBILITY.keys():
|
||||||
for app in APP_INCOMPATIBILITY[section]:
|
for app in APP_INCOMPATIBILITY[section]:
|
||||||
if config.getboolean(app, "enabled"):
|
if config.getboolean(app, "enabled"):
|
||||||
error(f"{section} cannot be installed if {app} is enabled. "
|
error(
|
||||||
"Please disable one of them.")
|
f"{section} cannot be installed if {app} is enabled. "
|
||||||
|
"Please disable one of them."
|
||||||
|
)
|
||||||
incompatible_app.append(app)
|
incompatible_app.append(app)
|
||||||
return len(incompatible_app) == 0
|
return len(incompatible_app) == 0
|
||||||
|
|||||||
197
run.py
197
run.py
@@ -22,16 +22,15 @@ from modoboa_installer import disclaimers
|
|||||||
PRIMARY_APPS = [
|
PRIMARY_APPS = [
|
||||||
"fail2ban",
|
"fail2ban",
|
||||||
"modoboa",
|
"modoboa",
|
||||||
"automx",
|
|
||||||
"radicale",
|
"radicale",
|
||||||
"uwsgi",
|
"uwsgi",
|
||||||
"nginx",
|
"nginx",
|
||||||
"postfix",
|
"postfix",
|
||||||
"dovecot"
|
"dovecot",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def backup_system(config, args):
|
def backup_system(config, args, antispam_apps):
|
||||||
"""Launch backup procedure."""
|
"""Launch backup procedure."""
|
||||||
disclaimers.backup_disclaimer()
|
disclaimers.backup_disclaimer()
|
||||||
backup_path = None
|
backup_path = None
|
||||||
@@ -53,8 +52,7 @@ def backup_system(config, args):
|
|||||||
user_value = None
|
user_value = None
|
||||||
while not user_value or not backup_path:
|
while not user_value or not backup_path:
|
||||||
utils.printcolor(
|
utils.printcolor(
|
||||||
"Enter backup path (it must be an empty directory)",
|
"Enter backup path (it must be an empty directory)", utils.MAGENTA
|
||||||
utils.MAGENTA
|
|
||||||
)
|
)
|
||||||
utils.printcolor("CTRL+C to cancel", utils.MAGENTA)
|
utils.printcolor("CTRL+C to cancel", utils.MAGENTA)
|
||||||
user_value = utils.user_input("-> ")
|
user_value = utils.user_input("-> ")
|
||||||
@@ -65,7 +63,9 @@ def backup_system(config, args):
|
|||||||
# Backup configuration file
|
# Backup configuration file
|
||||||
utils.copy_file(args.configfile, backup_path)
|
utils.copy_file(args.configfile, backup_path)
|
||||||
# Backup applications
|
# Backup applications
|
||||||
for app in PRIMARY_APPS:
|
for app in PRIMARY_APPS + antispam_apps:
|
||||||
|
if config.has_option(app, "enabled") and not config.getboolean(app, "enabled"):
|
||||||
|
continue
|
||||||
if app == "dovecot" and args.no_mail:
|
if app == "dovecot" and args.no_mail:
|
||||||
utils.printcolor("Skipping mail backup", utils.BLUE)
|
utils.printcolor("Skipping mail backup", utils.BLUE)
|
||||||
continue
|
continue
|
||||||
@@ -73,65 +73,97 @@ def backup_system(config, args):
|
|||||||
|
|
||||||
|
|
||||||
def config_file_update_complete(backup_location):
|
def config_file_update_complete(backup_location):
|
||||||
utils.printcolor("Update complete. It seems successful.",
|
utils.printcolor("Update complete. It seems successful.", utils.BLUE)
|
||||||
utils.BLUE)
|
|
||||||
if backup_location is not None:
|
if backup_location is not None:
|
||||||
utils.printcolor("You will find your old config file "
|
utils.printcolor(
|
||||||
f"here: {backup_location}",
|
"You will find your old config file " f"here: {backup_location}", utils.BLUE
|
||||||
utils.BLUE)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parser_setup(input_args):
|
def parser_setup(input_args):
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
versions = (
|
versions = ["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys())
|
||||||
["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys())
|
|
||||||
)
|
|
||||||
parser.add_argument("--debug", action="store_true", default=False,
|
|
||||||
help="Enable debug output")
|
|
||||||
parser.add_argument("--force", action="store_true", default=False,
|
|
||||||
help="Force installation")
|
|
||||||
parser.add_argument("--configfile", default="installer.cfg",
|
|
||||||
help="Configuration file to use")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--version", default="latest", choices=versions,
|
"--debug", action="store_true", default=False, help="Enable debug output"
|
||||||
help="Modoboa version to install")
|
|
||||||
parser.add_argument(
|
|
||||||
"--stop-after-configfile-check", action="store_true", default=False,
|
|
||||||
help="Check configuration, generate it if needed and exit")
|
|
||||||
parser.add_argument(
|
|
||||||
"--interactive", action="store_true", default=False,
|
|
||||||
help="Generate configuration file with user interaction")
|
|
||||||
parser.add_argument(
|
|
||||||
"--upgrade", action="store_true", default=False,
|
|
||||||
help="Run the installer in upgrade mode")
|
|
||||||
parser.add_argument(
|
|
||||||
"--beta", action="store_true", default=False,
|
|
||||||
help="Install latest beta release of Modoboa instead of the stable one")
|
|
||||||
parser.add_argument(
|
|
||||||
"--backup-path", type=str, metavar="path",
|
|
||||||
help="To use with --silent-backup, you must provide a valid path")
|
|
||||||
parser.add_argument(
|
|
||||||
"--backup", action="store_true", default=False,
|
|
||||||
help="Backing up interactively previously installed instance"
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--silent-backup", action="store_true", default=False,
|
"--force", action="store_true", default=False, help="Force installation"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--configfile", default="installer.cfg", help="Configuration file to use"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--version",
|
||||||
|
default="latest",
|
||||||
|
choices=versions,
|
||||||
|
help="Modoboa version to install",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stop-after-configfile-check",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Check configuration, generate it if needed and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--interactive",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Generate configuration file with user interaction",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--upgrade",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Run the installer in upgrade mode",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--beta",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Install latest beta release of Modoboa instead of the stable one",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--backup-path",
|
||||||
|
type=str,
|
||||||
|
metavar="path",
|
||||||
|
help="To use with --silent-backup, you must provide a valid path",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--backup",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Backing up interactively previously installed instance",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--silent-backup",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
help="For script usage, do not require user interaction "
|
help="For script usage, do not require user interaction "
|
||||||
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M "
|
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M "
|
||||||
"if --backup-path is not provided")
|
"if --backup-path is not provided",
|
||||||
parser.add_argument(
|
|
||||||
"--no-mail", action="store_true", default=False,
|
|
||||||
help="Disable mail backup (save space)")
|
|
||||||
parser.add_argument(
|
|
||||||
"--restore", type=str, metavar="path",
|
|
||||||
help="Restore a previously backup up modoboa instance on a NEW machine. "
|
|
||||||
"You MUST provide backup directory"
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--skip-checks", action="store_true", default=False,
|
"--no-mail",
|
||||||
help="Skip the checks the installer performs initially")
|
action="store_true",
|
||||||
parser.add_argument("domain", type=str,
|
default=False,
|
||||||
help="The main domain of your future mail server")
|
help="Disable mail backup (save space)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--restore",
|
||||||
|
type=str,
|
||||||
|
metavar="path",
|
||||||
|
help="Restore a previously backup up modoboa instance on a NEW machine. "
|
||||||
|
"You MUST provide backup directory",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--skip-checks",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Skip the checks the installer performs initially",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"domain", type=str, help="The main domain of your future mail server"
|
||||||
|
)
|
||||||
return parser.parse_args(input_args)
|
return parser.parse_args(input_args)
|
||||||
|
|
||||||
|
|
||||||
@@ -148,9 +180,7 @@ def main(input_args):
|
|||||||
is_restoring = True
|
is_restoring = True
|
||||||
args.configfile = os.path.join(args.restore, args.configfile)
|
args.configfile = os.path.join(args.restore, args.configfile)
|
||||||
if not os.path.exists(args.configfile):
|
if not os.path.exists(args.configfile):
|
||||||
utils.error(
|
utils.error("Installer configuration file not found in backup!")
|
||||||
"Installer configuration file not found in backup!"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
utils.success("Welcome to Modoboa installer!\n")
|
utils.success("Welcome to Modoboa installer!\n")
|
||||||
@@ -162,26 +192,34 @@ def main(input_args):
|
|||||||
utils.success("Checks complete\n")
|
utils.success("Checks complete\n")
|
||||||
|
|
||||||
is_config_file_available, outdate_config = utils.check_config_file(
|
is_config_file_available, outdate_config = utils.check_config_file(
|
||||||
args.configfile, args.interactive, args.upgrade, args.backup, is_restoring)
|
args.configfile, args.interactive, args.upgrade, args.backup, is_restoring
|
||||||
|
)
|
||||||
|
|
||||||
if not is_config_file_available and (
|
if not is_config_file_available and (
|
||||||
args.upgrade or args.backup or args.silent_backup):
|
args.upgrade or args.backup or args.silent_backup
|
||||||
|
):
|
||||||
utils.error("No config file found.")
|
utils.error("No config file found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if config is outdated and ask user if it needs to be updated
|
# Check if config is outdated and ask user if it needs to be updated
|
||||||
if is_config_file_available and outdate_config:
|
if is_config_file_available and outdate_config:
|
||||||
answer = utils.user_input("It seems that your config file is outdated. "
|
answer = utils.user_input(
|
||||||
"Would you like to update it? (Y/n) ")
|
"It seems that your config file is outdated. "
|
||||||
|
"Would you like to update it? (Y/n) "
|
||||||
|
)
|
||||||
if not answer or answer.lower().startswith("y"):
|
if not answer or answer.lower().startswith("y"):
|
||||||
config_file_update_complete(utils.update_config(args.configfile))
|
config_file_update_complete(utils.update_config(args.configfile))
|
||||||
if not args.stop_after_configfile_check:
|
if not args.stop_after_configfile_check:
|
||||||
answer = utils.user_input("Would you like to stop to review the updated config? (Y/n)")
|
answer = utils.user_input(
|
||||||
|
"Would you like to stop to review the updated config? (Y/n)"
|
||||||
|
)
|
||||||
if not answer or answer.lower().startswith("y"):
|
if not answer or answer.lower().startswith("y"):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
utils.error("You might encounter unexpected errors ! "
|
utils.error(
|
||||||
"Make sure to update your config before opening an issue!")
|
"You might encounter unexpected errors ! "
|
||||||
|
"Make sure to update your config before opening an issue!"
|
||||||
|
)
|
||||||
|
|
||||||
if args.stop_after_configfile_check:
|
if args.stop_after_configfile_check:
|
||||||
return
|
return
|
||||||
@@ -202,7 +240,7 @@ def main(input_args):
|
|||||||
antispam_apps = ["rspamd"]
|
antispam_apps = ["rspamd"]
|
||||||
|
|
||||||
if args.backup or args.silent_backup:
|
if args.backup or args.silent_backup:
|
||||||
backup_system(config, args)
|
backup_system(config, args, antispam_apps)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Display disclaimer python 3 linux distribution
|
# Display disclaimer python 3 linux distribution
|
||||||
@@ -217,11 +255,20 @@ def main(input_args):
|
|||||||
# Show concerned components
|
# Show concerned components
|
||||||
components = []
|
components = []
|
||||||
for section in config.sections():
|
for section in config.sections():
|
||||||
if section in ["general", "antispam", "database", "mysql", "postgres",
|
if section in [
|
||||||
"certificate", "letsencrypt", "backup"]:
|
"general",
|
||||||
|
"antispam",
|
||||||
|
"database",
|
||||||
|
"mysql",
|
||||||
|
"postgres",
|
||||||
|
"certificate",
|
||||||
|
"letsencrypt",
|
||||||
|
"backup",
|
||||||
|
]:
|
||||||
continue
|
continue
|
||||||
if (config.has_option(section, "enabled") and
|
if config.has_option(section, "enabled") and not config.getboolean(
|
||||||
not config.getboolean(section, "enabled")):
|
section, "enabled"
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
incompatible_app_detected = not utils.check_app_compatibility(section, config)
|
incompatible_app_detected = not utils.check_app_compatibility(section, config)
|
||||||
if incompatible_app_detected:
|
if incompatible_app_detected:
|
||||||
@@ -234,8 +281,9 @@ def main(input_args):
|
|||||||
return
|
return
|
||||||
config.set("general", "force", str(args.force))
|
config.set("general", "force", str(args.force))
|
||||||
utils.printcolor(
|
utils.printcolor(
|
||||||
"The process can be long, feel free to take a coffee "
|
"The process can be long, feel free to take a coffee " "and come back later ;)",
|
||||||
"and come back later ;)", utils.BLUE)
|
utils.BLUE,
|
||||||
|
)
|
||||||
utils.success("Starting...")
|
utils.success("Starting...")
|
||||||
package.backend.prepare_system()
|
package.backend.prepare_system()
|
||||||
package.backend.install_many(["sudo", "wget"])
|
package.backend.install_many(["sudo", "wget"])
|
||||||
@@ -269,13 +317,8 @@ def main(input_args):
|
|||||||
"You like the project and want it to be sustainable?\n"
|
"You like the project and want it to be sustainable?\n"
|
||||||
"Then don't wait anymore and go sponsor it here:\n"
|
"Then don't wait anymore and go sponsor it here:\n"
|
||||||
)
|
)
|
||||||
utils.printcolor(
|
utils.printcolor("https://github.com/sponsors/modoboa\n", utils.YELLOW)
|
||||||
"https://github.com/sponsors/modoboa\n",
|
utils.success("Thank you for your help :-)\n")
|
||||||
utils.YELLOW
|
|
||||||
)
|
|
||||||
utils.success(
|
|
||||||
"Thank you for your help :-)\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
2
tests.py
2
tests.py
@@ -126,7 +126,7 @@ class ConfigFileTestCase(unittest.TestCase):
|
|||||||
"example.test"])
|
"example.test"])
|
||||||
self.assertTrue(os.path.exists(self.cfgfile))
|
self.assertTrue(os.path.exists(self.cfgfile))
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"fail2ban modoboa automx amavis clamav dovecot nginx "
|
"fail2ban modoboa amavis clamav dovecot nginx "
|
||||||
"postfix postwhite spamassassin uwsgi radicale opendkim",
|
"postfix postwhite spamassassin uwsgi radicale opendkim",
|
||||||
out.getvalue()
|
out.getvalue()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
506b59cda748708242cadaa831aa84aa320e1cfc
|
1d701353d900f4b6e2f7ffba6f6b7a46d304f58b
|
||||||
Reference in New Issue
Block a user