From 3e5b9ab3104cf5f72d95d9de52c7ddceef431c66 Mon Sep 17 00:00:00 2001 From: Spitap Date: Sat, 1 Nov 2025 17:18:22 +0100 Subject: [PATCH] Improved backups --- modoboa_installer/scripts/__init__.py | 16 +- modoboa_installer/scripts/amavis.py | 56 +++--- modoboa_installer/scripts/backup.py | 226 ------------------------- modoboa_installer/scripts/base.py | 39 ++--- modoboa_installer/scripts/opendkim.py | 79 +++++---- modoboa_installer/scripts/postwhite.py | 20 +-- modoboa_installer/scripts/radicale.py | 46 ++--- modoboa_installer/scripts/rspamd.py | 86 +++++----- run.py | 178 +++++++++++-------- 9 files changed, 287 insertions(+), 459 deletions(-) delete mode 100644 modoboa_installer/scripts/backup.py diff --git a/modoboa_installer/scripts/__init__.py b/modoboa_installer/scripts/__init__.py index d006f4c..0be741e 100644 --- a/modoboa_installer/scripts/__init__.py +++ b/modoboa_installer/scripts/__init__.py @@ -9,8 +9,7 @@ from .. import utils def load_app_script(appname): """Load module corresponding to the given appname.""" try: - script = importlib.import_module( - "modoboa_installer.scripts.{}".format(appname)) + script = importlib.import_module("modoboa_installer.scripts.{}".format(appname)) except ImportError: print("Unknown application {}".format(appname)) sys.exit(1) @@ -19,8 +18,9 @@ def load_app_script(appname): def install(appname: str, config, upgrade: bool, archive_path: str): """Install an application.""" - if (config.has_option(appname, "enabled") and - not config.getboolean(appname, "enabled")): + if config.has_option(appname, "enabled") and not config.getboolean( + appname, "enabled" + ): return 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): """Backup an application.""" - if (config.has_option(appname, "enabled") and - not config.getboolean(appname, "enabled")): + if config.has_option(appname, "enabled") and not config.getboolean( + appname, "enabled" + ): return utils.printcolor("Backing up {}".format(appname), utils.MAGENTA) @@ -49,6 +50,5 @@ def backup(appname, config, path): def restore_prep(restore): """Restore instance""" - script = importlib.import_module( - "modoboa_installer.scripts.restore") + script = importlib.import_module("modoboa_installer.scripts.restore") getattr(script, "Restore")(restore) diff --git a/modoboa_installer/scripts/amavis.py b/modoboa_installer/scripts/amavis.py index 2e067c4..09f6509 100644 --- a/modoboa_installer/scripts/amavis.py +++ b/modoboa_installer/scripts/amavis.py @@ -10,18 +10,29 @@ from . import backup, install class Amavis(base.Installer): - """Amavis installer.""" appname = "amavis" packages = { "deb": [ - "libdbi-perl", "amavisd-new", "arc", "arj", "cabextract", - "liblz4-tool", "lrzip", "lzop", "p7zip-full", "rpm2cpio", + "libdbi-perl", + "amavisd-new", + "arc", + "arj", + "cabextract", + "liblz4-tool", + "lrzip", + "lzop", + "p7zip-full", + "rpm2cpio", "unrar-free", ], "rpm": [ - "amavisd-new", "arj", "lz4", "lzop", "p7zip", + "amavisd-new", + "arj", + "lz4", + "lzop", + "p7zip", ], } with_db = True @@ -43,8 +54,10 @@ class Amavis(base.Installer): """Return appropriate config files.""" if package.backend.FORMAT == "deb": return [ - "conf.d/05-node_id", "conf.d/15-content_filter_mode", - "conf.d/50-user"] + "conf.d/05-node_id", + "conf.d/15-content_filter_mode", + "conf.d/50-user", + ] return ["amavisd.conf"] def get_packages(self): @@ -62,7 +75,7 @@ class Amavis(base.Installer): if major_version >= 13: packages = [p if p != "liblz4-tool" else "lz4" for p in packages] return packages - + if self.db_driver == "pgsql": db_driver = "Pg" elif self.db_driver == "mysql": @@ -71,9 +84,9 @@ class Amavis(base.Installer): raise NotImplementedError("DB driver not supported") packages += ["perl-DBD-{}".format(db_driver)] name, version = utils.dist_info() - if version.startswith('7'): + if version.startswith("7"): packages += ["cabextract", "lrzip", "unar", "unzoo"] - elif version.startswith('8'): + elif version.startswith("8"): packages += ["perl-IO-stringy"] return packages @@ -85,12 +98,10 @@ class Amavis(base.Installer): version = package.backend.get_installed_version("amavis") if version is None: raise utils.FatalError("Amavis is not installed") - path = self.get_file_path( - "amavis_{}_{}.sql".format(self.dbengine, version)) + path = self.get_file_path("amavis_{}_{}.sql".format(self.dbengine, version)) if not os.path.exists(path): version = ".".join(version.split(".")[:-1]) + ".X" - path = self.get_file_path( - "amavis_{}_{}.sql".format(self.dbengine, version)) + path = self.get_file_path("amavis_{}_{}.sql".format(self.dbengine, version)) if not os.path.exists(path): raise utils.FatalError("Failed to find amavis database schema") return path @@ -107,20 +118,21 @@ class Amavis(base.Installer): def custom_backup(self, path): """Backup custom configuration if any.""" - if package.backend.FORMAT == "deb": - amavis_custom = f"{self.config_dir}/conf.d/99-custom" - if os.path.isfile(amavis_custom): - utils.copy_file(amavis_custom, path) - utils.success("Amavis custom configuration saved!") - backup("spamassassin", self.config, os.path.dirname(path)) + amavis_custom = f"{self.config_dir}/conf.d/99-custom" + if os.path.isfile(amavis_custom): + utils.copy_file(amavis_custom, path) + utils.success("Amavis custom configuration saved!") + backup("spamassassin", self.config, self.base_backup_path) def restore(self): """Restore custom config files.""" if package.backend.FORMAT != "deb": return 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): - utils.copy_file(amavis_custom_configuration, os.path.join( - self.config_dir, "conf.d")) + utils.copy_file( + amavis_custom_configuration, os.path.join(self.config_dir, "conf.d") + ) utils.success("Custom amavis configuration restored.") diff --git a/modoboa_installer/scripts/backup.py b/modoboa_installer/scripts/backup.py deleted file mode 100644 index 9ef9f5c..0000000 --- a/modoboa_installer/scripts/backup.py +++ /dev/null @@ -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() diff --git a/modoboa_installer/scripts/base.py b/modoboa_installer/scripts/base.py index 3b30e4a..b6faf0e 100644 --- a/modoboa_installer/scripts/base.py +++ b/modoboa_installer/scripts/base.py @@ -31,12 +31,12 @@ class Installer: self.app_config = dict(self.config.items(self.appname)) self.dbengine = self.config.get("database", "engine") # Used to install system packages - self.db_driver = ( - "pgsql" if self.dbengine == "postgres" else self.dbengine) + self.db_driver = "pgsql" if self.dbengine == "postgres" else self.dbengine self.backend = database.get_backend(self.config) self.dbhost = self.config.get("database", "host") self.dbport = self.config.get( - "database", "port", fallback=self.backend.default_port) + "database", "port", fallback=self.backend.default_port + ) self._config_dir = None if not self.with_db: return @@ -50,19 +50,19 @@ class Installer: modoboa_version = python.get_package_version( "modoboa", self.config.get("modoboa", "venv_path"), - sudo_user=self.config.get("modoboa", "user") - ) + sudo_user=self.config.get("modoboa", "user"), + ) condition = ( - (int(modoboa_version[0]) == 2 and int(modoboa_version[1]) >= 2) or - int(modoboa_version[0]) > 2 - ) + int(modoboa_version[0]) == 2 and int(modoboa_version[1]) >= 2 + ) or int(modoboa_version[0]) > 2 return condition @property def config_dir(self): """Return main configuration directory.""" 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") return self._config_dir @@ -73,11 +73,11 @@ class Installer: def get_sql_schema_from_backup(self): """Retrieve a dump path from a previous backup.""" utils.printcolor( - f"Trying to restore {self.appname} database from backup.", - utils.MAGENTA + f"Trying to restore {self.appname} database from backup.", utils.MAGENTA ) 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): utils.success(f"SQL dump found in backup for {self.appname}!") return database_backup_path @@ -86,8 +86,7 @@ class Installer: def get_file_path(self, fname): """Return the absolute path of this file.""" return os.path.abspath( - os.path.join( - os.path.dirname(__file__), "files", self.appname, fname) + os.path.join(os.path.dirname(__file__), "files", self.appname, fname) ) def setup_database(self): @@ -102,8 +101,7 @@ class Installer: if not schema: schema = self.get_sql_schema_path() if schema: - self.backend.load_sql_file( - self.dbname, self.dbuser, self.dbpasswd, schema) + self.backend.load_sql_file(self.dbname, self.dbuser, self.dbpasswd, schema) def setup_user(self): """Setup a system user.""" @@ -119,8 +117,7 @@ class Installer: def get_template_context(self): """Return context used for template rendering.""" context = { - "dbengine": ( - "Pg" if self.dbengine == "postgres" else self.dbengine), + "dbengine": ("Pg" if self.dbengine == "postgres" else self.dbengine), "dbhost": self.dbhost, "dbport": self.dbport, } @@ -172,9 +169,10 @@ class Installer: utils.copy_from_template(src, dst, context) def backup(self, path): + self.base_backup_path = path if self.with_db: self._dump_database(path) - custom_backup_path = os.path.join(path, "custom") + custom_backup_path = os.path.join(path, "custom", self.appname) self.custom_backup(custom_backup_path) def custom_backup(self, path): @@ -213,8 +211,7 @@ class Installer: """Create a new database dump for this app.""" target_dir = os.path.join(backup_path, "databases") target_file = os.path.join(target_dir, f"{self.appname}.sql") - self.backend.dump_database( - self.dbname, self.dbuser, self.dbpasswd, target_file) + self.backend.dump_database(self.dbname, self.dbuser, self.dbpasswd, target_file) def pre_run(self): """Tasks to execute before the installer starts.""" diff --git a/modoboa_installer/scripts/opendkim.py b/modoboa_installer/scripts/opendkim.py index fea60a7..3b9b239 100644 --- a/modoboa_installer/scripts/opendkim.py +++ b/modoboa_installer/scripts/opendkim.py @@ -16,10 +16,7 @@ class Opendkim(base.Installer): """OpenDKIM installer.""" appname = "opendkim" - packages = { - "deb": ["opendkim"], - "rpm": ["opendkim"] - } + packages = {"deb": ["opendkim"], "rpm": ["opendkim"]} config_files = ["opendkim.conf", "opendkim.hosts"] def get_packages(self): @@ -36,30 +33,34 @@ class Opendkim(base.Installer): """Make sure config directory exists.""" user = self.config.get("opendkim", "user") pw = pwd.getpwnam(user) - targets = [ - [self.app_config["keys_storage_dir"], pw[2], pw[3]] - ] + targets = [[self.app_config["keys_storage_dir"], pw[2], pw[3]]] for target in targets: if not os.path.exists(target[0]): utils.mkdir( target[0], - stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | - stat.S_IROTH | stat.S_IXOTH, - target[1], target[2] + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + target[1], + target[2], ) super().install_config_files() def get_template_context(self): """Additional variables.""" context = super(Opendkim, self).get_template_context() - context.update({ - "db_driver": self.db_driver, - "db_name": self.config.get("modoboa", "dbname"), - "db_user": self.app_config["dbuser"], - "db_password": self.app_config["dbpassword"], - "port": self.app_config["port"], - "user": self.app_config["user"] - }) + context.update( + { + "db_driver": self.db_driver, + "db_name": self.config.get("modoboa", "dbname"), + "db_user": self.app_config["dbuser"], + "db_password": self.app_config["dbpassword"], + "port": self.app_config["port"], + "user": self.app_config["user"], + } + ) return context def setup_database(self): @@ -72,11 +73,14 @@ class Opendkim(base.Installer): dbuser = self.config.get("modoboa", "dbuser") dbpassword = self.config.get("modoboa", "dbpassword") self.backend.load_sql_file( - dbname, dbuser, dbpassword, - self.get_file_path("dkim_view_{}.sql".format(self.dbengine)) + dbname, + dbuser, + dbpassword, + self.get_file_path("dkim_view_{}.sql".format(self.dbengine)), ) self.backend.grant_right_on_table( - dbname, "dkim", self.app_config["dbuser"], "SELECT") + dbname, "dkim", self.app_config["dbuser"], "SELECT" + ) def post_run(self): """Additional tasks. @@ -90,31 +94,33 @@ class Opendkim(base.Installer): else: params_file = "/etc/opendkim.conf" pattern = r"s/^(SOCKET=.*)/#\1/" - utils.exec_cmd( - "perl -pi -e '{}' {}".format(pattern, params_file)) + utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, params_file)) with open(params_file, "a") as f: - f.write('\n'.join([ - "", - 'SOCKET="inet:12345@localhost"', - ])) + f.write( + "\n".join( + [ + "", + 'SOCKET="inet:12345@localhost"', + ] + ) + ) # Make sure opendkim is started after postgresql and mysql, # respectively. - if (self.dbengine != "postgres" and package.backend.FORMAT == "deb"): + if self.dbengine != "postgres" and package.backend.FORMAT == "deb": dbservice = "mysql.service" - elif (self.dbengine != "postgres" and package.backend.FORMAT != "deb"): + elif self.dbengine != "postgres" and package.backend.FORMAT != "deb": dbservice = "mysqld.service" else: dbservice = "postgresql.service" - pattern = ( - "s/^After=(.*)$/After=$1 {}/".format(dbservice)) + pattern = "s/^After=(.*)$/After=$1 {}/".format(dbservice) 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): """Restore keys.""" - dkim_keys_backup = os.path.join( - self.archive_path, "custom/dkim") + dkim_keys_backup = os.path.join(self.archive_path, "custom/opendkim") keys_storage_dir = self.app_config["keys_storage_dir"] if os.path.isdir(dkim_keys_backup): for file in os.listdir(dkim_keys_backup): @@ -129,6 +135,5 @@ class Opendkim(base.Installer): def custom_backup(self, path): """Backup DKIM keys.""" if os.path.isdir(self.app_config["keys_storage_dir"]): - shutil.copytree(self.app_config["keys_storage_dir"], os.path.join(path, "dkim")) - utils.printcolor( - "DKIM keys saved!", utils.GREEN) + shutil.copytree(self.app_config["keys_storage_dir"], path) + utils.printcolor("DKIM keys saved!", utils.GREEN) diff --git a/modoboa_installer/scripts/postwhite.py b/modoboa_installer/scripts/postwhite.py index 0761cc7..ae079aa 100644 --- a/modoboa_installer/scripts/postwhite.py +++ b/modoboa_installer/scripts/postwhite.py @@ -19,10 +19,7 @@ class Postwhite(base.Installer): "crontab=/etc/cron.d/postwhite", ] no_daemon = True - packages = { - "deb": ["bind9-host", "unzip"], - "rpm": ["bind-utils", "unzip"] - } + packages = {"deb": ["bind9-host", "unzip"], "rpm": ["bind-utils", "unzip"]} def install_from_archive(self, repository, target_dir): """Install from an archive.""" @@ -36,8 +33,7 @@ class Postwhite(base.Installer): if os.path.exists(archive_dir): shutil.rmtree(archive_dir) utils.exec_cmd("unzip master.zip", cwd=target_dir) - utils.exec_cmd( - "mv {name}-master {name}".format(name=app_name), cwd=target_dir) + utils.exec_cmd("mv {name}-master {name}".format(name=app_name), cwd=target_dir) os.unlink(target) return archive_dir @@ -45,10 +41,8 @@ class Postwhite(base.Installer): """Additionnal tasks.""" install_dir = "/usr/local/bin" self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir) - self.postw_dir = self.install_from_archive( - POSTWHITE_REPOSITORY, install_dir) - utils.copy_file( - os.path.join(self.postw_dir, "postwhite.conf"), self.config_dir) + self.postw_dir = self.install_from_archive(POSTWHITE_REPOSITORY, install_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") utils.exec_cmd("{} /etc/postwhite.conf".format(self.postw_bin)) @@ -57,13 +51,13 @@ class Postwhite(base.Installer): postswhite_custom = "/etc/postwhite.conf" if os.path.isfile(postswhite_custom): utils.copy_file(postswhite_custom, path) - utils.printcolor( - "Postwhite configuration saved!", utils.GREEN) + utils.printcolor("Postwhite configuration saved!", utils.GREEN) def restore(self): """Restore config files.""" 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): utils.copy_file(postwhite_backup_configuration, self.config_dir) utils.success("postwhite.conf restored from backup") diff --git a/modoboa_installer/scripts/radicale.py b/modoboa_installer/scripts/radicale.py index fd45069..e0f79a3 100644 --- a/modoboa_installer/scripts/radicale.py +++ b/modoboa_installer/scripts/radicale.py @@ -18,10 +18,7 @@ class Radicale(base.Installer): appname = "radicale" config_files = ["config"] no_daemon = True - packages = { - "deb": ["supervisor"], - "rpm": ["supervisor"] - } + packages = {"deb": ["supervisor"], "rpm": ["supervisor"]} with_user = True def __init__(self, *args, **kwargs): @@ -32,24 +29,25 @@ class Radicale(base.Installer): def _setup_venv(self): """Prepare a dedicated virtualenv.""" python.setup_virtualenv(self.venv_path, sudo_user=self.user) - packages = [ - "Radicale", "pytz", "radicale-modoboa-auth-oauth2" - ] + packages = ["Radicale", "pytz", "radicale-modoboa-auth-oauth2"] python.install_packages(packages, self.venv_path, sudo_user=self.user) def get_template_context(self): """Additional variables.""" context = super().get_template_context() oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app( - "Radicale", "radicale", self.config) + "Radicale", "radicale", self.config + ) hostname = self.config.get("general", "hostname") oauth2_introspection_url = ( f"https://{oauth2_client_id}:{oauth2_client_secret}" f"@{hostname}/api/o/introspect/" ) - context.update({ - "oauth2_introspection_url": oauth2_introspection_url, - }) + context.update( + { + "oauth2_introspection_url": oauth2_introspection_url, + } + ) return context def get_config_files(self): @@ -67,16 +65,19 @@ class Radicale(base.Installer): if not os.path.exists(self.config_dir): utils.mkdir( self.config_dir, - stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | - stat.S_IROTH | stat.S_IXOTH, - 0, 0 + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + 0, + 0, ) super().install_config_files() def restore(self): """Restore collections.""" - radicale_backup = os.path.join( - self.archive_path, "custom/radicale") + radicale_backup = os.path.join(self.archive_path, "custom/radicale") if os.path.isdir(radicale_backup): restore_target = os.path.join(self.home_dir, "collections") if os.path.isdir(restore_target): @@ -87,18 +88,17 @@ class Radicale(base.Installer): def post_run(self): """Additional tasks.""" self._setup_venv() - daemon_name = ( - "supervisor" if package.backend.FORMAT == "deb" else "supervisord" - ) + daemon_name = "supervisor" if package.backend.FORMAT == "deb" else "supervisord" system.enable_service(daemon_name) utils.exec_cmd("service {} stop".format(daemon_name)) utils.exec_cmd("service {} start".format(daemon_name)) def custom_backup(self, path): """Backup collections.""" - radicale_backup = os.path.join(self.config.get( - "radicale", "home_dir", fallback="/srv/radicale"), "collections") + 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( - path, "radicale")) + shutil.copytree(radicale_backup, path) utils.printcolor("Radicale files saved", utils.GREEN) diff --git a/modoboa_installer/scripts/rspamd.py b/modoboa_installer/scripts/rspamd.py index c45681c..f770e96 100644 --- a/modoboa_installer/scripts/rspamd.py +++ b/modoboa_installer/scripts/rspamd.py @@ -16,11 +16,7 @@ class Rspamd(base.Installer): """Rspamd installer.""" appname = "rspamd" - packages = { - "deb": [ - "rspamd", "redis" - ] - } + packages = {"deb": ["rspamd", "redis"]} config_files = [ "local.d/arc.conf", "local.d/dkim_signing.conf", @@ -39,10 +35,9 @@ class Rspamd(base.Installer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.generate_password_condition = ( - not self.upgrade or utils.user_input( - "Do you want to (re)generate rspamd password ? (y/N)").lower().startswith("y") - ) + self.generate_password_condition = not self.upgrade or utils.user_input( + "Do you want to (re)generate rspamd password ? (y/N)" + ).lower().startswith("y") @property def config_dir(self): @@ -54,16 +49,20 @@ class Rspamd(base.Installer): if debian_based_dist: utils.mkdir_safe( "/etc/apt/keyrings", - stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | - stat.S_IROTH | stat.S_IXOTH, - 0, 0 + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + 0, + 0, ) package.backend.add_custom_repository( "rspamd", "http://rspamd.com/apt-stable/", "https://rspamd.com/apt-stable/gpg.key", - codename + codename, ) package.backend.update() @@ -73,16 +72,18 @@ class Rspamd(base.Installer): """Make sure config directory exists.""" user = self.config.get(self.appname, "user") pw = pwd.getpwnam(user) - targets = [ - [self.app_config["dkim_keys_storage_dir"], pw[2], pw[3]] - ] + targets = [[self.app_config["dkim_keys_storage_dir"], pw[2], pw[3]]] for target in targets: if not os.path.exists(target[0]): utils.mkdir( target[0], - stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | - stat.S_IROTH | stat.S_IXOTH, - target[1], target[2] + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + target[1], + target[2], ) super().install_config_files() @@ -101,16 +102,23 @@ class Rspamd(base.Installer): def get_template_context(self): _context = super().get_template_context() - _context["greylisting_disabled"] = "" if not self.app_config["greylisting"].lower() == "true" else "#" - _context["whitelist_auth_enabled"] = "" if self.app_config["whitelist_auth"].lower() == "true" else "#" + _context["greylisting_disabled"] = ( + "" 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: 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: - utils.error("Error setting rspamd password. " - "Please make sure it is not 'q1' or 'q2'." - "Storing the password in plain. See" - "https://rspamd.com/doc/quickstart.html#setting-the-controller-password") + utils.error( + "Error setting rspamd password. " + "Please make sure it is not 'q1' or 'q2'." + "Storing the password in plain. See" + "https://rspamd.com/doc/quickstart.html#setting-the-controller-password" + ) _context["controller_password"] = self.app_config["password"] else: controller_password = controller_password.decode().replace("\n", "") @@ -120,33 +128,31 @@ class Rspamd(base.Installer): def post_run(self): """Additional tasks.""" user = self.config.get(self.appname, "user") - system.add_user_to_group( - self.config.get("modoboa", "user"), - user - ) + system.add_user_to_group(self.config.get("modoboa", "user"), user) if self.config.getboolean("clamav", "enabled"): install("clamav", self.config, self.upgrade, self.archive_path) def custom_backup(self, path): """Backup custom configuration if any.""" - custom_config_dir = os.path.join(self.config_dir, - "/local.d/") - custom_backup_dir = os.path.join(path, "/rspamd/") - local_files = [f for f in os.listdir(custom_config_dir) - if os.path.isfile(custom_config_dir, f) - ] + custom_config_dir = os.path.join(self.config_dir, "local.d/") + local_files = [ + os.path.join(custom_config_dir, f) + for f in os.listdir(custom_config_dir) + if os.path.isfile(os.path.join(custom_config_dir, f)) + ] for file in local_files: - utils.copy_file(file, custom_backup_dir) + print(file) + utils.copy_file(file, path) if len(local_files) != 0: utils.success("Rspamd custom configuration saved!") def restore(self): """Restore custom config files.""" - custom_config_dir = os.path.join(self.config_dir, - "/local.d/") + custom_config_dir = os.path.join(self.config_dir, "/local.d/") custom_backup_dir = os.path.join(self.archive_path, "/rspamd/") 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) ] for f in backed_up_files: diff --git a/run.py b/run.py index efc094a..bc8640d 100755 --- a/run.py +++ b/run.py @@ -26,7 +26,7 @@ PRIMARY_APPS = [ "uwsgi", "nginx", "postfix", - "dovecot" + "dovecot", ] @@ -52,8 +52,7 @@ def backup_system(config, args, antispam_apps): user_value = None while not user_value or not backup_path: utils.printcolor( - "Enter backup path (it must be an empty directory)", - utils.MAGENTA + "Enter backup path (it must be an empty directory)", utils.MAGENTA ) utils.printcolor("CTRL+C to cancel", utils.MAGENTA) user_value = utils.user_input("-> ") @@ -65,9 +64,7 @@ def backup_system(config, args, antispam_apps): utils.copy_file(args.configfile, backup_path) # Backup applications for app in PRIMARY_APPS + antispam_apps: - if (config.has_option(section, "enabled") and - not config.getboolean(section, "enabled") - ): + if config.has_option(app, "enabled") and not config.getboolean(app, "enabled"): continue if app == "dovecot" and args.no_mail: utils.printcolor("Skipping mail backup", utils.BLUE) @@ -76,65 +73,97 @@ def backup_system(config, args, antispam_apps): def config_file_update_complete(backup_location): - utils.printcolor("Update complete. It seems successful.", - utils.BLUE) + utils.printcolor("Update complete. It seems successful.", utils.BLUE) if backup_location is not None: - utils.printcolor("You will find your old config file " - f"here: {backup_location}", - utils.BLUE) + utils.printcolor( + "You will find your old config file " f"here: {backup_location}", utils.BLUE + ) def parser_setup(input_args): parser = argparse.ArgumentParser() - versions = ( - ["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") + versions = ["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys()) parser.add_argument( - "--version", default="latest", choices=versions, - help="Modoboa version to install") + "--debug", action="store_true", default=False, help="Enable debug output" + ) parser.add_argument( - "--stop-after-configfile-check", action="store_true", default=False, - help="Check configuration, generate it if needed and exit") + "--force", action="store_true", default=False, help="Force installation" + ) parser.add_argument( - "--interactive", action="store_true", default=False, - help="Generate configuration file with user interaction") + "--configfile", default="installer.cfg", help="Configuration file to use" + ) parser.add_argument( - "--upgrade", action="store_true", default=False, - help="Run the installer in upgrade mode") + "--version", + default="latest", + choices=versions, + help="Modoboa version to install", + ) parser.add_argument( - "--beta", action="store_true", default=False, - help="Install latest beta release of Modoboa instead of the stable one") + "--stop-after-configfile-check", + action="store_true", + default=False, + help="Check configuration, generate it if needed and exit", + ) parser.add_argument( - "--backup-path", type=str, metavar="path", - help="To use with --silent-backup, you must provide a valid path") + "--interactive", + action="store_true", + default=False, + help="Generate configuration file with user interaction", + ) parser.add_argument( - "--backup", action="store_true", default=False, - help="Backing up interactively previously installed instance" - ) + "--upgrade", + action="store_true", + default=False, + help="Run the installer in upgrade mode", + ) parser.add_argument( - "--silent-backup", action="store_true", default=False, + "--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 " "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)") + "--no-mail", + action="store_true", + default=False, + help="Disable mail backup (save space)", + ) parser.add_argument( - "--restore", type=str, metavar="path", + "--restore", + type=str, + metavar="path", help="Restore a previously backup up modoboa instance on a NEW machine. " - "You MUST provide backup directory" - ) + "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") + "--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) @@ -151,9 +180,7 @@ def main(input_args): is_restoring = True args.configfile = os.path.join(args.restore, args.configfile) if not os.path.exists(args.configfile): - utils.error( - "Installer configuration file not found in backup!" - ) + utils.error("Installer configuration file not found in backup!") sys.exit(1) utils.success("Welcome to Modoboa installer!\n") @@ -165,26 +192,34 @@ def main(input_args): utils.success("Checks complete\n") 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 ( - args.upgrade or args.backup or args.silent_backup): + args.upgrade or args.backup or args.silent_backup + ): utils.error("No config file found.") return # Check if config is outdated and ask user if it needs to be updated if is_config_file_available and outdate_config: - answer = utils.user_input("It seems that your config file is outdated. " - "Would you like to update it? (Y/n) ") + answer = utils.user_input( + "It seems that your config file is outdated. " + "Would you like to update it? (Y/n) " + ) if not answer or answer.lower().startswith("y"): config_file_update_complete(utils.update_config(args.configfile)) 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"): return else: - utils.error("You might encounter unexpected errors ! " - "Make sure to update your config before opening an issue!") + utils.error( + "You might encounter unexpected errors ! " + "Make sure to update your config before opening an issue!" + ) if args.stop_after_configfile_check: return @@ -220,11 +255,20 @@ def main(input_args): # Show concerned components components = [] for section in config.sections(): - if section in ["general", "antispam", "database", "mysql", "postgres", - "certificate", "letsencrypt", "backup"]: + if section in [ + "general", + "antispam", + "database", + "mysql", + "postgres", + "certificate", + "letsencrypt", + "backup", + ]: continue - if (config.has_option(section, "enabled") and - not config.getboolean(section, "enabled")): + if config.has_option(section, "enabled") and not config.getboolean( + section, "enabled" + ): continue incompatible_app_detected = not utils.check_app_compatibility(section, config) if incompatible_app_detected: @@ -237,8 +281,9 @@ def main(input_args): return config.set("general", "force", str(args.force)) utils.printcolor( - "The process can be long, feel free to take a coffee " - "and come back later ;)", utils.BLUE) + "The process can be long, feel free to take a coffee " "and come back later ;)", + utils.BLUE, + ) utils.success("Starting...") package.backend.prepare_system() package.backend.install_many(["sudo", "wget"]) @@ -272,13 +317,8 @@ def main(input_args): "You like the project and want it to be sustainable?\n" "Then don't wait anymore and go sponsor it here:\n" ) - utils.printcolor( - "https://github.com/sponsors/modoboa\n", - utils.YELLOW - ) - utils.success( - "Thank you for your help :-)\n" - ) + utils.printcolor("https://github.com/sponsors/modoboa\n", utils.YELLOW) + utils.success("Thank you for your help :-)\n") if __name__ == "__main__":