From 335a676a1e3143c4ae494dda2913fb222d4f27f3 Mon Sep 17 00:00:00 2001 From: Spitap Date: Thu, 2 Mar 2023 20:28:55 +0100 Subject: [PATCH] Added ability to update configfile --- modoboa_installer/utils.py | 70 ++++++++++++++++++++++++++++++++++++-- run.py | 19 ++++++++++- test-requirements.txt | 1 - tests.py | 46 +++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 7 deletions(-) diff --git a/modoboa_installer/utils.py b/modoboa_installer/utils.py index 5f97ccb..00317d1 100644 --- a/modoboa_installer/utils.py +++ b/modoboa_installer/utils.py @@ -320,8 +320,8 @@ def get_entry_value(entry, interactive): return user_value if user_value else default_value -def gen_config(dest, interactive=False): - """Create config file from dict template""" +def load_config_template(interactive): + """Instanciate a configParser object with the predefined template.""" tpl_dict = config_dict_template.ConfigDictTemplate config = configparser.ConfigParser() # only ask about options we need, else still generate default @@ -337,6 +337,72 @@ def gen_config(dest, interactive=False): for config_entry in section["values"]: value = get_entry_value(config_entry, interactive_section) config.set(section["name"], config_entry["option"], value) + return config + + +def update_config(path): + """Update an existing config file.""" + config = configparser.ConfigParser() + with open(path) as fp: + config.read_file(fp) + new_config = load_config_template(False) + + old_sections = config.sections() + new_sections = new_config.sections() + + update = False + + dropped_sections = list(set(old_sections) - set(new_sections)) + + if len(dropped_sections) > 0: + printcolor("Follow section(s) will not be ported " + "due to being deleted or renamed: " + + ', '.join(dropped_sections), + RED) + + for section in new_sections: + if section in old_sections: + new_options = new_config.options(section) + old_options = config.options(section) + + dropped_options = list(set(old_options) - set(new_options)) + + if len(dropped_options) > 0: + printcolor(f"Following option(s) from section: {section}, " + "will not be ported due to being " + "deleted or renamed: " + + ', '.join(dropped_options), + RED) + + for option in new_options: + if option in old_options: + value = config.get(section, option, raw=True) + if value != new_config.get(section, option, raw=True): + update = True + new_config.set(section, option, value) + + if update: + # Backing up old config file + date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + dest = f"{os.path.splitext(path)[0]}_{date}.old" + shutil.copy(path, dest) + + # Overwritting old config file + with open(path, "w") as configfile: + new_config.write(configfile) + + # Set file owner to running user and group, and set config file permission to 600 + current_username = getpass.getuser() + current_user = pwd.getpwnam(current_username) + os.chown(dest, current_user[2], current_user[3]) + os.chmod(dest, stat.S_IRUSR | stat.S_IWUSR) + + return dest + + +def gen_config(dest, interactive=False): + """Create config file from dict template""" + config = load_config_template(interactive) with open(dest, "w") as configfile: config.write(configfile) diff --git a/run.py b/run.py index edd5720..5cd26ba 100755 --- a/run.py +++ b/run.py @@ -134,6 +134,10 @@ def main(input_args): parser.add_argument( "--stop-after-configfile-check", action="store_true", default=False, help="Check configuration, generate it if needed and exit") + parser.add_argument( + "--update-configfile", action="store_true", default=False, + help="Attempt to update the config file. " + "Installer will stop after performing the update.") parser.add_argument( "--interactive", action="store_true", default=False, help="Generate configuration file with user interaction") @@ -153,7 +157,8 @@ def main(input_args): 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") + "backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M " + "if --backup-path is not provided") parser.add_argument( "--restore", type=str, metavar="path", help="Restore a previously backup up modoboa instance on a NEW machine. " @@ -178,6 +183,18 @@ def main(input_args): sys.exit(1) utils.success("Welcome to Modoboa installer!\n") + + # Update configfile + if args.update_configfile: + backup_location = utils.update_config(args.configfile) + 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) + return + is_config_file_available = utils.check_config_file( args.configfile, args.interactive, args.upgrade, args.backup, is_restoring) diff --git a/test-requirements.txt b/test-requirements.txt index 55e3fa0..6ec2cdd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,2 @@ codecov mock -six diff --git a/tests.py b/tests.py index 201a252..55eabd1 100644 --- a/tests.py +++ b/tests.py @@ -6,8 +6,13 @@ import sys import tempfile import unittest -from six import StringIO -from six.moves import configparser +from io import StringIO +from pathlib import Path + +try: + import configparser +except ImportError: + import ConfigParser as configparser try: from unittest.mock import patch except ImportError: @@ -26,7 +31,11 @@ class ConfigFileTestCase(unittest.TestCase): def tearDown(self): """Delete temp dir.""" - shutil.rmtree(self.workdir) + out = StringIO() + sys.stdout = out + print(self.workdir) + #shutil.rmtree(self.workdir) + pass def test_configfile_generation(self): """Check simple case.""" @@ -57,6 +66,37 @@ class ConfigFileTestCase(unittest.TestCase): self.assertEqual(config.get("certificate", "type"), "self-signed") self.assertEqual(config.get("database", "engine"), "postgres") + def test_updating_configfile(self): + """Check configfile update mechanism.""" + cfgfile_temp = os.path.join(self.workdir, "installer_old.cfg") + + out = StringIO() + sys.stdout = out + run.main([ + "--stop-after-configfile-check", + "--configfile", cfgfile_temp, + "example.test"]) + self.assertTrue(os.path.exists(cfgfile_temp)) + + # Adding a dummy section + with open(cfgfile_temp, "a") as fp: + fp.write( +""" +[dummy] + weird_old_option = "hey +""") + print("here") + print(os.path.isfile(cfgfile_temp)) + + out = StringIO() + sys.stdout = out + run.main([ + "--update-configfile", + "--configfile", cfgfile_temp, + "example.test"]) + self.assertIn("dummy", out.getvalue()) + self.assertTrue(Path(self.workdir).glob("*.old")) + @patch("modoboa_installer.utils.user_input") def test_interactive_mode_letsencrypt(self, mock_user_input): """Check interactive mode."""