refactoring

This commit is contained in:
Spitap
2022-09-19 14:59:43 +02:00
parent b0d56b3989
commit f3811b4b39
8 changed files with 161 additions and 185 deletions

View File

@@ -439,4 +439,13 @@ ConfigDictTemplate = [
] ]
}, },
{
"name": "backup",
"values": [
{
"option": "default_path",
"default": "./modoboa_backup/"
}
]
}
] ]

View File

@@ -141,9 +141,10 @@ class PostgreSQL(Database):
self.dbhost, dbname, dbuser, path) self.dbhost, dbname, dbuser, path)
utils.exec_cmd(cmd, sudo_user=self.dbuser) utils.exec_cmd(cmd, sudo_user=self.dbuser)
def dumpDatabase(self, dbname, dbuser, dbpassword, path): def dump_database(self, dbname, dbuser, dbpassword, path):
"""Dump DB to SQL file""" """Dump DB to SQL file."""
self._pgpass_done = False #Reset pgpass since we backup multiple db (different secret set) # Reset pgpass since we backup multiple db (different secret set)
self._pgpass_done = False
self._setup_pgpass(dbname, dbuser, dbpassword) self._setup_pgpass(dbname, dbuser, dbpassword)
cmd = "pg_dump -h {} -d {} -U {} -O -w > {}".format( cmd = "pg_dump -h {} -d {} -U {} -O -w > {}".format(
self.dbhost, dbname, dbuser, path) self.dbhost, dbname, dbuser, path)
@@ -258,8 +259,9 @@ class MySQL(Database):
"mysql -h {} -u {} -p{} {} < {}".format( "mysql -h {} -u {} -p{} {} < {}".format(
self.dbhost, dbuser, dbpassword, dbname, path) self.dbhost, dbuser, dbpassword, dbname, path)
) )
def dumpDatabase(self, dbname, dbuser, dbpassword, path):
"""Dump DB to SQL file""" def dump_database(self, dbname, dbuser, dbpassword, path):
"""Dump DB to SQL file."""
cmd = "mysqldump -h {} -u {} -p{} {} > {}".format( cmd = "mysqldump -h {} -u {} -p{} {} > {}".format(
self.dbhost, dbuser, dbpassword, dbname, path) self.dbhost, dbuser, dbpassword, dbname, path)
utils.exec_cmd(cmd, sudo_user=self.dbuser) utils.exec_cmd(cmd, sudo_user=self.dbuser)

View File

@@ -26,29 +26,20 @@ def install(appname, config, upgrade, restore):
sys.exit(1) sys.exit(1)
def backup(config, bashArg, nomail): def backup(config, silent_backup, backup_path, nomail):
"""Backup instance""" """Backup instance"""
script = importlib.import_module(
"modoboa_installer.scripts.backup")
try: try:
script = importlib.import_module( getattr(script, "Backup")(
"modoboa_installer.scripts.backup") config, silent_backup, backup_path, nomail).run()
except ImportError:
print("Error importing backup")
try:
getattr(script, "Backup")(config, bashArg, nomail).run()
except utils.FatalError as inst: except utils.FatalError as inst:
utils.printcolor(u"{}".format(inst), utils.RED) utils.printcolor(u"{}".format(inst), utils.RED)
sys.exit(1) sys.exit(1)
def restore(restore): def restore_prep(restore):
"""Restore instance""" """Restore instance"""
try: script = importlib.import_module(
script = importlib.import_module( "modoboa_installer.scripts.restore")
"modoboa_installer.scripts.restore") getattr(script, "Restore")(restore)
except ImportError:
print("Error importing restore")
try:
getattr(script, "Restore")(restore)
except utils.FatalError as inst:
utils.printcolor(u"{}".format(inst), utils.RED)
sys.exit(1)

View File

