From 19ac9350d774d1b63fb0564bdb25e2fa95ba6b00 Mon Sep 17 00:00:00 2001 From: Yohann Rebattu Date: Sun, 8 Oct 2017 11:29:34 +0200 Subject: [PATCH] Add configuration wizard (#158) * add --interactive option refs #133 * delete uneeded template as config is a dict now * minor changes after code review --- installer.cfg.template | 103 ------ modoboa_installer/config_dict_template.py | 362 ++++++++++++++++++++++ modoboa_installer/utils.py | 83 ++++- run.py | 5 +- 4 files changed, 438 insertions(+), 115 deletions(-) delete mode 100644 installer.cfg.template create mode 100644 modoboa_installer/config_dict_template.py diff --git a/installer.cfg.template b/installer.cfg.template deleted file mode 100644 index 7e2cfe7..0000000 --- a/installer.cfg.template +++ /dev/null @@ -1,103 +0,0 @@ -[general] -# %(domain)s is the value specified when launching the installer -hostname = mail.%(domain)s - -[certificate] -generate = true -# Choose between self-signed or letsencrypt -type = self-signed - -[letsencrypt] -email = admin@example.com - -[database] -# Select database engine : postgres or mysql -engine = postgres -#engine = mysql -host = 127.0.0.1 -install = true - -[postgres] -user = postgres -password = - -[mysql] -user = root -password = $mysql_password -charset = utf8 -collation = utf8_general_ci - -[modoboa] -user = modoboa -home_dir = /srv/modoboa -venv_path = %(home_dir)s/env -instance_path = %(home_dir)s/instance -timezone = Europe/Paris -dbname = modoboa -dbuser = modoboa -dbpassword = $modoboa_password -# Extensions to install -# also available: modoboa-radicale modoboa-dmarc modoboa-imap-migration -extensions = modoboa-amavis modoboa-pdfcredentials modoboa-postfix-autoreply modoboa-sievefilters modoboa-stats modoboa-webmail modoboa-contacts - -# Deploy Modoboa and enable development mode -devmode = false - -[automx] -enabled = true -user = automx -config_dir = /etc -home_dir = /srv/automx -venv_path = %(home_dir)s/env -instance_path = %(home_dir)s/instance - -[amavis] -enabled = true -user = amavis -max_servers = 1 - -dbname = amavis -dbuser = amavis -dbpassword = $amavis_password - -[clamav] -enabled = true -user = clamav - -[dovecot] -enabled = true -config_dir = /etc/dovecot -user = vmail -home_dir = /srv/vmail -mailboxes_owner = vmail -# Enable extra procotols (in addition to imap and lmtp) -# Example: pop3 -extra_protocols = -# Replace localhost with your domain -postmaster_address = postmaster@localhost - -[nginx] -enabled = true -config_dir = /etc/nginx - -[razor] -enabled = true -config_dir = /etc/razor - -[postfix] -enabled = true -config_dir = /etc/postfix -message_size_limit = 11534336 - -[spamassassin] -enabled = true -config_dir = /etc/mail/spamassassin - -dbname = spamassassin -dbuser = spamassassin -dbpassword = $sa_password - -[uwsgi] -enabled = true -config_dir = /etc/uwsgi -nb_processes = 2 diff --git a/modoboa_installer/config_dict_template.py b/modoboa_installer/config_dict_template.py new file mode 100644 index 0000000..55a1319 --- /dev/null +++ b/modoboa_installer/config_dict_template.py @@ -0,0 +1,362 @@ +import random +import string + + +def make_password(length=16): + """Create a random password.""" + return "".join( + random.SystemRandom().choice( + string.ascii_letters + string.digits) for _ in range(length)) + + +# Validators should return a tuple bool, error message +def is_email(user_input): + """Return True in input is a valid email""" + return "@" in user_input, "Please enter a valid email" + + +ConfigDictTemplate = [ + { + "name": "general", + "values": [ + { + "option": "hostname", + "default": "mail.%(domain)s", + } + ] + }, + { + "name": "certificate", + "values": [ + { + "option": "generate", + "default": "true", + }, + { + "option": "type", + "default": "self-signed", + "customizable": True, + "question": "Please choose your certificate type", + "values": ["self-signed", "letsencrypt"], + } + ], + }, + { + "name": "letsencrypt", + "if": "certificate.type=letsencrypt", + "values": [ + { + "option": "email", + "default": "admin@example.com", + "question": ( + "Please enter the mail you wish to use for " + "letsencrypt"), + "customizable": True, + "validators": [is_email] + } + ] + }, + { + "name": "database", + "values": [ + { + "option": "engine", + "default": "postgres", + "customizable": True, + "question": "Please choose your database engine", + "values": ["postgres", "mysql"], + }, + { + "option": "host", + "default": "127.0.0.1", + }, + { + "option": "install", + "default": "true", + } + ] + }, + { + "name": "postgres", + "if": "database.engine=postgres", + "values": [ + { + "option": "user", + "default": "postgres", + }, + { + "option": "password", + "default": "", + "customizable": True, + "question": "Please enter postgres password", + }, + ] + }, + { + "name": "mysql", + "if": "database.engine=mysql", + "values": [ + { + "option": "user", + "default": "root", + }, + { + "option": "password", + "default": make_password, + "customizable": True, + "question": "Please enter mysql root password" + }, + { + "option": "charset", + "default": "utf8", + }, + { + "option": "collation", + "default": "utf8_general_ci", + } + ] + }, + { + "name": "modoboa", + "values": [ + { + "option": "user", + "default": "modoboa", + }, + { + "option": "home_dir", + "default": "/srv/modoboa", + }, + { + "option": "venv_path", + "default": "%(home_dir)s/instance", + }, + { + "option": "instance_path", + "default": "%(home_dir)s/env", + }, + { + "option": "timezone", + "default": "Europe/Paris", + }, + { + "option": "dbname", + "default": "modoboa", + }, + { + "option": "dbuser", + "default": "modoboa", + }, + { + "option": "dbpassword", + "default": make_password, + "customizable": True, + "question": "Please enter Modoboa db password", + }, + { + "option": "extensions", + "default": ( + "modoboa-amavis modoboa-pdfcredentials " + "modoboa-postfix-autoreply modoboa-sievefilters " + "modoboa-stats modoboa-webmail modoboa-contacts"), + }, + { + "option": "devmod", + "default": "false", + }, + ] + }, + { + "name": "automx", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "user", + "default": "automx", + }, + { + "option": "config_dir", + "default": "/etc", + }, + { + "option": "home_dir", + "default": "/srv/automx", + }, + { + "option": "venv_path", + "default": "%(home_dir)s/env", + }, + { + "option": "instance_path", + "default": "%(home_dir)s/instance", + }, + ] + }, + { + "name": "amavis", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "user", + "default": "amavis", + }, + { + "option": "max_servers", + "default": "1", + }, + { + "option": "dbname", + "default": "amavis", + }, + { + "option": "dbuser", + "default": "amavis", + }, + { + "option": "dbpassword", + "default": make_password, + "customizable": True, + "question": "Please enter amavis db password" + }, + ], + }, + { + "name": "clamav", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "user", + "default": "clamav", + }, + ] + }, + { + "name": "dovecot", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "config_dir", + "default": "/etc/dovecot", + }, + { + "option": "user", + "default": "vmail", + }, + { + "option": "home_dir", + "default": "/srv/vmail", + }, + { + "option": "mailboxes_owner", + "default": "vmail", + }, + { + "option": "extra_protocols", + "default": "", + }, + { + "option": "postmaster_address", + "default": "postmaster@%(domain)s", + }, + ] + }, + { + "name": "nginx", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "config_dir", + "default": "/etc/nginx", + }, + ], + }, + { + "name": "razor", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "config_dir", + "default": "/etc/razor", + }, + ] + }, + { + "name": "postfix", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "config_dir", + "default": "/etc/postfix", + }, + { + "option": "message_size_limit", + "default": "11534336", + }, + ] + }, + { + "name": "spamassassin", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "config_dir", + "default": "/etc/mail/spamassassin", + }, + { + "option": "dbname", + "default": "spamassassin", + }, + { + "option": "dbuser", + "default": "spamassassin", + }, + { + "option": "dbpassword", + "default": make_password, + "customizable": True, + "question": "Please enter spamassassin db password" + }, + ] + }, + { + "name": "uwsgi", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "config_dir", + "default": "/etc/uwsgi", + }, + { + "option": "nb_processes", + "default": "2", + }, + ] + }, +] diff --git a/modoboa_installer/utils.py b/modoboa_installer/utils.py index 8402a80..5368521 100644 --- a/modoboa_installer/utils.py +++ b/modoboa_installer/utils.py @@ -10,6 +10,12 @@ import shutil import string import subprocess import sys +try: + import configparser +except ImportError: + import ConfigParser as configparser + +from . import config_dict_template ENV = {} @@ -139,23 +145,14 @@ def copy_from_template(template, dest, context): fp.write(ConfigFileTemplate(buf).substitute(context)) -def check_config_file(dest): +def check_config_file(dest, interactive=False): """Create a new installer config file if needed.""" if os.path.exists(dest): return printcolor( "Configuration file {} not found, creating new one." .format(dest), YELLOW) - with open("installer.cfg.template") as fp: - buf = fp.read() - context = { - "mysql_password": make_password(), - "modoboa_password": make_password(), - "amavis_password": make_password(), - "sa_password": make_password() - } - with open(dest, "w") as fp: - fp.write(string.Template(buf).substitute(context)) + gen_config(dest, interactive) def has_colours(stream): @@ -222,3 +219,67 @@ def random_key(l=16): key = "".join(random.sample(population * l, l)) if len(key) == l: return key + + +def validate(value, config_entry): + if value is None: + return False + if "values" not in config_entry and "validators" not in config_entry: + return True + if "values" in config_entry: + try: + value = int(value) + except ValueError: + return False + return value >= 0 and value < len(config_entry["values"]) + if "validators" in config_entry: + for validator in config_entry["validators"]: + valide, message = validator(value) + if not valide: + printcolor(message, MAGENTA) + return False + return True + + +def get_entry_value(entry, interactive): + if callable(entry["default"]): + default_value = entry["default"]() + else: + default_value = entry["default"] + user_value = None + if entry.get("customizable") and interactive: + while (user_value != '' and not validate(user_value, entry)): + print(entry.get("question")) + if entry.get("values"): + print("Please choose from the list") + values = entry.get("values") + for index, value in enumerate(values): + print("{} {}".format(index, value)) + print("default is <{}>".format(default_value)) + user_value = user_input("->") + + if entry.get("values") and user_value != '': + user_value = values[int(user_value)] + return user_value if user_value else default_value + + +def gen_config(dest, interactive=False): + """Create config file from dict template""" + tpl_dict = config_dict_template.ConfigDictTemplate + config = configparser.ConfigParser() + # only ask about options we need, else still generate default + for section in tpl_dict: + if "if" in section: + config_key, value = section.get("if").split("=") + section_name, option = config_key.split(".") + interactive_section = ( + config.get(section_name, option) == value and interactive) + else: + interactive_section = interactive + config.add_section(section["name"]) + for config_entry in section["values"]: + value = get_entry_value(config_entry, interactive_section) + config.set(section["name"], config_entry["option"], value) + + with open(dest, "w") as configfile: + config.write(configfile) diff --git a/run.py b/run.py index 385829e..0e95ed5 100755 --- a/run.py +++ b/run.py @@ -31,6 +31,9 @@ def main(): parser.add_argument( "--stop-after-configfile-check", action="store_true", default=False, help="Check configuration, generate it if needed and exit") + parser.add_argument( + "--interactive", action="store_true", default=False, + help="Generate configuration file with user interaction") parser.add_argument("domain", type=str, help="The main domain of your future mail server") args = parser.parse_args() @@ -38,7 +41,7 @@ def main(): if args.debug: utils.ENV["debug"] = True utils.printcolor("Welcome to Modoboa installer!", utils.GREEN) - utils.check_config_file(args.configfile) + utils.check_config_file(args.configfile, args.interactive) if args.stop_after_configfile_check: return config = configparser.SafeConfigParser()