#!/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())