@@ -78,12 +78,12 @@ class Amavis(base.Installer):
if self.restore: if self.restore:
utils.printcolor( utils.printcolor(
"Trying to restore amavis database from backup", utils.MAGENTA) "Trying to restore amavis database from backup", utils.MAGENTA)
amavisDbBackupPath = os.path.join( amavis_db_backup_path = os.path.join(
self.restore, "databases/amavis.sql") self.restore, "databases/amavis.sql")
if os.path.isfile(amavisDbBackupPath): if os.path.isfile(amavis_db_backup_path):
utils.printcolor( utils.printcolor(
"Amavis database backup found ! Restoring...", utils.GREEN) "Amavis database backup found ! Restoring...", utils.GREEN)
return amavisDbBackupPath return amavis_db_backup_path
utils.printcolor( utils.printcolor(
"Amavis database backup not found, creating empty database", utils.RED) "Amavis database backup not found, creating empty database", utils.RED)

View File

@@ -1,4 +1,4 @@
"""Backup script for pre-installed instance""" """Backup script for pre-installed instance."""
import os import os
import pwd import pwd
@@ -10,60 +10,52 @@ import sys
from .. import database from .. import database
from .. import utils from .. import utils
# TODO: have version of each modoboa componenents saved into the config file to restore the same version
class Backup:
"""
Backup structure ( {optional} ):
{{backup_folder}}
||
||--> installer.cfg
||--> custom
|--> { (copy of) /etc/amavis/conf.d/99-custom }
|--> { (copy of) /etc/postfix/custom_whitelist.cidr }
||--> databases
|--> modoboa.sql
|--> { amavis.sql }
|--> { spamassassin.sql }
||--> mails
|--> vmails
"""
class Backup(): def __init__(self, config, silent_backup, backup_path, nomail):
# Backup structure ( {optional} ):
# {{backup_folder}}
# ||
# ||--> installer.cfg
# ||--> custom
# |--> { (copy of) /etc/amavis/conf.d/99-custom }
# |--> { (copy of) /etc/postfix/custom_whitelist.cidr }
# ||--> databases
# |--> modoboa.sql
# |--> { amavis.sql }
# |--> { spamassassin.sql }
# ||--> mails
# |--> vmails
def __init__(self, config, bashArg, nomail):
self.config = config self.config = config
self.destinationPath = "" self.destinationPath = backup_path
self.BACKUPDIRECTORY = ["custom/", "databases/"]
self.nomail = nomail self.nomail = nomail
self.isBash = False self.silent_backup = silent_backup
self.bash = ""
if bashArg != "NOBASH":
self.isBash = True
self.bash = bashArg
def preparePath(self): def preparePath(self):
pw = pwd.getpwnam("root") pw = pwd.getpwnam("root")
for dir in self.BACKUPDIRECTORY: for dir in ["custom/", "databases/"]:
utils.mkdir_safe(os.path.join(self.destinationPath, dir), utils.mkdir_safe(os.path.join(self.destinationPath, dir),
stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]) stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
def validatePath(self, path): def validatePath(self, path):
"""Check basic condition for backup directory""" """Check basic condition for backup directory."""
try:
pathExists = os.path.exists(path) path_exists = os.path.exists(path)
except:
print("Provided path is not recognized...") if path_exists and os.path.isfile(path):
utils.printcolor(
"Error, you provided a file instead of a directory!", utils.RED)
return False return False
if pathExists and os.path.isfile(path): if not path_exists:
print("Error, you provided a file instead of a directory!") if not self.silent_backup:
return False
if not pathExists:
if not self.isBash:
createDir = input( createDir = input(
f"\"{path}\" doesn't exists, would you like to create it ? [Y/n]\n").lower() f"\"{path}\" doesn't exists, would you like to create it ? [Y/n]\n").lower()
if self.isBash or (not self.isBash and (createDir == "y" or createDir == "yes")): if self.silent_backup or (not self.silent_backup and (createDir == "y" or createDir == "yes")):
pw = pwd.getpwnam("root") pw = pwd.getpwnam("root")
utils.mkdir_safe(path, stat.S_IRWXU | utils.mkdir_safe(path, stat.S_IRWXU |
stat.S_IRWXG, pw[2], pw[3]) stat.S_IRWXG, pw[2], pw[3])
@@ -71,10 +63,10 @@ class Backup():
return False return False
if len(os.listdir(path)) != 0: if len(os.listdir(path)) != 0:
if not self.isBash: if not self.silent_backup:
delDir = input( delDir = input(
"Warning : backup folder is not empty, it will be purged if you continue... [Y/n]\n").lower() "Warning : backup folder is not empty, it will be purged if you continue... [Y/n]\n").lower()
if self.isBash or (not self.isBash and (delDir == "y" or delDir == "yes")): if self.silent_backup or (not self.silent_backup and (delDir == "y" or delDir == "yes")):
shutil.rmtree(path) shutil.rmtree(path)
else: else:
return False return False
@@ -84,31 +76,30 @@ class Backup():
self.preparePath() self.preparePath()
return True return True
def setPath(self): def set_path(self):
"""Setup backup directory""" """Setup backup directory."""
if self.isBash: if self.silent_backup:
if self.bash == "TRUE": if self.destinationPath is None:
date = datetime.datetime.now().strftime("%m_%d_%Y_%H_%M") date = datetime.datetime.now().strftime("%m_%d_%Y_%H_%M")
path = f"/modoboa_backup/backup_{date}/" path = f"./modoboa_backup/backup_{date}/"
self.validatePath(path) self.validatePath(path)
else: else:
validate = self.validatePath(self.bash) if not self.validatePath(self.destinationPath):
if not validate: utils.printcolor(
print("provided bash is not right, exiting...") f"Path provided : {self.destinationPath}", utils.BLUE)
print(f"Path provided : {self.bash}")
sys.exit(1) sys.exit(1)
else: else:
user_value = None user_value = None
while (user_value == '' or user_value == None or not self.validatePath(user_value)): while user_value == "" or user_value is None or not self.validatePath(user_value):
print("Enter backup path, please provide an empty folder.") utils.printcolor(
print("CTRL+C to cancel") "Enter backup path, please provide an empty folder.", utils.MAGENTA)
utils.printcolor("CTRL+C to cancel", utils.MAGENTA)
user_value = utils.user_input("-> ") user_value = utils.user_input("-> ")
def backupConfigFile(self): def config_file_backup(self):
utils.copy_file("installer.cfg", self.destinationPath) utils.copy_file("installer.cfg", self.destinationPath)
def backupMails(self): def mail_backup(self):
if self.nomail: if self.nomail:
utils.printcolor( utils.printcolor(
"Skipping mail backup, no-mail argument provided", utils.MAGENTA) "Skipping mail backup, no-mail argument provided", utils.MAGENTA)
@@ -131,7 +122,7 @@ class Backup():
shutil.copytree(home_path, dst) shutil.copytree(home_path, dst)
utils.printcolor("Mail backup complete!", utils.GREEN) utils.printcolor("Mail backup complete!", utils.GREEN)
def backupCustomConfig(self): def custom_config_backup(self):
"""Custom config : """Custom config :
- Amavis : /etc/amavis/conf.d/99-custom - Amavis : /etc/amavis/conf.d/99-custom
- Postwhite : /etc/postwhite.conf - Postwhite : /etc/postwhite.conf
@@ -140,61 +131,57 @@ class Backup():
"Backing up some custom configuration...", utils.MAGENTA) "Backing up some custom configuration...", utils.MAGENTA)
custom_path = os.path.join( custom_path = os.path.join(
self.destinationPath, self.BACKUPDIRECTORY[0]) self.destinationPath, "custom")
"""AMAVIS""" # AMAVIS
amavis_custom = "/etc/amavis/conf.d/99-custom" if (self.config.has_option("amavis", "enabled") and
if os.path.isfile(amavis_custom): self.config.getboolean("amavis", "enabled")):
utils.copy_file(amavis_custom, custom_path) amavis_custom = "/etc/amavis/conf.d/99-custom"
utils.printcolor("Amavis custom configuration saved!", utils.GREEN) if os.path.isfile(amavis_custom):
utils.copy_file(amavis_custom, custom_path)
utils.printcolor(
"Amavis custom configuration saved!", utils.GREEN)
"""POSTWHITE""" # POSTWHITE
postswhite_custom = "/etc/postwhite.conf" if (self.config.has_option("postwhite", "enabled") and
if os.path.isfile(postswhite_custom): self.config.getboolean("postwhite", "enabled")):
utils.copy_file(postswhite_custom, custom_path) postswhite_custom = os.path.join(self.config.get(
utils.printcolor("Postwhite configuration saved!", utils.GREEN) "postwhite", "config_dir", "postwhite.conf"))
if os.path.isfile(postswhite_custom):
utils.copy_file(postswhite_custom, custom_path)
utils.printcolor(
"Postwhite configuration saved!", utils.GREEN)
def backupDBs(self): def database_backup(self):
"""Backing up databases""" """Backing up databases"""
utils.printcolor("Backing up databases...", utils.MAGENTA) utils.printcolor("Backing up databases...", utils.MAGENTA)
dump_path = os.path.join(self.destinationPath, self.BACKUPDIRECTORY[1]) self.database_dump("modoboa")
self.database_dump("amavis")
self.database_dump("spamassassin")
def database_dump(self, app_name):
dump_path = os.path.join(self.destinationPath, "backup")
backend = database.get_backend(self.config) backend = database.get_backend(self.config)
"""Modoboa""" if app_name == "modoboa" or (self.config.has_option(app_name, "enabled") and
dbname = self.config.get("modoboa", "dbname") self.config.getboolean(app_name, "enabled")):
dbuser = self.config.get("modoboa", "dbuser") dbname = self.config.get(app_name, "dbname")
dbpasswd = self.config.get("modoboa", "dbpassword") dbuser = self.config.get(app_name, "dbuser")
backend.dumpDatabase(dbname, dbuser, dbpasswd, dbpasswd = self.config.get(app_name, "dbpassword")
os.path.join(dump_path, "modoboa.sql")) backend.dump_database(dbname, dbuser, dbpasswd,
os.path.join(dump_path, f"{app_name}.sql"))
"""Amavis""" def backup_completed(self):
if (self.config.has_option("amavis", "enabled") and
self.config.getboolean("amavis", "enabled")):
dbname = self.config.get("amavis", "dbname")
dbuser = self.config.get("amavis", "dbuser")
dbpasswd = self.config.get("amavis", "dbpassword")
backend.dumpDatabase(dbname, dbuser, dbpasswd,
os.path.join(dump_path, "amavis.sql"))
"""SpamAssassin"""
if (self.config.has_option("spamassassin", "enabled") and
self.config.getboolean("spamassassin", "enabled")):
dbname = self.config.get("spamassassin", "dbname")
dbuser = self.config.get("spamassassin", "dbuser")
dbpasswd = self.config.get("spamassassin", "dbpassword")
backend.dumpDatabase(dbname, dbuser, dbpasswd,
os.path.join(dump_path, "spamassassin.sql"))
def backupCompletion(self):
utils.printcolor("Backup process done, your backup is availible here:" utils.printcolor("Backup process done, your backup is availible here:"
f"--> {self.destinationPath}", utils.GREEN) f"--> {self.destinationPath}", utils.GREEN)
def run(self): def run(self):
self.setPath() self.set_path()
self.backupConfigFile() self.config_file_backup()
self.backupMails() self.mail_backup()
self.backupCustomConfig() self.custom_config_backup()
self.backupDBs() self.database_backup()
self.backupCompletion() self.backup_completed()

