""" Middleware service generators. - VNC, Nginx, HAProxy, Keepalived, Redis, RabbitMQ, Tomcat, Docker, ETCD, Zookeeper """ from . import register, Generator, Field from . import bash_header, quote, bool_str, yes, port_list # ========================== VNC Server ========================== class VNCSrv(Generator): id = "vncserver" title = "VNC 远程桌面 (TigerVNC + 多桌面)" category = "middleware" icon = "🖥️" tags = ["vnc", "desktop", "xfce", "gnome", "tigervnc", "kde", "mate"] description = "在 Ubuntu/Debian 上一键安装并启动 TigerVNC,支持 XFCE / GNOME / KDE / MATE / LXQt 多种桌面环境。" warnings = [ "VNC 协议默认不加密;生产环境请配合 SSH 隧道 (-localhost yes) 或 VPN 使用。", "VNC 密码仅前 8 字节有效 (DES 限制),建议用强密码但勿超过 8 位核心。", "GNOME 桌面会显著增大磁盘占用 (~2-3 GB),启动比 XFCE 慢。", "KDE 桌面需要 sddm/lightdm,无显示器服务器上仅启动 VNC 会跳过 DM 阶段。", ] post_steps = [ "systemctl status vncserver@1 — 查看服务状态", "ss -ltn | grep 5901 — 验证监听端口", "用 VNC Viewer 连接 :5901 (5900 + display 号)", ] verify_steps = [ "ss -ltn | grep -E ':5901|:5902'", "systemctl is-active vncserver@1", "tail -f /var/log/Xvnc.1.log 或 journalctl -u vncserver@1 -f", ] os_support = ["ubuntu", "debian"] fields = [ Field("user", "运行用户", "text", default="root", placeholder="root", help="VNC 进程以该用户运行,推荐使用普通用户,root 登录需要修改配置。"), Field("display", "Display 号", "number", default="1", min_=1, max_=99, help="Display 1 = 端口 5901,Display 2 = 端口 5902。"), Field("password", "VNC 密码", "password", default="", required=False, help="留空则使用 vncpassword 自动生成;只取前 8 位。"), Field("desktop_env", "桌面环境", "select", default="xfce", options=["xfce", "gnome", "kde-plasma", "mate", "lxqt"], help="选 gnome = Ubuntu 原生 GNOME / GNOME Shell;" " 选 kde-plasma = KDE Plasma 5;" " 选 mate = MATE (XFCE 轻量替代);" " 选 lxqt = LXQt (超轻量);" " 选 xfce = XFCE (最稳定)。"), Field("geometry", "分辨率", "select", default="1920x1080", options=["1280x720", "1366x768", "1440x900", "1600x900", "1680x1050", "1920x1080", "2560x1440", "3840x2160"]), Field("depth", "颜色深度", "select", default="24", options=["16", "24", "32"]), Field("localhost", "仅本地监听", "checkbox", default="no", help="yes = 仅 localhost 监听,需 SSH 隧道;no = 0.0.0.0 监听(配合防火墙)。"), Field("nolisten_unix", "禁用 Unix socket (-nolisten unix)", "checkbox", default="yes", help="强烈建议启用 — 物理桌面环境(GDM/Xorg)已经在 /tmp/.X11-unix/X0 占着," "Xvnc 默认想创建 X1 会被 reject;开启此选项让 Xvnc 只 listen TCP 5901。"), Field("nolisten_tcp_local", "禁用 X11 TCP listener (6001)", "checkbox", default="no", help="如 6001 端口已被 GDM/Xorg 占,开启此选项避免冲突。" "对 VNC 连接无影响,只影响原生 X11 客户端直连能力。"), Field("use_xvfb", "使用 Xvfb (无显卡)", "checkbox", default="yes", help="Headless 服务器必须启用 — VNC 通过 Xvfb 渲染,无需物理显卡。"), Field("disable_wayland", "禁用 GDM Wayland (强制 X11)", "checkbox", default="yes", help="强烈建议启用 — Ubuntu 22.04 默认用 Wayland + GDM,但 Wayland 下 x11vnc " "会出现键盘映射异常、剪贴板双向不通、gnome-shell 偶发闪退等问题。" "此选项写 /etc/gdm3/custom.conf 把 WaylandEnable 设为 false,所有 GDM session " "(包括物理登录和锁屏)都强制走 X11。注意:在有活动的物理桌面登录时会断开一次。"), Field("restart_gdm", "立即重启 GDM 应用 Wayland 更改", "checkbox", default="no", help="yes = 立刻 systemctl restart gdm 让 Wayland 禁用生效(会断开当前物理会话);" "no = 仅改配置,下次重启后生效。生产机器建议 no,自己本地实验可以 yes。"), ] # ---- per-DE helpers (chosen at render time) ---- DE_PACKAGES = { # Ubuntu / Debian apt package names "xfce": ["xfce4", "xfce4-goodies"], "gnome": ["ubuntu-desktop-minimal"], # Ubuntu — fallback to gnome-session below "kde-plasma": ["kde-plasma-desktop"], "mate": ["mate-desktop", "mate-desktop-environment"], "lxqt": ["lxqt", "sddm"], } DE_PACKAGES_FALLBACK = { # Distro-agnostic fallback (for non-Ubuntu Debian or when minimal not present) "gnome": ["gnome-session", "gnome-shell", "gnome-terminal", "nautilus", "metacity"], "kde-plasma": ["plasma-desktop", "sddm"], "mate": ["mate-session-manager", "marco", "mate-panel", "mate-terminal", "caja"], "lxqt": ["lxqt-session", "openbox", "pcmanfm-qt", "sddm"], } DE_XSTARTUP = { # The xstartup block (bash heredoc body) for each DE. # dbus-launch is needed for all to make SystemDBus/DBus_SESSION work. "xfce": ( "unset SESSION_MANAGER\n" "unset DBUS_SESSION_BUS_ADDRESS\n" "exec dbus-launch --exit-with-session startxfce4\n" ), "gnome": ( "unset SESSION_MANAGER\n" "unset DBUS_SESSION_BUS_ADDRESS\n" # Ubuntu-flavored GNOME. The colon-separated form # `ubuntu:GNOME` is the canonical value Ubuntu ships in # /usr/share/gnome-session/sessions/ — using just `GNOME` # confuses the session chooser and some apps (gnome-terminal, # nautilus) refuse to launch. "export XDG_CURRENT_DESKTOP=ubuntu:GNOME\n" # Tell Mutter / gnome-shell we're an X11 session, not # Wayland. x11vnc only sees X, so this is correct. "export XDG_SESSION_TYPE=x11\n" # Ubuntu's patched gnome-shell reads this to enable the # Ubuntu-specific hot-corner + dock tweaks. "export GNOME_SHELL_SESSION_MODE=ubuntu\n" "exec dbus-launch --exit-with-session gnome-session\n" ), "kde-plasma": ( "unset SESSION_MANAGER\n" "unset DBUS_SESSION_BUS_ADDRESS\n" "export XDG_CURRENT_DESKTOP=KDE\n" "exec dbus-launch --exit-with-session startplasma-x11\n" ), "mate": ( "unset SESSION_MANAGER\n" "unset DBUS_SESSION_BUS_ADDRESS\n" "exec dbus-launch --exit-with-session mate-session\n" ), "lxqt": ( "unset SESSION_MANAGER\n" "unset DBUS_SESSION_BUS_ADDRESS\n" "export XDG_CURRENT_DESKTOP=LXQt\n" "exec dbus-launch --exit-with-session startlxqt\n" ), } def render(self, p): user = p.get("user", "root") or "root" display = str(p.get("display", "1")) password = p.get("password", "") or "" de = p.get("desktop_env", "xfce") geometry = p.get("geometry", "1920x1080") depth = p.get("depth", "24") localhost = "yes" if bool_str(p.get("localhost")) else "no" nolisten_unix = bool_str(p.get("nolisten_unix", True)) nolisten_tcp_local = bool_str(p.get("nolisten_tcp_local")) use_xvfb = bool_str(p.get("use_xvfb", True)) disable_wayland = bool_str(p.get("disable_wayland", True)) restart_gdm = bool_str(p.get("restart_gdm")) if de not in self.DE_PACKAGES: raise ValueError(f"Unknown desktop_env: {de}. Choose one of: {', '.join(self.DE_PACKAGES)}") pkgs = self.DE_PACKAGES[de] pkgs_fb = self.DE_PACKAGES_FALLBACK.get(de, []) xstartup = self.DE_XSTARTUP[de] out = [bash_header(self.title)] out.append(f'log "Installing VNC + {de} desktop for user {quote(user)} on display :{display}"') out.append('log "Updating apt cache..."') out.append('apt-get update') out.append(f'log "Installing {de} desktop + TigerVNC..."') # Build install command. Try distro-specific first, then generic. # We install xvfb + x11vnc + tigervnc-* (xfce stays as primary DE). # xvfb is the virtual framebuffer X server (used by xvfb-run); # x11vnc is the VNC server that attaches to the Xvfb display and # exposes it via RFB. tigervnc-standalone-server provides the # passwd / vncpasswd utilities used in the main script. install = ( 'DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n' ' ' + ' '.join(pkgs) + ' \\\n' ' dbus-x11 tigervnc-standalone-server tigervnc-common tigervnc-xorg-extension \\\n' ' xvfb x11vnc x11-utils 2>/dev/null' ) if pkgs_fb: install += ' || \\\nDEBIAN_FRONTEND=noninteractive apt-get install -y \\\n' install += ' ' + ' '.join(pkgs_fb) + ' \\\n' install += ' dbus-x11 tigervnc-standalone-server tigervnc-common \\\n' install += ' xvfb x11vnc x11-utils' # xvfb is now always installed above. The use_xvfb flag is kept # for backward compatibility but no longer changes the install list. if use_xvfb and de in ("gnome", "kde-plasma"): # Some compositing DEs need a fake display backend driver install += ' \\\n xserver-xorg-video-dummy' # GNOME needs several extra packages on top of ubuntu-desktop-minimal # to render a usable desktop over VNC: # - gnome-shell-extension-desktop-icons-ng: icons on the desktop # - gnome-terminal: a working terminal app # - nautilus-extension-gnome-terminal: opens Terminal in nautilus # right-click menu # - gnome-tweaks: advanced settings UI # These are missing from -minimal and would otherwise leave the user # with a blank desktop and no way to launch apps. if de == "gnome": install += (' \\\n gnome-shell-extension-desktop-icons-ng' ' gnome-terminal nautilus-extension-gnome-terminal' ' gnome-tweaks') out.append(install) # GDM Wayland disable — only relevant for GNOME on a machine that # has a physical GDM (i.e. ubuntu-desktop-01, not a headless server). # Ubuntu 22.04 defaults to WaylandEnable=true in /etc/gdm3/custom.conf. # Wayland + x11vnc has known issues: keyboard remapping gets # confused, clipboard isn't bidirectional, gnome-shell occasionally # crashes on X client events. Forcing X11 makes the VNC session # behave like a normal X11 desktop. if de == "gnome" and disable_wayland: out.append('log "Disabling GDM Wayland (forcing X11 session)..."') # custom.conf may not exist on a server (no gdm3 installed) — guard out.append('if [ -d /etc/gdm3 ] && [ -f /etc/gdm3/custom.conf ] || [ ! -f /etc/gdm3/custom.conf ]; then') out.append(' mkdir -p /etc/gdm3') out.append(' if [ -f /etc/gdm3/custom.conf ]; then') # Replace any existing WaylandEnable line, else add under [daemon] out.append(' if grep -q "^[[:space:]]*WaylandEnable" /etc/gdm3/custom.conf; then') out.append(' sed -i "s/^[[:space:]]*WaylandEnable.*/WaylandEnable=false/" /etc/gdm3/custom.conf') out.append(' else') out.append(' sed -i "/^[[:space:]]*\\[daemon\\]/a WaylandEnable=false" /etc/gdm3/custom.conf') out.append(' fi') out.append(' else') # No custom.conf yet — write a minimal one out.append(' cat > /etc/gdm3/custom.conf <&1 | head -5 || warn "gdm restart failed (no gdm installed?)"') else: out.append('log "Wayland disabled in custom.conf; will take effect after next gdm restart or reboot."') out.append('log "Preparing VNC directory..."') out.append(f'USER_HOME="$(getent passwd {quote(user)} | cut -d: -f6 || echo /root)"') # If getent returned /root for a non-root user, the account is in a # broken state (often created without -m, or the row got mangled). # Treat this as "use /home/" and recreate the home properly. out.append(f'if [ "$USER_HOME" = "/root" ] && [ "{user}" != "root" ]; then') out.append(f' warn "getent passwd {user} returned home=/root; assuming /home/{user}"') out.append(f' USER_HOME="/home/{user}"') out.append('fi') # Make sure the user + their home exist and are owned by them. Without # this, systemd fails with "status=200/CHDIR Permission denied" at the # WorkingDirectory=%h step. Two failure modes we guard against: # 1. The user was never created (no /home/ at all) # 2. /home/ was created with wrong owner (root, mode 755) and # the VNC user can't write into it out.append(f'log "User {quote(user)} home: $USER_HOME"') out.append('# Ensure user exists (idempotent — useradd exits 9 if user exists)') out.append('# Force login shell = /bin/bash so systemd User= can run xstartup') out.append('id "' + user + '" >/dev/null 2>&1 || useradd -m -d "$USER_HOME" -s /bin/bash ' + user) # If user existed but home was missing or wrong (e.g. /root for cnbug), # fix it now: usermod -m moves /root's contents to the new home. out.append('CURRENT_HOME="$(getent passwd "' + user + '" | cut -d: -f6)"') out.append('if [ "$CURRENT_HOME" != "$USER_HOME" ]; then') out.append(' warn "Fixing home for user ' + user + ': $CURRENT_HOME -> $USER_HOME"') out.append(' usermod -d "$USER_HOME" -m "' + user + '" 2>/dev/null || true') out.append('fi') # CRITICAL: also fix the login shell — system users (created with # useradd -r) often have /usr/sbin/nologin which breaks systemd User= out.append('CURRENT_SHELL="$(getent passwd "' + user + '" | cut -d: -f7)"') out.append('if [ "$CURRENT_SHELL" != "/bin/bash" ]; then') out.append(' warn "Fixing shell for user ' + user + ': $CURRENT_SHELL -> /bin/bash"') out.append(' usermod -s /bin/bash "' + user + '"') out.append('fi') # Make /home traversable (drwxr-xr-x). Some hardening tools tighten # /home to 750 which would block cnbugs from reaching /home/cnbugs # even if /home/cnbugs itself is owned by cnbugs. if user != "root": out.append('[ -d /home ] && chmod 755 /home || true') # Re-chown the home in case it was created by some other tool with # wrong ownership. install -d = mkdir + chown in one call. out.append('install -d -o "' + user + '" -g "' + user + '" -m 0750 "$USER_HOME"') out.append('install -d -o "' + user + '" -g "' + user + '" -m 0700 "$USER_HOME/.vnc"') out.append('') # Password if password: out.append('log "Setting VNC password..."') out.append(f'PASS={quote(password)}') out.append('printf "%s\\n" "$PASS" | vncpasswd -f > "$USER_HOME/.vnc/passwd"') out.append('chmod 600 "$USER_HOME/.vnc/passwd"') # passwd was written by root (we're running as root); chown to the # VNC user so the service User= can read it. out.append('chown "' + user + '":"' + user + '" "$USER_HOME/.vnc/passwd"') else: out.append('log "Skipping password (none provided). Run: su - $USER -c vncpasswd"') # Password file was already created in the user-owned .vnc dir above out.append('') # xstartup out.append(f'log "Writing xstartup ({de} via dbus-launch)..."') out.append('cat > "$USER_HOME/.vnc/xstartup" <<\'XSTARTUP_EOF\'\n' '#!/bin/sh\n' + xstartup + 'XSTARTUP_EOF') # .vnc dir was created user-owned; xstartup needs +x but stays user-owned out.append('chmod +x "$USER_HOME/.vnc/xstartup"') out.append('') # We need the actual home path at unit-write time, not at service # start time. systemd units do NOT support bash variable expansion # in WorkingDirectory=, and %h (which would expand at service-start # to the User='s NSS-resolved home) can be inconsistent after a # usermod -d in the same session. We write the literal absolute # path that was just resolved and verified to work above. out.append('') out.append('log "Writing systemd unit vncserver@.service..."') # Use vncserver -fg directly. TigerVNC 1.12+ supports -fg # (foreground mode) which is required for systemd Type=simple. # vncserver automatically reads ~/.vnc/xstartup for the desktop # session and ~/.vnc/passwd for authentication. localhost_flag = ' -localhost' if bool_str(localhost) else ' -localhost=0' out.append('cat > /etc/systemd/system/vncserver@.service <<\'UNIT_EOF\'\n' '[Unit]\n' 'Description=TigerVNC server on display :%i (' + de + ')\n' 'After=syslog.target network.target\n\n' '[Service]\n' 'Type=simple\n' f'User={user}\n' f'Group={user}\n' 'WorkingDirectory=__VNC_HOME__\n' 'ExecStartPre=-/usr/bin/vncserver -kill :%i\n' 'ExecStartPre=-/bin/sh -c \'for f in /tmp/.X%i-lock /tmp/.X11-unix/X%i; do [ -e "$f" ] && rm -f "$f"; done; exit 0\'\n' 'ExecStart=/usr/bin/vncserver -fg :%i' ' -geometry ' + geometry + ' -depth ' + depth + localhost_flag + '\n' 'ExecStop=/usr/bin/vncserver -kill :%i\n\n' '[Install]\n' 'WantedBy=multi-user.target\n' 'UNIT_EOF') out.append('# Substitute the placeholder with the actual home path') out.append('sed -i "s|__VNC_HOME__|$USER_HOME|" /etc/systemd/system/vncserver@.service') out.append('log " WorkingDirectory: $(grep ^WorkingDirectory= /etc/systemd/system/vncserver@.service)"') out.append('log " Unit written. Verifying WorkingDirectory..."') # Check if the requested display is already in use by another X server. # Physical desktops (GNOME/Xwayland) often occupy :0 or :1. out.append('# Check if display :${display} is already in use') out.append('if ss -xlpn 2>/dev/null | grep -q "/tmp/.X11-unix/X' + display + ' "; then') out.append(' EXISTING="$(ss -xlpn 2>/dev/null | grep "/tmp/.X11-unix/X' + display + ' " | head -1)"') out.append(' warn "Display :' + display + ' is already in use by another X server:"') out.append(' warn " $EXISTING"') out.append(' warn "This machine likely has a physical desktop on :' + display + '."') out.append(' warn "Please use a higher display number (e.g. :3 or :10)."') out.append(' die "Display :' + display + ' is occupied. Aborting."') out.append('fi') # CRITICAL: Clean up stale X11 lock/socket from previous failed starts. # systemd runs ExecStartPre under User=, which means the user # can't rm files owned by root. Do it here from the main script. out.append('log "Cleaning up stale /tmp/.X11-unix sockets (as root)..."') out.append('rm -f /tmp/.X' + display + '-lock /tmp/.X11-unix/X' + display + ' 2>/dev/null || true') out.append('pkill -f "Xvnc.*:' + display + ' " 2>/dev/null || true') out.append('pkill -f "Xtigervnc.*:' + display + '" 2>/dev/null || true') out.append('systemctl daemon-reload') out.append(f'systemctl enable vncserver@{display}.service') out.append(f'systemctl restart vncserver@{display}.service') out.append('sleep 2') out.append(f'systemctl --no-pager status vncserver@{display}.service || true') out.append('') port = str(5900 + int(display)) out.append('log "Verifying port..."') out.append(f'ss -ltn | grep ":{port}" || warn "Port {port} not listening yet, give it 5s."') out.append('log "Done. Connect with VNC Viewer to :' + port + ' (desktop: ' + de + ')"') return "\n".join(out) + "\n" # ========================== Nginx ========================== class Nginx(Generator): id = "nginx" title = "Nginx 静态/反向代理服务器" category = "middleware" icon = "🌍" tags = ["nginx", "web", "proxy"] description = "一键安装 Nginx,可生成 vhost 配置 (HTTP/HTTPS) 并签发自签名证书。" warnings = ["自签证书浏览器会告警,生产请使用 Let's Encrypt (certbot)。"] post_steps = [ "systemctl status nginx", "curl -I http://127.0.0.1 — 应返回 200", "tail -f /var/log/nginx/access.log", ] fields = [ Field("server_name", "Server Name", "text", default="example.local", placeholder="example.com", required=True, help="HTTP Server 块中的 server_name。"), Field("listen_port", "监听端口", "number", default="80", min_=1, max_=65535), Field("root_path", "网站根目录", "text", default="/var/www/html", help="静态文件根目录,留空使用默认。"), Field("enable_ssl", "启用 HTTPS", "checkbox", default="no", help="启用后会在 443 监听并生成自签名证书。"), Field("ssl_port", "HTTPS 端口", "number", default="443", min_=1, max_=65535), Field("enable_proxy", "启用反向代理", "checkbox", default="no"), Field("proxy_pass", "Proxy Pass 地址", "text", default="http://127.0.0.1:8080", placeholder="http://127.0.0.1:8080", help="启用反向代理时生效,转发的上游地址。"), Field("client_max_body_size", "上传大小限制", "text", default="50m", help="client_max_body_size 值,如 50m / 200m。"), Field("worker_processes", "Worker 数", "text", default="auto", help="auto = CPU 核数,或指定数字。"), ] def render(self, p): sn = p.get("server_name", "example.local") port = str(p.get("listen_port", "80")) root = p.get("root_path", "/var/www/html") ssl = bool_str(p.get("enable_ssl")) ssl_port = str(p.get("ssl_port", "443")) proxy = bool_str(p.get("enable_proxy")) proxy_pass = p.get("proxy_pass", "http://127.0.0.1:8080") cmbs = p.get("client_max_body_size", "50m") wp = p.get("worker_processes", "auto") out = [bash_header(self.title)] out.append('log "Installing nginx..."') out.append('PKG_INSTALL nginx openssl curl') out.append('mkdir -p /etc/nginx/conf.d /var/www/html /etc/nginx/ssl') out.append('') # main nginx.conf (only override the worker_processes line if not 'auto') if wp != "auto": out.append(f'sed -i "s/^worker_processes .*/worker_processes {wp};/" /etc/nginx/nginx.conf') out.append('') out.append('log "Writing site config..."') out.append(f'mkdir -p {quote(root)}') out.append(f'cat > {quote(root)}/index.html <\n' f'{sn}\n' f'

