Files
bongo/contrib/testing/setup-wizard-defaults.py
T
2026-07-23 15:57:32 +02:00

188 lines
7.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Drive the real curses setup wizard without installing anything."""
from __future__ import annotations
import argparse
import curses
import json
import os
import re
import sys
from pathlib import Path
def source_root() -> Path:
return Path(os.environ.get(
"BONGO_SOURCE_ROOT", Path(__file__).resolve().parents[2])).resolve()
def temporary_root() -> Path:
root = Path(os.environ.get(
"BONGO_TEST_CONFIG_ROOT", "/tmp/bongo-setup-wizard-defaults"))
resolved = root.resolve()
if resolved == Path("/tmp") or Path("/tmp") not in resolved.parents:
raise SystemExit("BONGO_TEST_CONFIG_ROOT must be a child of /tmp")
return resolved
def run_wizard() -> None:
source = source_root()
root = temporary_root()
sys.path.insert(0, str(source / "src" / "libs" / "python"))
os.environ["BONGO_CONFIG_TEMPLATE_DIR"] = str(
source / "src" / "apps" / "config" / "config")
os.environ["BONGO_CONFIG_DOCUMENT_DIR"] = str(root / "etc/bongo/config.d")
os.environ["BONGO_ALIASES_CONFIG_DIR"] = str(root / "etc/bongo/aliases.d")
os.environ["BONGO_SSL_DIR"] = str(root / "etc/bongo/ssl.d")
os.environ["BONGO_DKIM_DIR"] = str(root / "etc/bongo/dkim.d")
os.environ["BONGO_DMARC_DIR"] = str(root / "etc/bongo/dmarc.d")
os.environ["BONGO_ACME_CONFIG_DIR"] = str(root / "etc/bongo/acme.d")
os.environ["BONGO_ACME_PROVIDER_CONFIG"] = str(
root / "etc/bongo/acme.d/providers")
from bongo.configuration import tui # pylint: disable=import-outside-toplevel
from bongo.configuration.model import errors # pylint: disable=import-outside-toplevel
from bongo.configuration.probes import ProbeResult # pylint: disable=import-outside-toplevel
from bongo.configuration.storage import ConfigurationStore # pylint: disable=import-outside-toplevel
from bongo.domain import domain_to_ascii # pylint: disable=import-outside-toplevel
def mail_dns(domains, _selector):
return {
domain_to_ascii(domain): {
"spf": [], "dkim": [], "dmarc": [], "errors": []
}
for domain in domains
}
tui.inspect_mail_dns = mail_dns
tui.probe_clamav = lambda: ProbeResult(
False, "127.0.0.1:3310", "not installed", False)
tui.probe_spamd = lambda: ProbeResult(
False, "127.0.0.1:783", "not installed", False)
store = ConfigurationStore()
scenario, configurations, plans = curses.wrapper(tui.setup_wizard, store)
issues = store.validate(configurations, check_files=False)
result = {
"hostname": scenario.hostname,
"bind_address": scenario.bind_address,
"domains": scenario.domains,
"tls_hostnames": scenario.tls_hostnames,
"web_mode": scenario.web_mode,
"web_public_url": scenario.web_public_url,
"web_proxy_networks": scenario.web_proxy_networks,
"imap_enabled": scenario.imap_enabled,
"pop3_enabled": scenario.pop3_enabled,
"sieve_enabled": scenario.sieve_enabled,
"submission_enabled": scenario.submission_enabled,
"internal_relay_enabled": scenario.internal_relay_enabled,
"ldap_enabled": scenario.ldap_enabled,
"antispam_enabled": scenario.antispam_enabled,
"antivirus_enabled": scenario.antivirus_enabled,
"acme_enabled": scenario.acme_enabled,
"scanner_plan_count": len(plans),
"validation_errors": [str(issue) for issue in errors(issues)],
}
print("BONGO_WIZARD_RESULT=" + json.dumps(result, sort_keys=True))
def drive_wizard(password: str) -> None:
try:
import pexpect
except ImportError as error:
raise SystemExit("setup-wizard-defaults.py requires pexpect") from error
child = pexpect.spawn(
sys.executable, [str(Path(__file__).resolve()), "--wizard"],
env={**os.environ, "TERM": "xterm-256color"},
encoding="utf-8", timeout=30)
child.setwinsize(24, 100)
transcript: list[str] = []
def answer(prompt: str, value: str = "", replace: bool = False) -> None:
child.expect(prompt)
transcript.append(child.before + child.after)
if replace:
child.send("\x7f" * 255)
child.send(value + "\r")
answer("Bongo development setup")
answer("Primary mail hostname", "mail.example.test", True)
answer("IPv4 address Bongo listens on", "192.0.2.10", True)
answer("Hosted domains, separated by commas", "example.test", True)
answer("DNS hostnames used by mail and Web clients")
answer("Enable automatic ACME certificate issuance")
answer("How is Bongo Web exposed")
answer("Public base URL")
answer("Trusted proxy IPv4 networks")
answer("behind HAProxy")
answer("Enable IMAP4/IMAP4rev1/rev2")
answer(r"POP3\?")
answer(r"filtering\?")
answer(r"submission\?")
answer(r"old clients\?")
answer("DKIM selector")
answer("Public outbound IPv4")
answer("missing SPF, DKIM, DMARC")
answer("Private key for bongo._domainkey.example.test")
answer("Enable the rate-limited port 26 relay")
answer("Use LDAP/Active Directory")
answer("Optional scanners")
answer("Initial admin password", password)
answer("Repeat the admin password", password)
answer("Install now")
child.expect(r"BONGO_WIZARD_RESULT=(.*)\r?\n")
result = json.loads(child.match.group(1))
child.expect(pexpect.EOF)
expected = {
"hostname": "mail.example.test",
"bind_address": "192.0.2.10",
"domains": ["example.test"],
"tls_hostnames": ["mail.example.test"],
"web_mode": "apache",
"web_public_url": "https://mail.example.test",
"web_proxy_networks": ["127.0.0.1/32"],
"imap_enabled": True,
"pop3_enabled": True,
"sieve_enabled": True,
"submission_enabled": True,
"internal_relay_enabled": False,
"ldap_enabled": False,
"antispam_enabled": False,
"antivirus_enabled": False,
"acme_enabled": False,
"scanner_plan_count": 0,
"validation_errors": [],
}
if result != expected:
raise SystemExit("wizard defaults differ:\nexpected="
+ json.dumps(expected, sort_keys=True)
+ "\nactual=" + json.dumps(result, sort_keys=True))
plain = re.sub(
r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))", "",
"".join(transcript))
if "WeYesdirect" in plain or "MaNo proxy" in plain:
raise SystemExit("detected an overlapped choice label")
print("PASS setup wizard defaults and narrow-terminal layout")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--wizard", action="store_true",
help=argparse.SUPPRESS)
arguments = parser.parse_args()
if arguments.wizard:
run_wizard()
return 0
password = os.environ.get("BONGO_TEST_PASSWORD", "")
if len(password) < 12:
raise SystemExit("set a disposable BONGO_TEST_PASSWORD (12+ characters)")
drive_wizard(password)
return 0
if __name__ == "__main__":
raise SystemExit(main())