View File

@@ -161,6 +161,10 @@ class Installer(object):
"""Tasks to execute before the installer starts.""" """Tasks to execute before the installer starts."""
pass pass
def restore(self):
"""Tasks to execute to restore files/databases."""
pass
def post_run(self): def post_run(self):
"""Additionnal tasks.""" """Additionnal tasks."""
pass pass

View File

@@ -5,29 +5,22 @@ from .. import utils
class Restore: class Restore:
def __init__(self, restore): def __init__(self, restore):
"""Restoring pre-check (backup integriety)""" """
"""REQUIRED : modoboa.sql""" Restoring pre-check (backup integriety)
"""OPTIONAL : mails/, custom/, amavis.sql, spamassassin.sql""" REQUIRED : modoboa.sql
"""Only checking required""" OPTIONAL : mails/, custom/, amavis.sql, spamassassin.sql
Only checking required
"""
try: if not os.path.isdir(restore):
if not os.path.isdir(restore): utils.printcolor(
utils.printcolor( "Provided path is not a directory !", utils.RED)
"Provided path is not a directory !", utils.RED)
sys.exit(1)
except:
utils.printcolor("Provided path is not right...", utils.RED)
sys.exit(1) sys.exit(1)
try: modobasql_file = os.path.join(restore, "databases/modoboa.sql")
modobasql_file = os.path.join(restore, "databases/modoboa.sql") if not os.path.isfile(modobasql_file):
if not os.path.isfile(modobasql_file): utils.printcolor(
utils.printcolor( modobasql_file + " not found, please check your backup", utils.RED)
modobasql_file + " not found, please check your backup", utils.RED)
sys.exit(1)
except:
utils.printcolor(modobasql_file +
" not found, please check your backup", utils.RED)
sys.exit(1) sys.exit(1)
# Everything seems allright here, proceding... # Everything seems allright here, proceding...