Hello from shell-gen

Server: {sn}

\n' f'

Generated: {__import__("datetime").datetime.now():%Y-%m-%d %H:%M:%S}

\n' '\n' 'HTML_EOF') out.append('') # Server block server_block = [] server_block.append(f' listen {port};\n listen [::]:{port};\n' f' server_name {sn};\n root {root};\n' f' client_max_body_size {cmbs};\n' ' index index.html;\n\n' ' access_log /var/log/nginx/' + sn + '.access.log;\n' ' error_log /var/log/nginx/' + sn + '.error.log;\n') if proxy: server_block.append(' location / {\n' f' proxy_pass {proxy_pass};\n' ' proxy_set_header Host $host;\n' ' proxy_set_header X-Real-IP $remote_addr;\n' ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n' ' proxy_set_header X-Forwarded-Proto $scheme;\n' ' }\n') else: server_block.append(' location / {\n' ' try_files $uri $uri/ =404;\n' ' }\n') if ssl: server_block.append('\n # Redirect all plain HTTP to HTTPS\n' f' if ($scheme != "https") {{ return 301 https://$host$request_uri; }}\n') siteconf = f"server {{\n{''.join(server_block)}}}\n" out.append(f'cat > /etc/nginx/conf.d/{sn}.conf </dev/null') out.append(f'cat > /etc/nginx/conf.d/{sn}-ssl.conf < /etc/haproxy/haproxy.cfg </dev/null || systemctl restart haproxy') out.append('log "Done. Stats: http://:' + stats_port + '/stats"') return "\n".join(out) + "\n" # ========================== Keepalived ========================== class Keepalived(Generator): id = "keepalived" title = "Keepalived 高可用 VIP" category = "middleware" icon = "🔁" tags = ["keepalived", "vrrp", "ha", "vip"] description = "部署 Keepalived + VRRP 双机热备,自动漂移 VIP。" warnings = [ "Keepalived 需要至少 2 台同网段机器组成集群。", "multicast 在云厂商可能被禁用,需使用 unicast peer。", ] fields = [ Field("state", "本节点角色", "select", default="MASTER", options=["MASTER", "BACKUP"]), Field("interface", "网卡", "text", default="eth0", help="VRRP 绑定的网卡,可用 ip a 查询。"), Field("virtual_router_id", "Virtual Router ID", "number", default="51", min_=1, max_=255), Field("priority", "优先级 (MASTER > BACKUP)", "number", default="100", min_=1, max_=254), Field("virtual_ip", "VIP 地址 (CIDR)", "text", default="192.168.1.100/24", placeholder="192.168.1.100/24"), Field("auth_pass", "认证密码", "password", default="changeme"), Field("unicast_peer_ip", "对端 IP (unicast 模式)", "text", default="", help="云环境用单播,留空则用多播;填对端 IP 即可。"), Field("notify_script", "状态切换脚本 (可选)", "text", default="", help="MASTER->BACKUP 或反向时执行的脚本路径。"), Field("check_script", "健康检查脚本 (可选)", "text", default="", help="如: /etc/keepalived/check_nginx.sh,return 0 = 健康。"), Field("check_interval", "检查间隔", "text", default="2", help="weight 下降间隔,单位秒。"), ] def render(self, p): state = p.get("state", "MASTER") iface = p.get("interface", "eth0") vrid = str(p.get("virtual_router_id", "51")) prio = str(p.get("priority", "100")) vip = p.get("virtual_ip", "192.168.1.100/24") auth = p.get("auth_pass", "changeme") peer = p.get("unicast_peer_ip", "").strip() notify = p.get("notify_script", "").strip() check = p.get("check_script", "").strip() interval = p.get("check_interval", "2") out = [bash_header(self.title)] out.append('log "Installing keepalived..."') out.append('PKG_INSTALL keepalived') out.append('') out.append('cat > /etc/keepalived/keepalived.conf <> /etc/keepalived/keepalived.conf < /etc/redis/redis.conf < /etc/redis/users.acl <{acl_pwd} ~* &* +@all\n' 'ACL_EOF') out.append('echo "aclfile /etc/redis/users.acl" >> /etc/redis/redis.conf') out.append('systemctl enable --now redis-server || systemctl enable --now redis') out.append('sleep 1') out.append('systemctl --no-pager status redis-server || systemctl --no-pager status redis || true') out.append('redis-cli -h 127.0.0.1 -p ' + port + ' PING || warn "redis-cli ping failed"') return "\n".join(out) + "\n" # ========================== RabbitMQ ========================== class RabbitMQ(Generator): id = "rabbitmq" title = "RabbitMQ 消息队列" category = "middleware" icon = "🐰" tags = ["rabbitmq", "mq", "amqp"] description = "安装 RabbitMQ,启用 management 插件,创建 admin 用户。" fields = [ Field("admin_user", "Admin 用户", "text", default="admin"), Field("admin_password", "Admin 密码", "password", default="changeme123"), Field("listeners", "AMQP 监听端口", "number", default="5672", min_=1, max_=65535), Field("mgmt_port", "Management 端口", "number", default="15672", min_=1, max_=65535), Field("cluster_name", "节点名", "text", default="rabbit@localhost"), ] def render(self, p): admin = p.get("admin_user", "admin") pwd = p.get("admin_password", "changeme123") port = str(p.get("listeners", "5672")) mgmt = str(p.get("mgmt_port", "15672")) out = [bash_header(self.title)] out.append('log "Installing rabbitmq..."') # repo install (covers all distros; apt and yum both have rabbitmq-server package) out.append('case "$PKG" in\n' ' apt-get) DEBIAN_FRONTEND=noninteractive apt-get install -y rabbitmq-server ;;\n' ' yum) yum install -y epel-release && yum install -y rabbitmq-server ;;\n' ' dnf) dnf install -y rabbitmq-server ;;\n' 'esac') out.append('systemctl enable --now rabbitmq-server') out.append('rabbitmq-plugins enable rabbitmq_management') out.append('systemctl restart rabbitmq-server') out.append('sleep 3') out.append('log "Creating admin user..."') out.append(f'rabbitmqctl add_user {admin} {pwd} || rabbitmqctl change_password {admin} {pwd}') out.append(f'rabbitmqctl set_user_tags {admin} administrator') out.append(f'rabbitmqctl set_permissions -p / {admin} ".*" ".*" ".*"') out.append('log "Management UI: http://:' + mgmt + ' (user: ' + admin + ')"') return "\n".join(out) + "\n" # ========================== Tomcat ========================== class Tomcat(Generator): id = "tomcat" title = "Apache Tomcat (Java Servlet 容器)" category = "middleware" icon = "🐱" tags = ["tomcat", "java", "servlet", "jsp"] description = "下载并安装 Apache Tomcat,自动配置 systemd。" fields = [ Field("version", "Tomcat 版本", "select", default="10.1", options=["10.1", "10.0", "9.0", "8.5"]), Field("port", "HTTP 端口", "number", default="8080", min_=1, max_=65535), Field("shutdown_port", "Shutdown 端口", "number", default="8005", min_=1, max_=65535), Field("ajp_port", "AJP 端口", "number", default="8009", min_=1, max_=65535), Field("java_opts", "JAVA_OPTS", "text", default="-Xms512m -Xmx1024m -Djava.awt.headless=true"), Field("user", "运行用户", "text", default="tomcat"), Field("install_dir", "安装目录", "text", default="/opt/tomcat"), ] def render(self, p): ver = p.get("version", "10.1") port = str(p.get("port", "8080")) s_port = str(p.get("shutdown_port", "8005")) ajp = str(p.get("ajp_port", "8009")) java_opts = p.get("java_opts", "-Xms512m -Xmx1024m -Djava.awt.headless=true") user = p.get("user", "tomcat") install_dir = p.get("install_dir", "/opt/tomcat") # Pick the latest patch for the chosen major url = "https://dlcdn.apache.org/tomcat/tomcat-" + ver.split(".")[0] + ("/v" + ver + "/bin/apache-tomcat-" + ver + ".tar.gz" if ver != "10.1" else "/v10.1.x/bin/apache-tomcat-10.1.x.tar.gz") out = [bash_header(self.title)] out.append('log "Installing Java + Tomcat..."') out.append('command -v java >/dev/null || PKG_INSTALL java-11-openjdk-devel java-11-openjdk || PKG_INSTALL default-jdk') out.append('useradd -r -s /bin/false ' + user + ' 2>/dev/null || true') out.append('cd /opt') out.append('curl -fsSL -o tomcat.tar.gz "' + url + '"') out.append('tar -xzf tomcat.tar.gz') out.append('rm -rf ' + install_dir) out.append('mv apache-tomcat-* ' + install_dir) out.append('rm -f tomcat.tar.gz') out.append('chown -R ' + user + ':' + user + ' ' + install_dir) out.append('') out.append('log "Patching server.xml ports..."') out.append('sed -i "s/port=\"8080\"/port=\"' + port + '\"/" ' + install_dir + '/conf/server.xml') out.append('sed -i "s/port=\"8005\"/port=\"' + s_port + '\"/" ' + install_dir + '/conf/server.xml') out.append('sed -i "s/port=\"8009\"/port=\"' + ajp + '\"/" ' + install_dir + '/conf/server.xml') out.append('') out.append('cat > /etc/systemd/system/tomcat.service < /etc/apt/sources.list.d/docker.list\n' ' apt-get update\n' ' PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n' ' ;;\n' ' yum|dnf)\n' ' PKG_INSTALL yum-utils\n' ' yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo || dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n' ' PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n' ' ;;\n' 'esac') out.append('systemctl enable --now docker') out.append('') out.append('mkdir -p /etc/docker') out.append('cat > /etc/docker/daemon.json </dev/null || true') out.append('mkdir -p ' + data + ' /etc/etcd') out.append('cd /tmp') out.append('curl -fsSL -o etcd.tar.gz https://github.com/etcd-io/etcd/releases/download/v' + ver + '/etcd-v' + ver + '-linux-amd64.tar.gz') out.append('tar -xzf etcd.tar.gz') out.append('cp etcd-v' + ver + '-linux-amd64/etcd /usr/local/bin/') out.append('cp etcd-v' + ver + '-linux-amd64/etcdctl /usr/local/bin/') out.append('rm -rf etcd.tar.gz etcd-v' + ver + '-linux-amd64') out.append('chown -R etcd:etcd ' + data + ' /etc/etcd') out.append('') out.append('cat > /etc/etcd/etcd.conf < /etc/systemd/system/etcd.service </dev/null || PKG_INSTALL java-11-openjdk-devel java-11-openjdk || PKG_INSTALL default-jdk') out.append('useradd -r -s /bin/false zookeeper 2>/dev/null || true') out.append('cd /opt') out.append('curl -fsSL -o zk.tar.gz https://archive.apache.org/dist/zookeeper/zookeeper-' + ver + '/apache-zookeeper-' + ver + '-bin.tar.gz') out.append('rm -rf /opt/zookeeper') out.append('tar -xzf zk.tar.gz') out.append('mv apache-zookeeper-' + ver + '-bin /opt/zookeeper') out.append('rm -f zk.tar.gz') out.append('mkdir -p ' + data) out.append('chown -R zookeeper:zookeeper /opt/zookeeper ' + data) out.append('') out.append('cat > /opt/zookeeper/conf/zoo.cfg < ' + data + '/myid') out.append('') out.append('cat > /etc/systemd/system/zookeeper.service <