Add configuration wizard (#158)

* add --interactive option refs #133

* delete uneeded template as config is a dict now

* minor changes after code review
This commit is contained in:
Yohann Rebattu
2017-10-08 11:29:34 +02:00
committed by Antoine Nguyen
parent 45c777683c
commit 19ac9350d7
4 changed files with 438 additions and 115 deletions

View File

@@ -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

View File

@@ -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",
},
]
},
]

View File

@@ -10,6 +10,12 @@ import shutil
import string import string
import subprocess import subprocess
import sys import sys
try:
import configparser
except ImportError:
import ConfigParser as configparser
from . import config_dict_template
ENV = {} ENV = {}
@@ -139,23 +145,14 @@ def copy_from_template(template, dest, context):
fp.write(ConfigFileTemplate(buf).substitute(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.""" """Create a new installer config file if needed."""
if os.path.exists(dest): if os.path.exists(dest):
return return
printcolor( printcolor(
"Configuration file {} not found, creating new one." "Configuration file {} not found, creating new one."
.format(dest), YELLOW) .format(dest), YELLOW)
with open("installer.cfg.template") as fp: gen_config(dest, interactive)
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))
def has_colours(stream): def has_colours(stream):
@@ -222,3 +219,67 @@ def random_key(l=16):
key = "".join(random.sample(population * l, l)) key = "".join(random.sample(population * l, l))
if len(key) == l: if len(key) == l:
return key 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)

5
run.py
View File

@@ -31,6 +31,9 @@ def main():
parser.add_argument( parser.add_argument(
"--stop-after-configfile-check", action="store_true", default=False, "--stop-after-configfile-check", action="store_true", default=False,
help="Check configuration, generate it if needed and exit") 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, parser.add_argument("domain", type=str,
help="The main domain of your future mail server") help="The main domain of your future mail server")
args = parser.parse_args() args = parser.parse_args()
@@ -38,7 +41,7 @@ def main():
if args.debug: if args.debug:
utils.ENV["debug"] = True utils.ENV["debug"] = True
utils.printcolor("Welcome to Modoboa installer!", utils.GREEN) 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: if args.stop_after_configfile_check:
return return
config = configparser.SafeConfigParser() config = configparser.SafeConfigParser()