66
run.py
View File

@@ -46,7 +46,7 @@ def upgrade_disclaimer(config):
) )
def backup_disclamer(): def backup_disclaimer():
"""Display backup disclamer. """ """Display backup disclamer. """
utils.printcolor( utils.printcolor(
"Your mail server will be backed up (messages and databases) locally." "Your mail server will be backed up (messages and databases) locally."
@@ -54,7 +54,7 @@ def backup_disclamer():
" Custom configuration (like to postfix) won't be saved.", utils.BLUE) " Custom configuration (like to postfix) won't be saved.", utils.BLUE)
def restore_disclamer(): def restore_disclaimer():
"""Display restore disclamer. """ """Display restore disclamer. """
utils.printcolor( utils.printcolor(
"You are about to restore a previous installation of Modoboa." "You are about to restore a previous installation of Modoboa."
@@ -69,7 +69,7 @@ def main(input_args):
["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys()) ["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys())
) )
parser.add_argument("--backup", action="store_true", default=False, parser.add_argument("--backup", action="store_true", default=False,
help="Backing up previously installed instance") help="Backing up interactively previously installed instance")
parser.add_argument("--debug", action="store_true", default=False, parser.add_argument("--debug", action="store_true", default=False,
help="Enable debug output") help="Enable debug output")
parser.add_argument("--force", action="store_true", default=False, parser.add_argument("--force", action="store_true", default=False,
@@ -92,17 +92,19 @@ def main(input_args):
"--beta", action="store_true", default=False, "--beta", action="store_true", default=False,
help="Install latest beta release of Modoboa instead of the stable one") help="Install latest beta release of Modoboa instead of the stable one")
parser.add_argument( parser.add_argument(
"--bash", type=str, metavar="path", "--backup-path", type=str, metavar="path",
help="(backup only) - For script usage, No interaction will be required, you must provide a path") help="To use with --silent-backup, you must provide a valid path")
parser.add_argument( parser.add_argument(
"--sbash", action="store_true", default=False, "--silent-backup", action="store_true", default=False,
help="same as --bash but backup will be at /modoboa_backup/Backup_M_Y_d_H_M") 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( parser.add_argument(
"--no-mail", action="store_true", default=False, "--no-mail-backup", action="store_true", default=False,
help="Disable mail backup (save space)") help="Disable mail backup (save space)")
parser.add_argument( 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" help="Restore a previously backup up modoboa instance on a NEW machine. "
"You MUST provide backup directory"
) )
parser.add_argument("domain", type=str, parser.add_argument("domain", type=str,
help="The main domain of your future mail server") help="The main domain of your future mail server")
@@ -111,32 +113,24 @@ def main(input_args):
if args.debug: if args.debug:
utils.ENV["debug"] = True utils.ENV["debug"] = True
if not args.backup and (args.bash != None or args.sbash or args.no_mail):
utils.printcolor("You provided --bash or --sbash without --backup, "
"if you want to do a backup, please provide --backup!", utils.RED)
return
elif args.bash != None and args.sbash:
utils.printcolor("You provided --bash PATH and --sbash at the same time. "
"Please provided only one!", utils.RED)
return
elif args.bash == "TRUE":
utils.printcolor(
"You can't pick *TRUE* as backup directory !", utils.RED)
# Restore prep # Restore prep
isRestoring = False is_restoring = False
if args.restore != None: if args.restore is not None:
isRestoring = True is_restoring = True
args.configfile = os.path.join(args.restore, "installer.cfg") args.configfile = os.path.join(args.restore, "installer.cfg")
if not os.path.exists(args.configfile): if not os.path.exists(args.configfile):
utils.printcolor("installer.cfg from backup not found!", utils.RED) utils.printcolor("installer.cfg from backup not found!", utils.RED)
sys.exit(1) sys.exit(1)
utils.printcolor("Welcome to Modoboa installer!\n", utils.GREEN) utils.printcolor("Welcome to Modoboa installer!\n", utils.GREEN)
wasConfigFileAlreadyThere = utils.check_config_file( is_config_file_availible = utils.check_config_file(
args.configfile, args.interactive, args.upgrade, args.backup, isRestoring) args.configfile, args.interactive, args.upgrade, args.backup, is_restoring)
if args.stop_after_configfile_check or (not wasConfigFileAlreadyThere and args.backup): if is_config_file_availible and args.backup:
utils.printcolor("No config file found,", utils.RED)
return
if args.stop_after_configfile_check:
return return
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -151,18 +145,14 @@ def main(input_args):
# Display disclaimer python 3 linux distribution # Display disclaimer python 3 linux distribution
if args.upgrade: if args.upgrade:
upgrade_disclaimer(config) upgrade_disclaimer(config)
elif args.backup: elif args.backup or args.silent_backup:
backup_disclamer() backup_disclaimer()
bashArg = "NOBASH" scripts.backup(config, args.silent_backup,
if args.bash != None: args.backup_path, args.no_mail)
bashArg = args.bash
elif args.sbash:
bashArg = "TRUE"
scripts.backup(config, bashArg, args.no_mail)
return return
elif args.restore: elif args.restore:
restore_disclamer() restore_disclaimer()
scripts.restore(args.restore) scripts.restore_prep(args.restore)
else: else:
installation_disclaimer(args, config) installation_disclaimer(args, config)
@@ -209,7 +199,7 @@ def main(input_args):
utils.GREEN) utils.GREEN)
else: else:
utils.printcolor( utils.printcolor(
"Resotre complete! You can enjoy Modoboa at https://{} (same credentials as before)" "Restore complete! You can enjoy Modoboa at https://{} (same credentials as before)"
.format(config.get("general", "hostname")), .format(config.get("general", "hostname")),
utils.GREEN) utils.GREEN)