diff --git a/README.rst b/README.rst index dd8424e..e2af8c7 100644 --- a/README.rst +++ b/README.rst @@ -92,6 +92,58 @@ You can activate it as follows:: It will automatically install latest versions of modoboa and its plugins. +Backup mode +------------ + +An experimental backup mode is available. + +.. warning:: + + You must keep the original configuration file, i.e. the one used for + the installation. Otherwise, you will need to recreate it manually with the right information ! + +You can start the process as follows:: + + $ sudo ./run.py --backup + +Then follow the step on the console. + +There are also a non-interactive mode: + +1. Silent mode + +Command:: + + $ sudo ./run.py --silent-backup + +This mode is the silent batch mode, when executed, it will create /modoboa_backup/ and each time you execute it, it will create a new backup directory with current date and time. + +You can supply a custom path. + +Command:: + + $ sudo ./run.py --silent-backup --backup-path /path/of/backup/directory + +This mode is the same as silent batch mode, but you provide the path to the backup directory you want. + + +If you want to disable mail backup:: + + $ sudo ./run.py {--backup|--silent-backup} --no-mail-backup + +This can be useful for larger instance. + +Restore mode +------------ + +An experimental restore mode is available. + +You can start the process as follows:: + + $ sudo ./run.py --restore /path/to/backup/directory/ + +Then wait for the process to finish + Change the generated hostname ----------------------------- diff --git a/modoboa_installer/config_dict_template.py b/modoboa_installer/config_dict_template.py index b1d8252..1895d08 100644 --- a/modoboa_installer/config_dict_template.py +++ b/modoboa_installer/config_dict_template.py @@ -1,6 +1,8 @@ import random import string +from constants import DEFAULT_BACKUP_DIRECTORY + def make_password(length=16): """Create a random password.""" @@ -439,4 +441,13 @@ ConfigDictTemplate = [ ] }, + { + "name": "backup", + "values": [ + { + "option": "default_path", + "default": DEFAULT_BACKUP_DIRECTORY + } + ] + } ] diff --git a/modoboa_installer/constants.py b/modoboa_installer/constants.py new file mode 100644 index 0000000..7df5082 --- /dev/null +++ b/modoboa_installer/constants.py @@ -0,0 +1 @@ +DEFAULT_BACKUP_DIRECTORY = "./modoboa_backup/" diff --git a/modoboa_installer/database.py b/modoboa_installer/database.py index d19d8d7..0242800 100644 --- a/modoboa_installer/database.py +++ b/modoboa_installer/database.py @@ -146,6 +146,15 @@ class PostgreSQL(Database): self.dbhost, self.dbport, dbname, dbuser, path) utils.exec_cmd(cmd, sudo_user=self.dbuser) + def dump_database(self, dbname, dbuser, dbpassword, path): + """Dump DB to SQL file.""" + # Reset pgpass since we backup multiple db (different secret set) + self._pgpass_done = False + self._setup_pgpass(dbname, dbuser, dbpassword) + cmd = "pg_dump -h {} -d {} -U {} -O -w > {}".format( + self.dbhost, dbname, dbuser, path) + utils.exec_cmd(cmd, sudo_user=self.dbuser) + class MySQL(Database): @@ -258,6 +267,12 @@ class MySQL(Database): self.dbhost, self.dbport, dbuser, dbpassword, dbname, path) ) + def dump_database(self, dbname, dbuser, dbpassword, path): + """Dump DB to SQL file.""" + cmd = "mysqldump -h {} -u {} -p{} {} > {}".format( + self.dbhost, dbuser, dbpassword, dbname, path) + utils.exec_cmd(cmd, sudo_user=self.dbuser) + def get_backend(config): """Return appropriate backend.""" diff --git a/modoboa_installer/scripts/__init__.py b/modoboa_installer/scripts/__init__.py index 3edfa66..94efdcb 100644 --- a/modoboa_installer/scripts/__init__.py +++ b/modoboa_installer/scripts/__init__.py @@ -6,11 +6,12 @@ import sys from .. import utils -def install(appname, config, upgrade): +def install(appname, config, upgrade, restore): """Install an application.""" if (config.has_option(appname, "enabled") and not config.getboolean(appname, "enabled")): return + utils.printcolor("Installing {}".format(appname), utils.MAGENTA) try: script = importlib.import_module( @@ -19,7 +20,26 @@ def install(appname, config, upgrade): print("Unknown application {}".format(appname)) sys.exit(1) try: - getattr(script, appname.capitalize())(config, upgrade).run() + getattr(script, appname.capitalize())(config, upgrade, restore).run() except utils.FatalError as inst: utils.printcolor(u"{}".format(inst), utils.RED) sys.exit(1) + + +def backup(config, silent_backup, backup_path, nomail): + """Backup instance""" + script = importlib.import_module( + "modoboa_installer.scripts.backup") + try: + getattr(script, "Backup")( + config, silent_backup, backup_path, nomail).run() + except utils.FatalError as inst: + utils.printcolor(u"{}".format(inst), utils.RED) + sys.exit(1) + + +def restore_prep(restore): + """Restore instance""" + 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 4fdc187..8988695 100644 --- a/modoboa_installer/scripts/amavis.py +++ b/modoboa_installer/scripts/amavis.py @@ -1,7 +1,6 @@ """Amavis related functions.""" import os -import platform from .. import package from .. import utils @@ -43,6 +42,14 @@ class Amavis(base.Installer): def get_config_files(self): """Return appropriate config files.""" if package.backend.FORMAT == "deb": + if self.restore is not None: + amavis_custom_configuration = os.path.join( + self.restore, "custom/99-custom") + if os.path.isfile(amavis_custom_configuration): + utils.copy_file(amavis_custom_configuration, os.path.join( + self.config_dir, "conf.d")) + utils.printcolor( + "Custom amavis configuration restored.", utils.GREEN) return [ "conf.d/05-node_id", "conf.d/15-content_filter_mode", "conf.d/50-user"] @@ -70,6 +77,11 @@ class Amavis(base.Installer): def get_sql_schema_path(self): """Return schema path.""" + if self.restore: + db_dump_path = self._restore_database_dump("amavis") + if db_dump_path is not None: + return db_dump_path + version = package.backend.get_installed_version("amavisd-new") if version is None: # Fallback to amavis... @@ -83,7 +95,7 @@ class Amavis(base.Installer): 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") + raise utils.FatalError("Failed to find amavis database schema") return path def pre_run(self): @@ -93,5 +105,5 @@ class Amavis(base.Installer): def post_run(self): """Additional tasks.""" - install("spamassassin", self.config, self.upgrade) - install("clamav", self.config, self.upgrade) + install("spamassassin", self.config, self.upgrade, self.restore) + install("clamav", self.config, self.upgrade, self.restore) diff --git a/modoboa_installer/scripts/backup.py b/modoboa_installer/scripts/backup.py new file mode 100644 index 0000000..ffa4fda --- /dev/null +++ b/modoboa_installer/scripts/backup.py @@ -0,0 +1,229 @@ +"""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.printcolor( + "Error, you provided a file instead of a directory!", utils.RED) + return False + + if not path_exists: + if not self.silent_backup: + create_dir = input( + f"\"{path}\" doesn't exists, 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.printcolor( + "Error, backup directory not present.", utils.RED + ) + 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 == "y" or delete_dir == "yes")): + 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.printcolor( + "Error, backup dir not clean.", utils.RED + ) + 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.printcolor("Error backing up Email, provided path " + f" ({home_path}) seems not right...", utils.RED) + + 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 (calendat, 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 availible 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 8627132..3a1505b 100644 --- a/modoboa_installer/scripts/base.py +++ b/modoboa_installer/scripts/base.py @@ -20,10 +20,11 @@ class Installer(object): with_db = False config_files = [] - def __init__(self, config, upgrade): + def __init__(self, config, upgrade, restore): """Get configuration.""" self.config = config self.upgrade = upgrade + self.restore = restore if self.config.has_section(self.appname): self.app_config = dict(self.config.items(self.appname)) self.dbengine = self.config.get("database", "engine") @@ -159,6 +160,20 @@ class Installer(object): self.post_run() self.restart_daemon() + def _restore_database_dump(self, app_name): + """Restore database dump from a dump.""" + + utils.printcolor( + f"Trying to restore {app_name} database from backup.", utils.MAGENTA) + database_backup_path = os.path.join( + self.restore, f"databases/{app_name}.sql") + if os.path.isfile(database_backup_path): + utils.printcolor( + f"{app_name.capitalize()} database backup found ! Restoring...", utils.GREEN) + return database_backup_path + utils.printcolor( + f"{app_name.capitalize()} database backup not found, creating empty database.", utils.RED) + def pre_run(self): """Tasks to execute before the installer starts.""" pass diff --git a/modoboa_installer/scripts/dovecot.py b/modoboa_installer/scripts/dovecot.py index de98c6f..e3d9e04 100644 --- a/modoboa_installer/scripts/dovecot.py +++ b/modoboa_installer/scripts/dovecot.py @@ -3,6 +3,7 @@ import glob import os import pwd +import shutil from .. import database from .. import package @@ -95,6 +96,25 @@ class Dovecot(base.Installer): def post_run(self): """Additional tasks.""" + mail_dir = os.path.join(self.restore, "mails/") + if self.restore is not None and len(os.listdir(mail_dir)) > 0: + utils.printcolor( + "Copying mail backup over dovecot directory.", utils.GREEN) + + if os.path.exists(self.home_dir): + shutil.rmtree(self.home_dir) + + shutil.copytree(mail_dir, self.home_dir) + # Resetting permission for vmail + for dirpath, dirnames, filenames in os.walk(self.home_dir): + shutil.chown(dirpath, self.user, self.user) + for filename in filenames: + shutil.chown(os.path.join(dirpath, filename), + self.user, self.user) + elif self.restore is not None: + utils.printcolor( + "It seems that mails were not backed up, skipping mail restoration.", utils.MAGENTA) + if self.dbengine == "postgres": dbname = self.config.get("modoboa", "dbname") dbuser = self.config.get("modoboa", "dbuser") diff --git a/modoboa_installer/scripts/modoboa.py b/modoboa_installer/scripts/modoboa.py index ea1320c..9719782 100644 --- a/modoboa_installer/scripts/modoboa.py +++ b/modoboa_installer/scripts/modoboa.py @@ -192,6 +192,14 @@ class Modoboa(base.Installer): self.backend.grant_access( self.config.get("amavis", "dbname"), self.dbuser) + def get_sql_schema_path(self): + if self.restore is not None: + db_dump_path = self._restore_database_dump("modoboa") + if db_dump_path is not None: + return db_dump_path + + return super().get_sql_schema_path()() + def get_packages(self): """Include extra packages if needed.""" packages = super(Modoboa, self).get_packages() diff --git a/modoboa_installer/scripts/nginx.py b/modoboa_installer/scripts/nginx.py index bfa0850..3537842 100644 --- a/modoboa_installer/scripts/nginx.py +++ b/modoboa_installer/scripts/nginx.py @@ -26,7 +26,7 @@ class Nginx(base.Installer): "app_instance_path": ( self.config.get(app, "instance_path")), "uwsgi_socket_path": ( - Uwsgi(self.config, self.upgrade).get_socket_path(app)) + Uwsgi(self.config, self.upgrade, self.restore).get_socket_path(app)) }) return context diff --git a/modoboa_installer/scripts/opendkim.py b/modoboa_installer/scripts/opendkim.py index 9875a93..1dec655 100644 --- a/modoboa_installer/scripts/opendkim.py +++ b/modoboa_installer/scripts/opendkim.py @@ -46,6 +46,18 @@ class Opendkim(base.Installer): stat.S_IROTH | stat.S_IXOTH, target[1], target[2] ) + # Restore dkim keys from backup if restoring + if self.restore is not None: + dkim_keys_backup = os.path.join( + self.restore, "custom/dkim") + if os.path.isdir(dkim_keys_backup): + for file in os.listdir(dkim_keys_backup): + file_path = os.path.join(dkim_keys_backup, file) + if os.path.isfile(file_path): + utils.copy_file(file_path, self.config.get( + "opendkim", "keys_storage_dir", fallback="/var/lib/dkim")) + utils.printcolor( + "DKIM keys restored from backup", utils.GREEN) super(Opendkim, self).install_config_files() def get_template_context(self): diff --git a/modoboa_installer/scripts/postfix.py b/modoboa_installer/scripts/postfix.py index 607a905..fc1a8f2 100644 --- a/modoboa_installer/scripts/postfix.py +++ b/modoboa_installer/scripts/postfix.py @@ -97,4 +97,4 @@ class Postfix(base.Installer): utils.exec_cmd("postalias {}".format(aliases_file)) # Postwhite - install("postwhite", self.config, self.upgrade) + install("postwhite", self.config, self.upgrade, self.restore) diff --git a/modoboa_installer/scripts/postwhite.py b/modoboa_installer/scripts/postwhite.py index 039bd11..ec54639 100644 --- a/modoboa_installer/scripts/postwhite.py +++ b/modoboa_installer/scripts/postwhite.py @@ -47,6 +47,15 @@ class Postwhite(base.Installer): self.install_from_archive(SPF_TOOLS_REPOSITORY, install_dir) postw_dir = self.install_from_archive( POSTWHITE_REPOSITORY, install_dir) - utils.copy_file(os.path.join(postw_dir, "postwhite.conf"), "/etc") + # Attempt to restore config file from backup + if self.restore is not None: + postwhite_backup_configuration = os.path.join( + self.restore, "custom/postwhite.conf") + if os.path.isfile(postwhite_backup_configuration): + utils.copy_file(postwhite_backup_configuration, self.config_dir) + utils.printcolor( + "postwhite.conf restored from backup", utils.GREEN) + else: + utils.copy_file(os.path.join(postw_dir, "postwhite.conf"), self.config_dir) postw_bin = os.path.join(postw_dir, "postwhite") utils.exec_cmd("{} /etc/postwhite.conf".format(postw_bin)) diff --git a/modoboa_installer/scripts/radicale.py b/modoboa_installer/scripts/radicale.py index 889403f..df6e38b 100644 --- a/modoboa_installer/scripts/radicale.py +++ b/modoboa_installer/scripts/radicale.py @@ -1,6 +1,7 @@ """Radicale related tasks.""" import os +import shutil import stat from .. import package @@ -70,6 +71,17 @@ class Radicale(base.Installer): stat.S_IROTH | stat.S_IXOTH, 0, 0 ) + # Attempt to restore radicale collections from backup + if self.restore is not None: + radicale_backup = os.path.join( + self.restore, "custom/radicale") + if os.path.isdir(radicale_backup): + restore_target = os.path.join(self.home_dir, "collections") + if os.path.isdir(restore_target): + shutil.rmtree(restore_target) + shutil.copytree(radicale_backup, restore_target) + utils.printcolor( + "Radicale collections restored from backup", utils.GREEN) super(Radicale, self).install_config_files() def post_run(self): diff --git a/modoboa_installer/scripts/restore.py b/modoboa_installer/scripts/restore.py new file mode 100644 index 0000000..d5076e9 --- /dev/null +++ b/modoboa_installer/scripts/restore.py @@ -0,0 +1,26 @@ +import os +import sys +from .. import utils + + +class Restore: + def __init__(self, restore): + """ + Restoring pre-check (backup integriety) + REQUIRED : modoboa.sql + OPTIONAL : mails/, custom/, amavis.sql, spamassassin.sql + Only checking required + """ + + if not os.path.isdir(restore): + utils.printcolor( + "Provided path is not a directory !", utils.RED) + sys.exit(1) + + modoba_sql_file = os.path.join(restore, "databases/modoboa.sql") + if not os.path.isfile(modoba_sql_file): + utils.printcolor( + modoba_sql_file + " not found, please check your backup", utils.RED) + sys.exit(1) + + # Everything seems allright here, proceding... diff --git a/modoboa_installer/scripts/spamassassin.py b/modoboa_installer/scripts/spamassassin.py index b2d0f35..f47ebd8 100644 --- a/modoboa_installer/scripts/spamassassin.py +++ b/modoboa_installer/scripts/spamassassin.py @@ -25,6 +25,10 @@ class Spamassassin(base.Installer): def get_sql_schema_path(self): """Return SQL schema.""" + if self.restore is not None: + db_dump_path = self._restore_database_dump("spamassassin") + if db_dump_path is not None: + return db_dump_path if self.dbengine == "postgres": fname = "bayes_pg.sql" else: @@ -63,7 +67,7 @@ class Spamassassin(base.Installer): "pyzor --homedir {} discover".format(pw[5]), sudo_user=amavis_user, login=False ) - install("razor", self.config, self.upgrade) + install("razor", self.config, self.upgrade, self.restore) if utils.dist_name() in ["debian", "ubuntu"]: utils.exec_cmd( "perl -pi -e 's/^CRON=0/CRON=1/' /etc/cron.daily/spamassassin") diff --git a/modoboa_installer/utils.py b/modoboa_installer/utils.py index 8a793af..cf19ff9 100644 --- a/modoboa_installer/utils.py +++ b/modoboa_installer/utils.py @@ -107,6 +107,13 @@ def mkdir(path, mode, uid, gid): os.chown(path, uid, gid) +def mkdir_safe(path, mode, uid, gid): + """Create a directory. Safe way (-p)""" + if not os.path.exists(path): + os.makedirs(os.path.abspath(path), mode) + mkdir(path, mode, uid, gid) + + def make_password(length=16): """Create a random password.""" return "".join( @@ -163,19 +170,32 @@ def copy_from_template(template, dest, context): fp.write(ConfigFileTemplate(buf).substitute(context)) -def check_config_file(dest, interactive=False, upgrade=False): +def check_config_file(dest, interactive=False, upgrade=False, backup=False, restore=False): """Create a new installer config file if needed.""" + is_present = True if os.path.exists(dest): - return + return is_present if upgrade: printcolor( "You cannot upgrade an existing installation without a " "configuration file.", RED) sys.exit(1) + elif backup: + is_present = False + printcolor( + "Your configuration file hasn't been found. A new one will be generated. " + "Please edit it with correct password for the databases !", RED) + elif restore: + printcolor( + "You cannot restore an existing installation without a " + f"configuration file. (file : {dest} has not been found...", RED) + sys.exit(1) + printcolor( "Configuration file {} not found, creating new one." .format(dest), YELLOW) gen_config(dest, interactive) + return is_present def has_colours(stream): diff --git a/run.py b/run.py index 8bfeaf2..c07040e 100755 --- a/run.py +++ b/run.py @@ -3,6 +3,7 @@ """An installer for Modoboa.""" import argparse +import os try: import configparser except ImportError: @@ -45,12 +46,30 @@ def upgrade_disclaimer(config): ) +def backup_disclaimer(): + """Display backup disclamer. """ + utils.printcolor( + "Your mail server will be backed up (messages and databases) locally." + " !! You should really transfer the backup somewhere else..." + " Custom configuration (like to postfix) won't be saved.", utils.BLUE) + + +def restore_disclaimer(): + """Display restore disclamer. """ + utils.printcolor( + "You are about to restore a previous installation of Modoboa." + "If a new version has been released in between, please update your database !", + utils.BLUE) + + def main(input_args): """Install process.""" parser = argparse.ArgumentParser() versions = ( ["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys()) ) + parser.add_argument("--backup", action="store_true", default=False, + help="Backing up interactively previously installed instance") parser.add_argument("--debug", action="store_true", default=False, help="Enable debug output") parser.add_argument("--force", action="store_true", default=False, @@ -72,16 +91,48 @@ def main(input_args): 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( + "--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") + parser.add_argument( + "--no-mail-backup", 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("domain", type=str, help="The main domain of your future mail server") args = parser.parse_args(input_args) if args.debug: utils.ENV["debug"] = True + + # Restore prep + is_restoring = False + if args.restore is not None: + is_restoring = True + args.configfile = os.path.join(args.restore, "installer.cfg") + if not os.path.exists(args.configfile): + utils.printcolor("installer.cfg from backup not found!", utils.RED) + sys.exit(1) + utils.printcolor("Welcome to Modoboa installer!\n", utils.GREEN) - utils.check_config_file(args.configfile, args.interactive, args.upgrade) + is_config_file_available = utils.check_config_file( + args.configfile, args.interactive, args.upgrade, args.backup, is_restoring) + + if is_config_file_available and args.backup: + utils.printcolor("No config file found,", utils.RED) + return + if args.stop_after_configfile_check: return + config = configparser.ConfigParser() with open(args.configfile) as fp: config.read_file(fp) @@ -91,11 +142,20 @@ def main(input_args): config.set("dovecot", "domain", args.domain) config.set("modoboa", "version", args.version) config.set("modoboa", "install_beta", str(args.beta)) - # Display disclaimerpython 3 linux distribution - if not args.upgrade: - installation_disclaimer(args, config) - else: + # Display disclaimer python 3 linux distribution + if args.upgrade: upgrade_disclaimer(config) + elif args.backup or args.silent_backup: + backup_disclaimer() + scripts.backup(config, args.silent_backup, + args.backup_path, args.no_mail_backup) + return + elif args.restore: + restore_disclaimer() + scripts.restore_prep(args.restore) + else: + installation_disclaimer(args, config) + # Show concerned components components = [] for section in config.sections(): @@ -121,21 +181,27 @@ def main(input_args): ssl_backend = ssl.get_backend(config) if ssl_backend and not args.upgrade: ssl_backend.generate_cert() - scripts.install("amavis", config, args.upgrade) - scripts.install("modoboa", config, args.upgrade) - scripts.install("automx", config, args.upgrade) - scripts.install("radicale", config, args.upgrade) - scripts.install("uwsgi", config, args.upgrade) - scripts.install("nginx", config, args.upgrade) - scripts.install("opendkim", config, args.upgrade) - scripts.install("postfix", config, args.upgrade) - scripts.install("dovecot", config, args.upgrade) + scripts.install("amavis", config, args.upgrade, args.restore) + scripts.install("modoboa", config, args.upgrade, args.restore) + scripts.install("automx", config, args.upgrade, args.restore) + scripts.install("radicale", config, args.upgrade, args.restore) + scripts.install("uwsgi", config, args.upgrade, args.restore) + scripts.install("nginx", config, args.upgrade, args.restore) + scripts.install("opendkim", config, args.upgrade, args.restore) + scripts.install("postfix", config, args.upgrade, args.restore) + scripts.install("dovecot", config, args.upgrade, args.restore) system.restart_service("cron") package.backend.restore_system() - utils.printcolor( - "Congratulations! You can enjoy Modoboa at https://{} (admin:password)" - .format(config.get("general", "hostname")), - utils.GREEN) + if not args.restore: + utils.printcolor( + "Congratulations! You can enjoy Modoboa at https://{} (admin:password)" + .format(config.get("general", "hostname")), + utils.GREEN) + else: + utils.printcolor( + "Restore complete! You can enjoy Modoboa at https://{} (same credentials as before)" + .format(config.get("general", "hostname")), + utils.GREEN) if __name__ == "__main__":