Initial commit: 50 shell-script generators with web UI
- 8 categories / 50 generators covering middleware, databases, runtimes, systemd services, system tools, network, monitoring, security - VNC supports XFCE / GNOME / KDE Plasma / MATE / LXQt desktops - Node.js versions 18-26 (incl. current LTS 24 Krypton and current 26) - Live preview, copy / download / multi-script bundle in web UI - Distro-aware (Ubuntu/Debian/CentOS/RHEL/Rocky/Alma/Fedora) - All 50 generators pass bash -n syntax check - Zero pip dependencies (Flask stdlib only)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Generator registry — every generator module imports @register and a Generator class.
|
||||
The class is responsible for rendering a shell script given a params dict.
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
import shlex
|
||||
|
||||
_REGISTRY: Dict[str, "Generator"] = {}
|
||||
|
||||
|
||||
def register(gen: "Generator"):
|
||||
if gen.id in _REGISTRY:
|
||||
raise ValueError(f"Duplicate generator id: {gen.id}")
|
||||
_REGISTRY[gen.id] = gen
|
||||
return gen
|
||||
|
||||
|
||||
def get_generator(gid: str) -> Optional["Generator"]:
|
||||
return _REGISTRY.get(gid)
|
||||
|
||||
|
||||
def list_generators() -> Dict[str, "Generator"]:
|
||||
return dict(_REGISTRY)
|
||||
|
||||
|
||||
def get_categories() -> List[str]:
|
||||
seen = []
|
||||
for g in _REGISTRY.values():
|
||||
if g.category not in seen:
|
||||
seen.append(g.category)
|
||||
return seen
|
||||
|
||||
|
||||
# Human-readable labels and FontAwesome-style emoji icons
|
||||
category_labels = {
|
||||
"middleware": "中间件服务",
|
||||
"databases": "数据库",
|
||||
"runtimes": "开发语言运行时",
|
||||
"systemd": "Systemd 服务",
|
||||
"system": "系统工具",
|
||||
"network": "网络工具",
|
||||
"monitoring": "监控告警",
|
||||
"security": "安全加固",
|
||||
}
|
||||
|
||||
category_icons = {
|
||||
"middleware": "🧩",
|
||||
"databases": "🗄️",
|
||||
"runtimes": "⚙️",
|
||||
"systemd": "🛠️",
|
||||
"system": "🖥️",
|
||||
"network": "🌐",
|
||||
"monitoring": "📊",
|
||||
"security": "🔒",
|
||||
}
|
||||
|
||||
|
||||
# ----- Field / Param descriptors for the UI -----
|
||||
|
||||
class Field:
|
||||
"""One form field rendered in the web UI."""
|
||||
|
||||
def __init__(self, name, label, type="text", default="", placeholder="",
|
||||
help="", options=None, required=False, min_=None, max_=None,
|
||||
step=None, pattern=None, group=None):
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.type = type # text|number|select|checkbox|textarea|password|portlist
|
||||
self.default = default
|
||||
self.placeholder = placeholder
|
||||
self.help = help
|
||||
self.options = options or []
|
||||
self.required = required
|
||||
self.min = min_
|
||||
self.max = max_
|
||||
self.step = step
|
||||
self.pattern = pattern
|
||||
self.group = group # for grouping in UI
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"name": self.name, "label": self.label, "type": self.type,
|
||||
"default": self.default, "placeholder": self.placeholder,
|
||||
"help": self.help, "options": self.options, "required": self.required,
|
||||
"min": self.min, "max": self.max, "step": self.step, "pattern": self.pattern,
|
||||
"group": self.group,
|
||||
}
|
||||
|
||||
|
||||
# ----- Generator base class -----
|
||||
|
||||
class Generator:
|
||||
id: str = ""
|
||||
title: str = ""
|
||||
category: str = "middleware"
|
||||
description: str = ""
|
||||
icon: str = "📦"
|
||||
tags: List[str] = []
|
||||
fields: List[Field] = []
|
||||
warnings: List[str] = []
|
||||
post_steps: List[str] = []
|
||||
verify_steps: List[str] = []
|
||||
os_support: List[str] = ["ubuntu", "debian", "centos", "rhel"]
|
||||
dangerous: bool = False
|
||||
|
||||
def render(self, params: dict) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def summary(self) -> dict:
|
||||
return {
|
||||
"id": self.id, "title": self.title, "category": self.category,
|
||||
"description": self.description, "icon": self.icon, "tags": self.tags,
|
||||
"os": self.os_support, "dangerous": self.dangerous,
|
||||
"fields": [f.to_dict() for f in self.fields],
|
||||
}
|
||||
|
||||
|
||||
# ----------------------------- Helpers -----------------------------
|
||||
|
||||
BASH_HEADER = """#!/usr/bin/env bash
|
||||
# Generated by shell-gen (https://github.com/yourname/shell-gen)
|
||||
# Generator: {title}
|
||||
# Generated at: {ts}
|
||||
# OS: {os_hint}
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${{BASH_SOURCE[0]}}")" &>/dev/null && pwd)"
|
||||
LOG_PREFIX="[{title}]"
|
||||
|
||||
log() {{ echo "${{LOG_PREFIX}} $*" ; }}
|
||||
warn() {{ echo "${{LOG_PREFIX}} WARN: $*" >&2 ; }}
|
||||
die() {{ echo "${{LOG_PREFIX}} ERROR: $*" >&2 ; exit 1 ; }}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "This script must be run as root. Re-run with sudo."
|
||||
|
||||
# ---- detect distro ----
|
||||
. /etc/os-release 2>/dev/null || true
|
||||
DISTRO_ID="${{ID:-unknown}}"
|
||||
DISTRO_LIKE="${{ID_LIKE:-}}"
|
||||
case "$DISTRO_ID" in
|
||||
ubuntu|debian) PKG="apt-get"; PKG_INSTALL="DEBIAN_FRONTEND=noninteractive apt-get install -y" ;;
|
||||
centos|rhel|rocky|almalinux|ol) PKG="yum"; PKG_INSTALL="yum install -y" ;;
|
||||
fedora) PKG="dnf"; PKG_INSTALL="dnf install -y" ;;
|
||||
*)
|
||||
if echo "$DISTRO_LIKE" | grep -qiE "rhel|centos|fedora"; then
|
||||
PKG="yum"; PKG_INSTALL="yum install -y"
|
||||
elif echo "$DISTRO_LIKE" | grep -qi "debian"; then
|
||||
PKG="apt-get"; PKG_INSTALL="DEBIAN_FRONTEND=noninteractive apt-get install -y"
|
||||
else
|
||||
die "Unsupported distribution: $DISTRO_ID"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
log "Detected distro: $DISTRO_ID (package manager: $PKG)"
|
||||
"""
|
||||
|
||||
|
||||
def bash_header(title: str) -> str:
|
||||
return BASH_HEADER.format(
|
||||
title=title,
|
||||
ts=datetime_now_str(),
|
||||
os_hint="Ubuntu / Debian / CentOS / RHEL / Rocky / Alma",
|
||||
)
|
||||
|
||||
|
||||
def datetime_now_str() -> str:
|
||||
import datetime as _dt
|
||||
return _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def quote(value) -> str:
|
||||
"""Bash-safe single-quote escape."""
|
||||
if value is None:
|
||||
return "''"
|
||||
s = str(value)
|
||||
if s == "":
|
||||
return "''"
|
||||
return "'" + s.replace("'", "'\\''") + "'"
|
||||
|
||||
|
||||
def bool_str(value, default=False) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if value is None:
|
||||
return default
|
||||
return str(value).lower() in ("1", "true", "yes", "on", "y", "t")
|
||||
|
||||
|
||||
def yes(value) -> str:
|
||||
return "yes" if bool_str(value) else "no"
|
||||
|
||||
|
||||
def port_list(value, default=None) -> str:
|
||||
"""Normalize port list input: '80,443' or '80 443' -> '80 443'."""
|
||||
if value is None or value == "":
|
||||
value = default or ""
|
||||
s = str(value).replace(",", " ").replace(";", " ")
|
||||
parts = [p.strip() for p in s.split() if p.strip()]
|
||||
return " ".join(parts)
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Extra databases (Memcached, SQLite, Elasticsearch, ClickHouse).
|
||||
"""
|
||||
from . import register, Generator, Field
|
||||
from . import bash_header, quote, bool_str, yes
|
||||
|
||||
|
||||
# ========================== Memcached ==========================
|
||||
class Memcached(Generator):
|
||||
id = "memcached"
|
||||
title = "Memcached"
|
||||
category = "databases"
|
||||
icon = "🧠"
|
||||
tags = ["memcached", "cache"]
|
||||
description = "安装 memcached 并配置监听地址/内存限制/连接数。"
|
||||
fields = [
|
||||
Field("port", "端口", "number", default="11211", min_=1, max_=65535),
|
||||
Field("memory_mb", "最大内存 (MB)", "number", default="256"),
|
||||
Field("max_connections", "最大连接数", "number", default="1024"),
|
||||
Field("listen", "监听地址", "text", default="0.0.0.0"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
port = str(p.get("port", "11211"))
|
||||
mem = str(p.get("memory_mb", "256"))
|
||||
conn = str(p.get("max_connections", "1024"))
|
||||
listen = p.get("listen", "0.0.0.0")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing memcached..."')
|
||||
out.append('$PKG_INSTALL memcached')
|
||||
out.append('cp /etc/memcached.conf /etc/memcached.conf.bak.$(date +%s) || true')
|
||||
out.append('sed -i "s/^-m .*/-m ' + mem + '/" /etc/memcached.conf')
|
||||
out.append('sed -i "s/^-p .*/-p ' + port + '/" /etc/memcached.conf')
|
||||
out.append('sed -i "s/^-l .*/-l ' + listen + '/" /etc/memcached.conf')
|
||||
out.append('sed -i "s/^-c .*/-c ' + conn + '/" /etc/memcached.conf')
|
||||
out.append('systemctl enable --now memcached')
|
||||
out.append('echo "stats" | nc -q1 127.0.0.1 ' + port + ' | head -5')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== SQLite3 (system tools, no server) ==========================
|
||||
class SqliteTools(Generator):
|
||||
id = "sqlite"
|
||||
title = "SQLite 命令行工具"
|
||||
category = "databases"
|
||||
icon = "📁"
|
||||
tags = ["sqlite"]
|
||||
description = "安装最新版 sqlite3 + sqlite-utils。"
|
||||
fields = [
|
||||
Field("install_python_bindings", "安装 Python sqlite-utils", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
pyb = bool_str(p.get("install_python_bindings", True))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing sqlite3..."')
|
||||
out.append('$PKG_INSTALL sqlite3 libsqlite3-dev || $PKG_INSTALL sqlite sqlite-devel')
|
||||
if pyb:
|
||||
out.append('command -v pip3 >/dev/null || $PKG_INSTALL python3-pip')
|
||||
out.append('pip3 install --break-system-packages sqlite-utils || pip3 install sqlite-utils')
|
||||
out.append('sqlite3 --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Elasticsearch ==========================
|
||||
class Elasticsearch(Generator):
|
||||
id = "elasticsearch"
|
||||
title = "Elasticsearch"
|
||||
category = "databases"
|
||||
icon = "🔍"
|
||||
tags = ["elasticsearch", "es", "search"]
|
||||
description = "安装 Elasticsearch (单节点开发模式)。"
|
||||
warnings = [
|
||||
"ES JVM 默认 1G,小内存机器请调小 -Xms -Xmx。",
|
||||
"生产环境必须设置 xpack.security.enabled=true。",
|
||||
]
|
||||
fields = [
|
||||
Field("version", "版本", "select", default="8.13.4",
|
||||
options=["7.17.20", "8.11.4", "8.13.4", "8.14.3"]),
|
||||
Field("port", "HTTP 端口", "number", default="9200", min_=1, max_=65535),
|
||||
Field("jvm_heap", "JVM 堆", "text", default="1g"),
|
||||
Field("discovery", "discovery.type", "select", default="single-node",
|
||||
options=["single-node"]),
|
||||
Field("enable_security", "启用 xpack security", "checkbox", default="no"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "8.13.4")
|
||||
port = str(p.get("port", "9200"))
|
||||
heap = p.get("jvm_heap", "1g")
|
||||
sec = bool_str(p.get("enable_security"))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Elasticsearch ' + ver + '..."')
|
||||
out.append('useradd -r -s /bin/false elasticsearch 2>/dev/null || true')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-' + ver + '-linux-x86_64.tar.gz')
|
||||
out.append('rm -rf /opt/elasticsearch')
|
||||
out.append('tar -xzf elasticsearch-' + ver + '-linux-x86_64.tar.gz -C /opt/')
|
||||
out.append('mv /opt/elasticsearch-' + ver + ' /opt/elasticsearch')
|
||||
out.append('rm -f elasticsearch-' + ver + '-linux-x86_64.tar.gz')
|
||||
out.append('chown -R elasticsearch:elasticsearch /opt/elasticsearch')
|
||||
out.append('sed -i "s/^-Xms.*/-Xms' + heap + '/" /opt/elasticsearch/config/jvm.options')
|
||||
out.append('sed -i "s/^-Xmx.*/-Xmx' + heap + '/" /opt/elasticsearch/config/jvm.options')
|
||||
out.append('sed -i "s/^#http.port.*/http.port: ' + port + '/" /opt/elasticsearch/config/elasticsearch.yml')
|
||||
out.append('echo "discovery.type: single-node" >> /opt/elasticsearch/config/elasticsearch.yml')
|
||||
out.append('echo "network.host: 0.0.0.0" >> /opt/elasticsearch/config/elasticsearch.yml')
|
||||
out.append('echo "xpack.security.enabled: ' + ('true' if sec else 'false') + '" >> /opt/elasticsearch/config/elasticsearch.yml')
|
||||
out.append('cat > /etc/systemd/system/elasticsearch.service <<UNIT_EOF\n'
|
||||
'[Unit]\nDescription=Elasticsearch\nAfter=network.target\n\n'
|
||||
'[Service]\nType=simple\n'
|
||||
'User=elasticsearch\nGroup=elasticsearch\n'
|
||||
'LimitNOFILE=65536\n'
|
||||
'Environment=ES_HOME=/opt/elasticsearch\n'
|
||||
'Environment=ES_PATH_CONF=/opt/elasticsearch/config\n'
|
||||
'ExecStart=/opt/elasticsearch/bin/elasticsearch\n'
|
||||
'Restart=on-failure\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now elasticsearch')
|
||||
out.append('log "Wait ~30s then: curl http://127.0.0.1:' + port + '"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== ClickHouse ==========================
|
||||
class ClickHouse(Generator):
|
||||
id = "clickhouse"
|
||||
title = "ClickHouse"
|
||||
category = "databases"
|
||||
icon = "🏛️"
|
||||
tags = ["clickhouse", "olap", "columnar"]
|
||||
description = "安装 ClickHouse 服务端 + 客户端。"
|
||||
fields = [
|
||||
Field("tcp_port", "TCP 端口", "number", default="9000"),
|
||||
Field("http_port", "HTTP 端口", "number", default="8123"),
|
||||
Field("listen_host", "Listen host", "text", default="0.0.0.0"),
|
||||
Field("data_path", "数据目录", "text", default="/var/lib/clickhouse"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
tcp = str(p.get("tcp_port", "9000"))
|
||||
http = str(p.get("http_port", "8123"))
|
||||
lh = p.get("listen_host", "0.0.0.0")
|
||||
dp = p.get("data_path", "/var/lib/clickhouse")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing ClickHouse..."')
|
||||
out.append('$PKG_INSTALL apt-transport-https ca-certificates curl || $PKG_INSTALL ca-certificates curl')
|
||||
out.append('mkdir -p /etc/apt/keyrings')
|
||||
out.append('curl -fsSL https://clickhouse.com/keys/clickhouse.asc | gpg --dearmor -o /etc/apt/keyrings/clickhouse.gpg')
|
||||
out.append('echo "deb [signed-by=/etc/apt/keyrings/clickhouse.gpg] https://packages.clickhouse.com/deb stable main" > /etc/apt/sources.list.d/clickhouse.list')
|
||||
out.append('apt-get update')
|
||||
out.append('DEBIAN_FRONTEND=noninteractive apt-get install -y clickhouse-server clickhouse-client')
|
||||
out.append('mkdir -p ' + dp)
|
||||
out.append('chown -R clickhouse:clickhouse ' + dp)
|
||||
out.append('sed -i "s|<tcp_port>.*</tcp_port>|<tcp_port>' + tcp + '</tcp_port>|" /etc/clickhouse-server/config.xml')
|
||||
out.append('sed -i "s|<http_port>.*</http_port>|<http_port>' + http + '</http_port>|" /etc/clickhouse-server/config.xml')
|
||||
out.append('sed -i "s|<listen_host>.*</listen_host>|<listen_host>' + lh + '</listen_host>|" /etc/clickhouse-server/config.xml')
|
||||
out.append('sed -i "s|<path>.*</path>|<path>' + dp + '</path>|" /etc/clickhouse-server/config.xml')
|
||||
out.append('systemctl enable --now clickhouse-server')
|
||||
out.append('sleep 3')
|
||||
out.append('clickhouse-client -q "SELECT version()"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Consul (service mesh) ==========================
|
||||
class Consul(Generator):
|
||||
id = "consul"
|
||||
title = "HashiCorp Consul"
|
||||
category = "middleware"
|
||||
icon = "🌐"
|
||||
tags = ["consul", "service-discovery"]
|
||||
description = "安装 Consul 单节点开发模式。"
|
||||
fields = [
|
||||
Field("version", "Consul 版本", "text", default="1.18.1"),
|
||||
Field("datacenter", "Datacenter", "text", default="dc1"),
|
||||
Field("ui", "启用 Web UI", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "1.18.1")
|
||||
dc = p.get("datacenter", "dc1")
|
||||
ui = bool_str(p.get("ui", True))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('useradd -r -s /bin/false consul 2>/dev/null || true')
|
||||
out.append('mkdir -p /etc/consul.d /var/lib/consul')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://releases.hashicorp.com/consul/' + ver + '/consul_' + ver + '_linux_amd64.zip')
|
||||
out.append('unzip -o consul_' + ver + '_linux_amd64.zip -d /usr/local/bin/')
|
||||
out.append('rm -f consul_' + ver + '_linux_amd64.zip')
|
||||
out.append('chown -R consul:consul /etc/consul.d /var/lib/consul')
|
||||
out.append('cat > /etc/consul.d/consul.hcl <<CFG_EOF\n'
|
||||
'datacenter = "' + dc + '"\n'
|
||||
'data_dir = "/var/lib/consul"\n'
|
||||
'client_addr = "0.0.0.0"\n'
|
||||
'ui_config { enabled = ' + ('true' if ui else 'false') + ' }\n'
|
||||
'server = true\n'
|
||||
'bootstrap_expect = 1\n'
|
||||
'CFG_EOF')
|
||||
out.append('cat > /etc/systemd/system/consul.service <<UNIT_EOF\n'
|
||||
'[Unit]\nDescription=HashiCorp Consul\nAfter=network.target\n\n'
|
||||
'[Service]\nType=simple\nUser=consul\nGroup=consul\n'
|
||||
'ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d/\n'
|
||||
'Restart=on-failure\nLimitNOFILE=65536\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now consul')
|
||||
out.append('sleep 3')
|
||||
out.append('consul members || true')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
for _g in [Memcached, SqliteTools, Elasticsearch, ClickHouse, Consul]:
|
||||
register(_g())
|
||||
@@ -0,0 +1,984 @@
|
||||
"""
|
||||
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 连接 <server-ip>: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("use_xvfb", "使用 Xvfb (无显卡)", "checkbox", default="yes",
|
||||
help="Headless 服务器必须启用 — VNC 通过 Xvfb 渲染,无需物理显卡。"),
|
||||
]
|
||||
|
||||
# ---- 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"
|
||||
# GNOME requires XDG_CURRENT_DESKTOP and a dbus session
|
||||
"export XDG_CURRENT_DESKTOP=GNOME\n"
|
||||
"export XDG_SESSION_TYPE=x11\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"
|
||||
use_xvfb = bool_str(p.get("use_xvfb", True))
|
||||
|
||||
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.
|
||||
install = (
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n'
|
||||
' ' + ' '.join(pkgs) + ' \\\n'
|
||||
' dbus-x11 tigervnc-standalone-server tigervnc-common tigervnc-xorg-extension 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'
|
||||
if use_xvfb and de in ("gnome", "kde-plasma"):
|
||||
# Some compositing DEs need a fake display backend
|
||||
install += ' \\\n xserver-xorg-video-dummy xvfb'
|
||||
out.append(install)
|
||||
|
||||
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/<user>" 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/<user> at all)
|
||||
# 2. /home/<user> 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('HOME_ABS="' + '$USER_HOME' + '"')
|
||||
out.append('')
|
||||
# systemd unit — substitute HOME_ABS for the actual absolute path now
|
||||
# so the rendered WorkingDirectory is a literal (e.g. /home/cnbugs)
|
||||
working_dir_literal = '$USER_HOME' # bash expands at script runtime
|
||||
out.append('log "Writing systemd unit vncserver@.service..."')
|
||||
# Write to a .in template, then sed-replace $USER_HOME with the
|
||||
# resolved absolute path so the final .service has a literal path.
|
||||
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'
|
||||
# Use the literal absolute home path. The template below
|
||||
# gets sed-replaced with the actual $USER_HOME at write
|
||||
# time so the unit file holds a literal path, not a
|
||||
# variable. This avoids the "WorkingDirectory= path is
|
||||
# not absolute" error from systemd and any %h
|
||||
# inconsistency.
|
||||
'WorkingDirectory=__VNC_HOME__\n'
|
||||
# Best-effort cleanup. Main script also does this as root
|
||||
# (where rm -f actually works on root-owned files). The
|
||||
# leading "-" tells systemd to ignore non-zero exit.
|
||||
'ExecStartPre=-/bin/sh -c \'rm -f /tmp/.X%i-lock /tmp/.X11-unix/X%i 2>/dev/null; /usr/bin/vncserver -kill :%i >/dev/null 2>&1 || true\'\n'
|
||||
f'ExecStart=/usr/bin/vncserver -fg -localhost {localhost} :%i \\\n'
|
||||
f' -geometry {geometry} -depth {depth}\n'
|
||||
'ExecStop=/usr/bin/vncserver -kill :%i\n\n'
|
||||
'[Install]\n'
|
||||
'WantedBy=multi-user.target\n'
|
||||
'UNIT_EOF')
|
||||
# Replace the placeholder with the literal absolute home path.
|
||||
out.append('# Substitute the placeholder with the actual home path we resolved above')
|
||||
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)"')
|
||||
# CRITICAL: Clean up stale X11 lock/socket from previous failed starts.
|
||||
# systemd runs ExecStartPre under User=<user>, which means cnbugs can't
|
||||
# rm -f files owned by root. Do it here from the main script (running
|
||||
# as root) so the cleanup actually takes effect.
|
||||
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 <server>:' + 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 <<HTML_EOF\n'
|
||||
'<!doctype html><html><head><meta charset="utf-8">\n'
|
||||
f'<title>{sn}</title></head><body>\n'
|
||||
f'<h1>Hello from shell-gen</h1><p>Server: {sn}</p>\n'
|
||||
f'<p>Generated: {__import__("datetime").datetime.now():%Y-%m-%d %H:%M:%S}</p>\n'
|
||||
'</body></html>\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 <<CONF_EOF\n{siteconf}CONF_EOF')
|
||||
out.append('rm -f /etc/nginx/sites-enabled/default')
|
||||
if ssl:
|
||||
out.append('log "Generating self-signed certificate..."')
|
||||
out.append(f'openssl req -x509 -nodes -newkey rsa:2048 -days 365 \\\n'
|
||||
f' -keyout /etc/nginx/ssl/{sn}.key \\\n'
|
||||
f' -out /etc/nginx/ssl/{sn}.crt \\\n'
|
||||
f' -subj "/CN={sn}" 2>/dev/null')
|
||||
out.append(f'cat > /etc/nginx/conf.d/{sn}-ssl.conf <<CONF_EOF\n'
|
||||
f'server {{\n'
|
||||
f' listen {ssl_port} ssl;\n'
|
||||
f' listen [::]:{ssl_port} ssl;\n'
|
||||
f' server_name {sn};\n'
|
||||
f' ssl_certificate /etc/nginx/ssl/{sn}.crt;\n'
|
||||
f' ssl_certificate_key /etc/nginx/ssl/{sn}.key;\n'
|
||||
f' root {root};\n'
|
||||
f' client_max_body_size {cmbs};\n'
|
||||
f' index index.html;\n'
|
||||
f' access_log /var/log/nginx/{sn}-ssl.access.log;\n'
|
||||
f' error_log /var/log/nginx/{sn}-ssl.error.log;\n'
|
||||
+ (' 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' if proxy else
|
||||
' location / {\n'
|
||||
' try_files $uri $uri/ =404;\n'
|
||||
' }\n')
|
||||
+ '}}\n'
|
||||
'CONF_EOF')
|
||||
out.append('log "Testing nginx config..."')
|
||||
out.append('nginx -t')
|
||||
out.append('systemctl enable --now nginx')
|
||||
out.append('systemctl reload nginx || systemctl restart nginx')
|
||||
out.append('log "Done. Listening on :' + port + (", :"+ssl_port+" (HTTPS)" if ssl else "") + '"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== HAProxy ==========================
|
||||
class HAProxy(Generator):
|
||||
id = "haproxy"
|
||||
title = "HAProxy 负载均衡器"
|
||||
category = "middleware"
|
||||
icon = "⚖️"
|
||||
tags = ["haproxy", "loadbalancer", "l4", "l7"]
|
||||
description = "安装 HAProxy 并生成前端/后端配置,支持 TCP / HTTP 模式、健康检查、stats 面板。"
|
||||
fields = [
|
||||
Field("haproxy_name", "HAProxy 实例名", "text", default="haproxy"),
|
||||
Field("frontend_name", "Frontend 名称", "text", default="http-in"),
|
||||
Field("frontend_port", "Frontend 端口", "number", default="80", min_=1, max_=65535),
|
||||
Field("mode", "运行模式", "select", default="http",
|
||||
options=["http", "tcp"]),
|
||||
Field("backend_name", "Backend 名称", "text", default="app-servers"),
|
||||
Field("balance_method", "负载算法", "select", default="roundrobin",
|
||||
options=["roundrobin", "leastconn", "source", "uri", "url_param",
|
||||
"hdr", "random", "first", "static-rr"]),
|
||||
Field("backends", "后端服务器列表", "textarea", default="192.168.1.10:8080\n192.168.1.11:8080",
|
||||
help="一行一个,格式: <ip>:<port>。"),
|
||||
Field("stats_port", "Stats 端口", "number", default="8404", min_=1, max_=65535),
|
||||
Field("stats_user", "Stats 用户", "text", default="admin"),
|
||||
Field("stats_password", "Stats 密码", "password", default="changeme"),
|
||||
Field("enable_https", "启用 HTTPS 前端", "checkbox", default="no"),
|
||||
Field("https_port", "HTTPS 端口", "number", default="443"),
|
||||
Field("cert_path", "SSL 证书路径(PEM)", "text", default="/etc/haproxy/cert.pem",
|
||||
help="PEM 格式,含 cert+key 拼接,或 cert+单独 key。"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
frontend_port = str(p.get("frontend_port", "80"))
|
||||
mode = p.get("mode", "http")
|
||||
backend_name = p.get("backend_name", "app-servers")
|
||||
balance = p.get("balance_method", "roundrobin")
|
||||
backends = p.get("backends", "").strip().splitlines()
|
||||
backends = [b.strip() for b in backends if b.strip()]
|
||||
stats_port = str(p.get("stats_port", "8404"))
|
||||
stats_user = p.get("stats_user", "admin")
|
||||
stats_pwd = p.get("stats_password", "changeme")
|
||||
https = bool_str(p.get("enable_https"))
|
||||
https_port = str(p.get("https_port", "443"))
|
||||
cert_path = p.get("cert_path", "/etc/haproxy/cert.pem")
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing haproxy..."')
|
||||
out.append('$PKG_INSTALL haproxy')
|
||||
out.append('cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak.$(date +%s) || true')
|
||||
out.append('mkdir -p /etc/haproxy')
|
||||
out.append('')
|
||||
cfg = []
|
||||
cfg.append('global\n log /dev/log local0\n log /dev/log local1 notice\n'
|
||||
' chroot /var/lib/haproxy\n stats timeout 30s\n user haproxy\n group haproxy\n'
|
||||
' daemon\n maxconn 4096\n\n')
|
||||
cfg.append('defaults\n log global\n mode ' + mode + '\n'
|
||||
' option httplog\n option dontlognull\n'
|
||||
' timeout connect 10s\n timeout client 60s\n'
|
||||
' timeout server 60s\n\n')
|
||||
cfg.append('frontend stats\n bind *:' + stats_port + '\n mode http\n'
|
||||
' stats enable\n stats uri /stats\n stats refresh 10s\n'
|
||||
f' stats auth {stats_user}:{stats_pwd}\n'
|
||||
' stats admin if TRUE\n\n')
|
||||
cfg.append(f'frontend {p.get("frontend_name","http-in")}\n'
|
||||
f' bind *:{frontend_port}\n'
|
||||
f' mode {mode}\n'
|
||||
f' default_backend {backend_name}\n\n')
|
||||
if https:
|
||||
cfg.append(f'frontend {p.get("frontend_name","http-in")}-ssl\n'
|
||||
f' bind *:{https_port} ssl crt {cert_path}\n'
|
||||
f' mode {mode}\n'
|
||||
f' default_backend {backend_name}\n\n')
|
||||
cfg.append(f'backend {backend_name}\n balance {balance}\n'
|
||||
' option httpchk GET /healthz\n'
|
||||
' http-check expect status 200\n')
|
||||
for b in backends:
|
||||
cfg.append(f' server {b.replace(".", "_").replace(":", "_")} {b} check inter 3s fall 3 rise 2\n')
|
||||
out.append('cat > /etc/haproxy/haproxy.cfg <<CFG_EOF\n' + ''.join(cfg) + 'CFG_EOF')
|
||||
out.append('haproxy -c -f /etc/haproxy/haproxy.cfg')
|
||||
out.append('systemctl enable --now haproxy')
|
||||
out.append('systemctl reload haproxy 2>/dev/null || systemctl restart haproxy')
|
||||
out.append('log "Done. Stats: http://<server>:' + 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 <<CONF_EOF\n'
|
||||
'global_defs {\n'
|
||||
' enable_script_security\n'
|
||||
' script_user root\n'
|
||||
'}\n\n'
|
||||
'vrrp_script check_local {\n' +
|
||||
(f' script "{check}"\n' if check else
|
||||
' script "exit 0"\n') +
|
||||
f' interval {interval}\n'
|
||||
' weight -50\n'
|
||||
' fall 3\n rise 2\n'
|
||||
'}\n\n'
|
||||
'vrrp_instance VI_1 {\n'
|
||||
f' state {state}\n'
|
||||
f' interface {iface}\n'
|
||||
f' virtual_router_id {vrid}\n'
|
||||
f' priority {prio}\n'
|
||||
' advert_int 1\n'
|
||||
' authentication {\n'
|
||||
f' auth_type PASS\n auth_pass {auth}\n'
|
||||
' }\n'
|
||||
' virtual_ipaddress {\n'
|
||||
f' {vip}\n'
|
||||
' }\n'
|
||||
' track_script {\n'
|
||||
' check_local\n'
|
||||
' }\n' +
|
||||
(f' notify "{notify}"\n' if notify else '') +
|
||||
'}\nCONF_EOF')
|
||||
if peer:
|
||||
out.append('log "Patching unicast_peer_ip..."')
|
||||
out.append('cat >> /etc/keepalived/keepalived.conf <<CONF_EOF\n\n'
|
||||
'unicast_src_ip "$(ip -4 addr show ' + iface + ' | grep -oP "(?<=inet\\s)\\d+(\\.\\d+){3}")"\n'
|
||||
'unicast_peer {\n'
|
||||
f' {peer}\n'
|
||||
'}\n'
|
||||
'CONF_EOF')
|
||||
out.append('sed -i \'s/vrrp_instance/, &unicast_src_ip_check\\n&/\' /etc/keepalived/keepalived.conf || true')
|
||||
out.append('systemctl enable --now keepalived')
|
||||
out.append('log "Verifying VIP..."')
|
||||
out.append('sleep 2')
|
||||
out.append('ip a | grep -A1 "' + iface + ':" | grep "' + vip.split('/')[0] + '" || warn "VIP not on this node (expected if BACKUP)."')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Redis ==========================
|
||||
class Redis(Generator):
|
||||
id = "redis"
|
||||
title = "Redis 内存数据库"
|
||||
category = "databases"
|
||||
icon = "🔴"
|
||||
tags = ["redis", "nosql", "cache"]
|
||||
description = "安装 Redis 并启用密码、AOF 持久化、systemd 服务。"
|
||||
fields = [
|
||||
Field("bind", "Bind 地址", "text", default="0.0.0.0"),
|
||||
Field("port", "端口", "number", default="6379", min_=1, max_=65535),
|
||||
Field("requirepass", "密码 (留空=不设)", "password", default=""),
|
||||
Field("maxmemory", "最大内存", "text", default="256mb",
|
||||
help="如 256mb / 1gb / 2gb"),
|
||||
Field("maxmemory_policy", "淘汰策略", "select", default="allkeys-lru",
|
||||
options=["noeviction", "allkeys-lru", "allkeys-lfu",
|
||||
"volatile-lru", "volatile-lfu", "volatile-random",
|
||||
"allkeys-random"]),
|
||||
Field("appendonly", "启用 AOF", "checkbox", default="yes",
|
||||
help="yes = 更安全;no = 仅 RDB 快照。"),
|
||||
Field("enable_acl", "启用 ACL (6+)", "checkbox", default="no",
|
||||
help="6.0+ 推荐,使用 ACL 替代 requirepass。"),
|
||||
Field("acl_user", "ACL 用户名", "text", default="app"),
|
||||
Field("acl_password", "ACL 密码", "password", default="changeme"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
bind = p.get("bind", "0.0.0.0")
|
||||
port = str(p.get("port", "6379"))
|
||||
pwd = p.get("requirepass", "") or ""
|
||||
maxmem = p.get("maxmemory", "256mb")
|
||||
policy = p.get("maxmemory_policy", "allkeys-lru")
|
||||
aof = "yes" if bool_str(p.get("appendonly", True)) else "no"
|
||||
acl = bool_str(p.get("enable_acl"))
|
||||
acl_user = p.get("acl_user", "app")
|
||||
acl_pwd = p.get("acl_password", "changeme")
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing redis..."')
|
||||
out.append('$PKG_INSTALL redis-server')
|
||||
out.append('')
|
||||
out.append('cat > /etc/redis/redis.conf <<CFG_EOF\n'
|
||||
'bind ' + bind + '\n'
|
||||
'port ' + port + '\n'
|
||||
'daemonize yes\n'
|
||||
'supervised systemd\n'
|
||||
'pidfile /var/run/redis/redis-server.pid\n'
|
||||
'loglevel notice\n'
|
||||
'logfile /var/log/redis/redis-server.log\n'
|
||||
'databases 16\n'
|
||||
'maxmemory ' + maxmem + '\n'
|
||||
'maxmemory-policy ' + policy + '\n'
|
||||
'appendonly ' + aof + '\n'
|
||||
'appendfilename "appendonly.aof"\n'
|
||||
'save 900 1\n'
|
||||
'save 300 10\n'
|
||||
'save 60 10000\n'
|
||||
'tcp-keepalive 60\n'
|
||||
'timeout 0\n'
|
||||
'tcp-backlog 511\n'
|
||||
+ (f'requirepass {pwd}\n' if pwd and not acl else '') +
|
||||
'CFG_EOF')
|
||||
if acl:
|
||||
out.append('cat > /etc/redis/users.acl <<ACL_EOF\n'
|
||||
f'user {acl_user} on >{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://<server>:' + 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 <<UNIT_EOF\n'
|
||||
'[Unit]\n'
|
||||
'Description=Apache Tomcat\n'
|
||||
'After=network.target\n\n'
|
||||
'[Service]\n'
|
||||
'Type=forking\n'
|
||||
f'User={user}\nGroup={user}\n'
|
||||
f'Environment=JAVA_HOME=/usr/lib/jvm/java-11-openjdk\n'
|
||||
f'Environment=CATALINA_HOME={install_dir}\n'
|
||||
f'Environment=CATALINA_BASE={install_dir}\n'
|
||||
f'Environment=JAVA_OPTS="{java_opts}"\n'
|
||||
f'ExecStart={install_dir}/bin/startup.sh\n'
|
||||
f'ExecStop={install_dir}/bin/shutdown.sh\n'
|
||||
'Restart=on-failure\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\n'
|
||||
'UNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now tomcat')
|
||||
out.append('sleep 3')
|
||||
out.append('ss -ltn | grep :' + port + ' || warn "Tomcat not listening yet"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Docker ==========================
|
||||
class Docker(Generator):
|
||||
id = "docker"
|
||||
title = "Docker Engine + Compose"
|
||||
category = "middleware"
|
||||
icon = "🐳"
|
||||
tags = ["docker", "container", "compose"]
|
||||
description = "一键安装 Docker Engine、buildx、docker-compose v2、配置 cgroupdriver。"
|
||||
fields = [
|
||||
Field("docker_user", "免 sudo 用户", "text", default="root",
|
||||
help="将该用户加入 docker 组,免 sudo。"),
|
||||
Field("registry_mirror", "Registry 镜像加速", "text",
|
||||
default="https://mirror.ccs.tencentyun.com",
|
||||
help="如 https://docker.mirrors.ustc.edu.cn"),
|
||||
Field("cgroup_driver", "Cgroup Driver", "select", default="systemd",
|
||||
options=["systemd", "cgroupfs"]),
|
||||
Field("log_driver", "日志驱动", "select", default="json-file",
|
||||
options=["json-file", "journald", "syslog", "none"]),
|
||||
Field("log_max_size", "日志最大尺寸", "text", default="100m"),
|
||||
Field("log_max_file", "日志最大文件数", "text", default="3"),
|
||||
Field("enable_buildx", "启用 buildx", "checkbox", default="yes"),
|
||||
Field("data_root", "Docker 数据目录", "text", default="/var/lib/docker",
|
||||
help="留空使用默认 /var/lib/docker"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
usr = p.get("docker_user", "root")
|
||||
mirror = p.get("registry_mirror", "https://mirror.ccs.tencentyun.com")
|
||||
cg = p.get("cgroup_driver", "systemd")
|
||||
log_d = p.get("log_driver", "json-file")
|
||||
log_sz = p.get("log_max_size", "100m")
|
||||
log_n = p.get("log_max_file", "3")
|
||||
data_root = p.get("data_root", "/var/lib/docker")
|
||||
buildx = bool_str(p.get("enable_buildx", True))
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Docker..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL ca-certificates curl gnupg lsb-release\n'
|
||||
' install -m 0755 -d /etc/apt/keyrings\n'
|
||||
' curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg\n'
|
||||
' chmod a+r /etc/apt/keyrings/docker.gpg\n'
|
||||
' echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /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 <<CFG_EOF\n{\n'
|
||||
' "registry-mirrors": ["' + mirror + '"],\n'
|
||||
' "log-driver": "' + log_d + '",\n'
|
||||
' "log-opts": {\n'
|
||||
' "max-size": "' + log_sz + '",\n'
|
||||
' "max-file": "' + log_n + '"\n'
|
||||
' },\n'
|
||||
' "storage-driver": "overlay2",\n'
|
||||
' "data-root": "' + data_root + '"\n'
|
||||
'}\nCFG_EOF')
|
||||
out.append('systemctl restart docker')
|
||||
if usr != "root":
|
||||
out.append('usermod -aG docker ' + usr)
|
||||
out.append('log "User ' + usr + ' added to docker group (need to re-login)"')
|
||||
out.append('docker --version')
|
||||
if buildx:
|
||||
out.append('docker buildx version || true')
|
||||
out.append('docker compose version || true')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== ETCD ==========================
|
||||
class ETCD(Generator):
|
||||
id = "etcd"
|
||||
title = "etcd 分布式 KV 存储"
|
||||
category = "middleware"
|
||||
icon = "🗝️"
|
||||
tags = ["etcd", "kv", "discovery"]
|
||||
description = "部署单节点 etcd(可扩展为集群),开启 v2/v3 双协议、http API。"
|
||||
fields = [
|
||||
Field("name", "节点名", "text", default="etcd-node1"),
|
||||
Field("data_dir", "数据目录", "text", default="/var/lib/etcd"),
|
||||
Field("listen_client", "客户端监听 URL", "text", default="http://0.0.0.0:2379"),
|
||||
Field("listen_peer", "Peer 监听 URL", "text", default="http://0.0.0.0:2380"),
|
||||
Field("advertise_client", "广播客户端 URL", "text", default="http://127.0.0.1:2379"),
|
||||
Field("initial_cluster", "初始集群列表", "text", default="etcd-node1=http://127.0.0.1:2380"),
|
||||
Field("version", "etcd 版本", "text", default="3.5.13"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
name = p.get("name", "etcd-node1")
|
||||
data = p.get("data_dir", "/var/lib/etcd")
|
||||
lclient = p.get("listen_client", "http://0.0.0.0:2379")
|
||||
lpeer = p.get("listen_peer", "http://0.0.0.0:2380")
|
||||
advertise = p.get("advertise_client", "http://127.0.0.1:2379")
|
||||
cluster = p.get("initial_cluster", "etcd-node1=http://127.0.0.1:2380")
|
||||
ver = p.get("version", "3.5.13")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing etcd v' + ver + '..."')
|
||||
out.append('useradd -r -s /bin/false etcd 2>/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 <<CFG_EOF\n'
|
||||
'ETCD_NAME="' + name + '"\n'
|
||||
'ETCD_DATA_DIR="' + data + '"\n'
|
||||
'ETCD_LISTEN_CLIENT_URLS="' + lclient + '"\n'
|
||||
'ETCD_LISTEN_PEER_URLS="' + lpeer + '"\n'
|
||||
'ETCD_ADVERTISE_CLIENT_URLS="' + advertise + '"\n'
|
||||
'ETCD_INITIAL_ADVERTISE_PEER_URLS="' + lpeer + '"\n'
|
||||
'ETCD_INITIAL_CLUSTER="' + cluster + '"\n'
|
||||
'ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-1"\n'
|
||||
'ETCD_INITIAL_CLUSTER_STATE="new"\nCFG_EOF')
|
||||
out.append('cat > /etc/systemd/system/etcd.service <<UNIT_EOF\n'
|
||||
'[Unit]\n'
|
||||
'Description=etcd\n'
|
||||
'After=network.target\n\n'
|
||||
'[Service]\n'
|
||||
'Type=notify\n'
|
||||
'User=etcd\n'
|
||||
'Group=etcd\n'
|
||||
'EnvironmentFile=/etc/etcd/etcd.conf\n'
|
||||
'ExecStart=/usr/local/bin/etcd\n'
|
||||
'Restart=on-failure\n'
|
||||
'LimitNOFILE=65536\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now etcd')
|
||||
out.append('sleep 3')
|
||||
out.append('etcdctl --endpoints=' + advertise + ' endpoint health || true')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Zookeeper ==========================
|
||||
class Zookeeper(Generator):
|
||||
id = "zookeeper"
|
||||
title = "Apache ZooKeeper"
|
||||
category = "middleware"
|
||||
icon = "🦓"
|
||||
tags = ["zookeeper", "zk", "coordination"]
|
||||
description = "部署单节点 ZooKeeper,可扩展为集群。"
|
||||
fields = [
|
||||
Field("client_port", "Client 端口", "number", default="2181", min_=1, max_=65535),
|
||||
Field("data_dir", "数据目录", "text", default="/var/lib/zookeeper"),
|
||||
Field("tick_time", "Tick Time (ms)", "number", default="2000"),
|
||||
Field("init_limit", "Init Limit", "number", default="10"),
|
||||
Field("sync_limit", "Sync Limit", "number", default="5"),
|
||||
Field("max_client_cnxns", "最大客户端连接数", "number", default="60"),
|
||||
Field("version", "ZooKeeper 版本", "text", default="3.9.2"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
port = str(p.get("client_port", "2181"))
|
||||
data = p.get("data_dir", "/var/lib/zookeeper")
|
||||
tick = str(p.get("tick_time", "2000"))
|
||||
init_l = str(p.get("init_limit", "10"))
|
||||
sync = str(p.get("sync_limit", "5"))
|
||||
cnxns = str(p.get("max_client_cnxns", "60"))
|
||||
ver = p.get("version", "3.9.2")
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Java + ZooKeeper..."')
|
||||
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 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 <<CFG_EOF\n'
|
||||
'tickTime=' + tick + '\n'
|
||||
'initLimit=' + init_l + '\n'
|
||||
'syncLimit=' + sync + '\n'
|
||||
'dataDir=' + data + '\n'
|
||||
'clientPort=' + port + '\n'
|
||||
'maxClientCnxns=' + cnxns + '\n'
|
||||
'4lw.commands.whitelist=*\\nCFG_EOF')
|
||||
out.append('echo "' + str(int(__import__('time').time())) + '" > ' + data + '/myid')
|
||||
out.append('')
|
||||
out.append('cat > /etc/systemd/system/zookeeper.service <<UNIT_EOF\n'
|
||||
'[Unit]\n'
|
||||
'Description=Apache ZooKeeper\n'
|
||||
'After=network.target\n\n'
|
||||
'[Service]\n'
|
||||
'Type=forking\n'
|
||||
'User=zookeeper\nGroup=zookeeper\n'
|
||||
'Environment=JAVA_HOME=/usr/lib/jvm/java-11-openjdk\n'
|
||||
'ExecStart=/opt/zookeeper/bin/zkServer.sh start\n'
|
||||
'ExecStop=/opt/zookeeper/bin/zkServer.sh stop\n'
|
||||
'ExecReload=/opt/zookeeper/bin/zkServer.sh restart\n'
|
||||
'Restart=on-failure\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now zookeeper')
|
||||
out.append('sleep 3')
|
||||
out.append('/opt/zookeeper/bin/zkServer.sh status || true')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# Register everything in this file
|
||||
for _g in [VNCSrv, Nginx, HAProxy, Keepalived, Redis, RabbitMQ, Tomcat, Docker, ETCD, Zookeeper]:
|
||||
register(_g())
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Monitoring & alerting.
|
||||
- Prometheus Node Exporter
|
||||
- Prometheus server
|
||||
- Grafana
|
||||
- node_exporter + alertmanager bundle
|
||||
- logrotate helper
|
||||
"""
|
||||
from . import register, Generator, Field
|
||||
from . import bash_header, quote, bool_str, yes
|
||||
|
||||
|
||||
# ========================== Node Exporter ==========================
|
||||
class NodeExporter(Generator):
|
||||
id = "node_exporter"
|
||||
title = "Prometheus Node Exporter"
|
||||
category = "monitoring"
|
||||
icon = "📊"
|
||||
tags = ["prometheus", "node_exporter", "monitoring"]
|
||||
description = "安装 Prometheus node_exporter,默认监听 9100。"
|
||||
fields = [
|
||||
Field("version", "版本", "text", default="1.8.2"),
|
||||
Field("port", "监听端口", "number", default="9100", min_=1, max_=65535),
|
||||
Field("extra_args", "额外参数", "text",
|
||||
default="--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "1.8.2")
|
||||
port = str(p.get("port", "9100"))
|
||||
extra = p.get("extra_args", "")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing node_exporter ' + ver + '..."')
|
||||
out.append('useradd -r -s /bin/false node_exporter 2>/dev/null || true')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://github.com/prometheus/node_exporter/releases/download/v' + ver + '/node_exporter-' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('tar -xzf node_exporter-' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('cp node_exporter-' + ver + '.linux-amd64/node_exporter /usr/local/bin/')
|
||||
out.append('rm -rf node_exporter-' + ver + '.linux-amd64*')
|
||||
out.append('cat > /etc/systemd/system/node_exporter.service <<UNIT_EOF\n'
|
||||
'[Unit]\nDescription=Prometheus Node Exporter\n'
|
||||
'After=network.target\n\n'
|
||||
'[Service]\n'
|
||||
'User=node_exporter\n'
|
||||
'ExecStart=/usr/local/bin/node_exporter --web.listen-address=:' + port + ' ' + extra + '\n'
|
||||
'Restart=on-failure\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now node_exporter')
|
||||
out.append('sleep 1')
|
||||
out.append('curl -s http://127.0.0.1:' + port + '/metrics | head -5')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Prometheus Server ==========================
|
||||
class Prometheus(Generator):
|
||||
id = "prometheus"
|
||||
title = "Prometheus 服务端"
|
||||
category = "monitoring"
|
||||
icon = "🔥"
|
||||
tags = ["prometheus", "monitoring", "metrics"]
|
||||
description = "安装 Prometheus,带可配置 scrape jobs。"
|
||||
fields = [
|
||||
Field("version", "版本", "text", default="2.53.0"),
|
||||
Field("port", "监听端口", "number", default="9090", min_=1, max_=65535),
|
||||
Field("retention", "保留时间", "text", default="30d"),
|
||||
Field("scrape_jobs", "Scrape 目标", "textarea",
|
||||
default="node-exporter: ['localhost:9100']\nmyapp: ['localhost:8000']",
|
||||
help="每行格式: <job_name>: ['host:port', ...]"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "2.53.0")
|
||||
port = str(p.get("port", "9090"))
|
||||
ret = p.get("retention", "30d")
|
||||
lines = p.get("scrape_jobs", "").splitlines()
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Prometheus ' + ver + '..."')
|
||||
out.append('useradd -r -s /bin/false prometheus 2>/dev/null || true')
|
||||
out.append('mkdir -p /etc/prometheus /var/lib/prometheus')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://github.com/prometheus/prometheus/releases/download/v' + ver + '/prometheus-' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('tar -xzf prometheus-' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('cp prometheus-' + ver + '.linux-amd64/prometheus /usr/local/bin/')
|
||||
out.append('cp prometheus-' + ver + '.linux-amd64/promtool /usr/local/bin/')
|
||||
out.append('cp -r prometheus-' + ver + '.linux-amd64/console_libraries /etc/prometheus/')
|
||||
out.append('cp -r prometheus-' + ver + '.linux-amd64/consoles /etc/prometheus/')
|
||||
out.append('rm -rf prometheus-' + ver + '.linux-amd64*')
|
||||
out.append('chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus')
|
||||
out.append('')
|
||||
cfg = ['global:', ' scrape_interval: 15s', ' evaluation_interval: 15s',
|
||||
'', 'rule_files:', ' - "rules/*.yml"', '', 'scrape_configs:',
|
||||
' - job_name: prometheus', ' static_configs:',
|
||||
' - targets: [\'localhost:' + port + '\']']
|
||||
for line in lines:
|
||||
if ':' not in line:
|
||||
continue
|
||||
name, targets = line.split(':', 1)
|
||||
name = name.strip()
|
||||
tg = [t.strip().strip("'").strip('"') for t in
|
||||
targets.strip().lstrip('[').rstrip(']').split(',') if t.strip()]
|
||||
cfg.append(f' - job_name: {name}')
|
||||
cfg.append(' static_configs:')
|
||||
cfg.append(' - targets: ' + str(tg).replace("'", '"'))
|
||||
out.append('cat > /etc/prometheus/prometheus.yml <<CFG_EOF\n'
|
||||
+ '\n'.join(cfg) + '\nCFG_EOF')
|
||||
out.append('cat > /etc/systemd/system/prometheus.service <<UNIT_EOF\n'
|
||||
'[Unit]\nDescription=Prometheus\nAfter=network.target\n\n'
|
||||
'[Service]\nUser=prometheus\nGroup=prometheus\n'
|
||||
'Type=simple\n'
|
||||
'ExecStart=/usr/local/bin/prometheus \\\n'
|
||||
' --config.file=/etc/prometheus/prometheus.yml \\\n'
|
||||
' --storage.tsdb.path=/var/lib/prometheus/ \\\n'
|
||||
' --web.console.libraries=/etc/prometheus/console_libraries \\\n'
|
||||
' --web.console.templates=/etc/prometheus/consoles \\\n'
|
||||
' --web.listen-address=:' + port + ' \\\n'
|
||||
' --storage.tsdb.retention.time=' + ret + '\n'
|
||||
'Restart=on-failure\n\n'
|
||||
'[Install]\nWantedBy=multi-user.target\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now prometheus')
|
||||
out.append('sleep 2')
|
||||
out.append('curl -s http://127.0.0.1:' + port + '/-/ready')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Grafana ==========================
|
||||
class Grafana(Generator):
|
||||
id = "grafana"
|
||||
title = "Grafana"
|
||||
category = "monitoring"
|
||||
icon = "📈"
|
||||
tags = ["grafana", "dashboard", "monitoring"]
|
||||
description = "安装 Grafana OSS,配置 admin 密码。"
|
||||
fields = [
|
||||
Field("admin_user", "Admin 用户", "text", default="admin"),
|
||||
Field("admin_password", "Admin 密码", "password", default="admin"),
|
||||
Field("port", "端口", "number", default="3000", min_=1, max_=65535),
|
||||
Field("prom_url", "Prometheus 数据源 URL", "text", default="http://localhost:9090"),
|
||||
Field("root_url", "Grafana 公开 URL", "text", default="http://localhost:3000"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
user = p.get("admin_user", "admin")
|
||||
pwd = p.get("admin_password", "admin")
|
||||
port = str(p.get("port", "3000"))
|
||||
prom = p.get("prom_url", "http://localhost:9090")
|
||||
root = p.get("root_url", "http://localhost:3000")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Grafana..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL -y software-properties-common\n'
|
||||
' mkdir -p /etc/apt/keyrings\n'
|
||||
' wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/grafana.gpg > /dev/null\n'
|
||||
' echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" > /etc/apt/sources.list.d/grafana.list\n'
|
||||
' apt-get update\n'
|
||||
' $PKG_INSTALL grafana\n'
|
||||
' ;;\n'
|
||||
' yum|dnf)\n'
|
||||
' cat > /etc/yum.repos.d/grafana.repo <<REPO_EOF\n'
|
||||
'[grafana]\nname=grafana\nbaseurl=https://rpm.grafana.com\nrepo=gpgkey\nenabled=1\ngpgcheck=1\ngpgkey=https://rpm.grafana.com/gpg.key\nREPO_EOF\n'
|
||||
' $PKG_INSTALL grafana\n'
|
||||
' ;;\n'
|
||||
'esac')
|
||||
out.append('systemctl enable --now grafana-server')
|
||||
out.append('sleep 3')
|
||||
out.append('log "Setting admin password..."')
|
||||
out.append('grafana-cli admin reset-admin-password ' + pwd + ' || warn "reset failed, use UI"')
|
||||
out.append('log "Adding Prometheus datasource..."')
|
||||
out.append('cat > /etc/grafana/provisioning/datasources/prometheus.yaml <<YAML_EOF\n'
|
||||
'apiVersion: 1\ndatasources:\n - name: Prometheus\n'
|
||||
' type: prometheus\n access: proxy\n url: ' + prom + '\n'
|
||||
' isDefault: true\nYAML_EOF')
|
||||
out.append('systemctl restart grafana-server')
|
||||
out.append('log "Open: ' + root + ' user: ' + user + ' pass: ' + pwd + '"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Logrotate (generic) ==========================
|
||||
class Logrotate(Generator):
|
||||
id = "logrotate"
|
||||
title = "Logrotate 日志轮转"
|
||||
category = "monitoring"
|
||||
icon = "🔄"
|
||||
tags = ["logrotate", "log"]
|
||||
description = "为指定日志文件生成 logrotate 配置。"
|
||||
fields = [
|
||||
Field("paths", "日志路径 (空格分隔)", "textarea", default="/var/log/myapp/*.log"),
|
||||
Field("rotate_count", "保留份数", "number", default="14", min_=1, max_=365),
|
||||
Field("size", "超过该大小就轮转", "text", default="100M",
|
||||
help="如 100M / 1G,留空 = 仅按时间。"),
|
||||
Field("compress", "启用压缩", "checkbox", default="yes"),
|
||||
Field("user", "轮转用户", "text", default="root"),
|
||||
Field("group", "轮转组", "text", default="root"),
|
||||
Field("postrotate", "Postrotate 脚本", "text", default=""),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
paths = p.get("paths", "/var/log/myapp/*.log").replace("\n", " ")
|
||||
n = str(p.get("rotate_count", "14"))
|
||||
sz = p.get("size", "100M")
|
||||
comp = bool_str(p.get("compress", True))
|
||||
u = p.get("user", "root")
|
||||
g = p.get("group", "root")
|
||||
pr = p.get("postrotate", "").strip()
|
||||
out = [bash_header(self.title)]
|
||||
logrot = [paths + ' {', ' daily', ' missingok', ' rotate ' + n]
|
||||
if sz:
|
||||
logrot.append(' size ' + sz)
|
||||
logrot.append(' compress' if comp else ' nocompress')
|
||||
if comp:
|
||||
logrot.append(' delaycompress')
|
||||
logrot.append(' notifempty')
|
||||
logrot.append(' create 0640 ' + u + ' ' + g)
|
||||
logrot.append(' sharedscripts')
|
||||
if pr:
|
||||
logrot.append(' postrotate\n ' + pr + '\n endscript')
|
||||
logrot.append('}')
|
||||
out.append('cat > /etc/logrotate.d/99-shellgen <<CFG_EOF\n' + '\n'.join(logrot) + '\nCFG_EOF')
|
||||
out.append('logrotate -d /etc/logrotate.d/99-shellgen')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
for _g in [NodeExporter, Prometheus, Grafana, Logrotate]:
|
||||
register(_g())
|
||||
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Network tools.
|
||||
- Wireguard VPN
|
||||
- OpenVPN
|
||||
- Tailscale client
|
||||
- DHCP server (isc-dhcp-server / kea)
|
||||
- DNS forwarder (unbound / dnsmasq)
|
||||
"""
|
||||
from . import register, Generator, Field
|
||||
from . import bash_header, quote, bool_str, yes
|
||||
|
||||
|
||||
# ========================== WireGuard ==========================
|
||||
class WireGuard(Generator):
|
||||
id = "wireguard"
|
||||
title = "WireGuard VPN"
|
||||
category = "network"
|
||||
icon = "🔒"
|
||||
tags = ["wireguard", "vpn", "wg"]
|
||||
description = "部署 WireGuard VPN server,可生成客户端配置。"
|
||||
fields = [
|
||||
Field("server_private_key", "Server Private Key", "password", default="",
|
||||
help="留空将自动生成 (使用 wg genkey)。"),
|
||||
Field("server_public_key", "Server Public Key", "text", default="",
|
||||
help="留空将自动从 private key 派生。"),
|
||||
Field("listen_port", "Listen 端口", "number", default="51820", min_=1, max_=65535),
|
||||
Field("interface", "网卡接口", "text", default="eth0"),
|
||||
Field("vpn_subnet", "VPN 子网 CIDR", "text", default="10.0.0.0/24"),
|
||||
Field("server_vpn_ip", "Server VPN IP", "text", default="10.0.0.1/24"),
|
||||
Field("peers", "客户端 Peer 列表", "textarea", default="client1\nclient2",
|
||||
help="每行一个 client name,会为每个生成密钥对。"),
|
||||
Field("peer_dns", "客户端 DNS", "text", default="1.1.1.1, 8.8.8.8"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
sk = p.get("server_private_key", "").strip()
|
||||
pk = p.get("server_public_key", "").strip()
|
||||
port = str(p.get("listen_port", "51820"))
|
||||
iface = p.get("interface", "eth0")
|
||||
subnet = p.get("vpn_subnet", "10.0.0.0/24")
|
||||
sip = p.get("server_vpn_ip", "10.0.0.1/24")
|
||||
peers = [p_.strip() for p_ in p.get("peers", "").splitlines() if p_.strip()]
|
||||
dns = p.get("peer_dns", "1.1.1.1, 8.8.8.8")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing WireGuard..."')
|
||||
out.append('$PKG_INSTALL wireguard qrencode')
|
||||
out.append('')
|
||||
# server keypair
|
||||
if not sk:
|
||||
out.append('wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub')
|
||||
else:
|
||||
out.append('mkdir -p /etc/wireguard && echo "' + sk + '" > /etc/wireguard/server.key')
|
||||
out.append('wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub')
|
||||
if pk:
|
||||
out.append('echo "' + pk + '" > /etc/wireguard/server.pub')
|
||||
out.append('chmod 600 /etc/wireguard/server.key')
|
||||
out.append('')
|
||||
# generate peer configs
|
||||
out.append('mkdir -p /etc/wireguard/clients')
|
||||
for i, name in enumerate(peers, start=2):
|
||||
ip = f'10.0.0.{i}/32'
|
||||
out.append('wg genkey | tee /etc/wireguard/clients/' + name + '.key | wg pubkey > /etc/wireguard/clients/' + name + '.pub')
|
||||
out.append('chmod 600 /etc/wireguard/clients/' + name + '.key')
|
||||
# server conf
|
||||
out.append('cat > /etc/wireguard/wg0.conf <<WG_EOF\n'
|
||||
'[Interface]\n'
|
||||
'Address = ' + sip + '\n'
|
||||
'ListenPort = ' + port + '\n'
|
||||
'PrivateKey = ' + (sk if sk else '$(cat /etc/wireguard/server.key)') + '\n'
|
||||
'PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ' + iface + ' -j MASQUERADE\n'
|
||||
'PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ' + iface + ' -j MASQUERADE\n\n')
|
||||
for i, name in enumerate(peers, start=2):
|
||||
out.append('[Peer]\n'
|
||||
f'# {name}\n'
|
||||
f'PublicKey = $(cat /etc/wireguard/clients/{name}.pub)\n'
|
||||
f'AllowedIPs = 10.0.0.{i}/32\n\n')
|
||||
out.append('WG_EOF')
|
||||
out.append('chmod 600 /etc/wireguard/wg0.conf')
|
||||
out.append('echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/99-wireguard.conf && sysctl -p /etc/sysctl.d/99-wireguard.conf')
|
||||
out.append('systemctl enable --now wg-quick@wg0')
|
||||
# build client confs
|
||||
for i, name in enumerate(peers, start=2):
|
||||
ip = f'10.0.0.{i}/32'
|
||||
out.append('SERVER_PUB=$(cat /etc/wireguard/server.pub)')
|
||||
out.append('CLIENT_PRIV=$(cat /etc/wireguard/clients/' + name + '.key)')
|
||||
out.append('cat > /etc/wireguard/clients/' + name + '.conf <<CLI_EOF\n'
|
||||
'[Interface]\n'
|
||||
'Address = ' + ip + '\n'
|
||||
'DNS = ' + dns + '\n'
|
||||
'PrivateKey = ${CLIENT_PRIV}\n\n'
|
||||
'[Peer]\n'
|
||||
'PublicKey = ${SERVER_PUB}\n'
|
||||
'Endpoint = YOUR_SERVER_IP:' + port + '\n'
|
||||
'AllowedIPs = 0.0.0.0/0, ::/0\n'
|
||||
'PersistentKeepalive = 25\n'
|
||||
'CLI_EOF')
|
||||
out.append('qrencode -t ansiutf8 < /etc/wireguard/clients/' + name + '.conf || true')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Tailscale ==========================
|
||||
class Tailscale(Generator):
|
||||
id = "tailscale"
|
||||
title = "Tailscale 客户端"
|
||||
category = "network"
|
||||
icon = "🦀"
|
||||
tags = ["tailscale", "vpn", "mesh"]
|
||||
description = "安装并启动 Tailscale,可指定自定义控制服务器。"
|
||||
fields = [
|
||||
Field("login_server", "控制服务器", "text", default="https://controlplane.tailscale.com",
|
||||
help="Headscale 自建时填入,如 https://hs.example.com。"),
|
||||
Field("accept_routes", "接受子网路由", "checkbox", default="no"),
|
||||
Field("advertise_exit", "作为 Exit Node", "checkbox", default="no"),
|
||||
Field("auth_key", "Auth Key (Headscale/preauth)", "password", default="",
|
||||
help="留空则用 sudo tailscale up 手动登录。"),
|
||||
Field("hostname", "Hostname", "text", default="$(hostname)"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ls = p.get("login_server", "https://controlplane.tailscale.com")
|
||||
ar = bool_str(p.get("accept_routes"))
|
||||
ex = bool_str(p.get("advertise_exit"))
|
||||
ak = p.get("auth_key", "").strip()
|
||||
hn = p.get("hostname", "$(hostname)")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Tailscale..."')
|
||||
out.append('curl -fsSL https://tailscale.com/install.sh | sh')
|
||||
out.append('systemctl enable --now tailscaled')
|
||||
out.append('sleep 2')
|
||||
flags = []
|
||||
if ls:
|
||||
flags.append('--login-server=' + ls)
|
||||
if ar:
|
||||
flags.append('--accept-routes')
|
||||
if ex:
|
||||
flags.append('--advertise-exit-node')
|
||||
if hn:
|
||||
flags.append('--hostname=' + hn)
|
||||
flag_str = " ".join(flags)
|
||||
if ak:
|
||||
out.append('tailscale up ' + flag_str + ' --authkey=' + ak)
|
||||
else:
|
||||
out.append('tailscale up ' + flag_str)
|
||||
out.append('log "Visit the URL printed above to authenticate."')
|
||||
out.append('tailscale status')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== DNSMASQ ==========================
|
||||
class Dnsmasq(Generator):
|
||||
id = "dnsmasq"
|
||||
title = "Dnsmasq (DHCP + DNS 缓存)"
|
||||
category = "network"
|
||||
icon = "📡"
|
||||
tags = ["dns", "dhcp", "dnsmasq"]
|
||||
description = "轻量级 DNS 转发 + DHCP server。"
|
||||
fields = [
|
||||
Field("enable_dhcp", "启用 DHCP", "checkbox", default="yes"),
|
||||
Field("dhcp_range_start", "DHCP 起始 IP", "text", default="192.168.1.100"),
|
||||
Field("dhcp_range_end", "DHCP 结束 IP", "text", default="192.168.1.200"),
|
||||
Field("dhcp_lease", "租约时间", "text", default="12h"),
|
||||
Field("dhcp_gateway", "DHCP 网关", "text", default="192.168.1.1"),
|
||||
Field("dhcp_dns", "DHCP 推送的 DNS", "text", default="1.1.1.1, 8.8.8.8"),
|
||||
Field("upstream_dns", "上游 DNS", "textarea", default="1.1.1.1\n8.8.8.8"),
|
||||
Field("local_domain", "本地域名", "text", default="lan"),
|
||||
Field("listen_address", "Listen IP", "text", default="0.0.0.0"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ed = bool_str(p.get("enable_dhcp", True))
|
||||
rstart = p.get("dhcp_range_start", "192.168.1.100")
|
||||
rend = p.get("dhcp_range_end", "192.168.1.200")
|
||||
lease = p.get("dhcp_lease", "12h")
|
||||
gw = p.get("dhcp_gateway", "192.168.1.1")
|
||||
dns = p.get("dhcp_dns", "1.1.1.1, 8.8.8.8")
|
||||
up = [u.strip() for u in p.get("upstream_dns", "").splitlines() if u.strip()]
|
||||
domain = p.get("local_domain", "lan")
|
||||
listen = p.get("listen_address", "0.0.0.0")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing dnsmasq..."')
|
||||
out.append('$PKG_INSTALL dnsmasq')
|
||||
out.append('cp /etc/dnsmasq.conf /etc/dnsmasq.conf.bak.$(date +%s) || true')
|
||||
cfg = []
|
||||
cfg.append('listen-address=' + listen)
|
||||
cfg.append('bind-interfaces')
|
||||
cfg.append('domain=' + domain)
|
||||
cfg.append('local=/' + domain + '/')
|
||||
cfg.append('cache-size=1000')
|
||||
for u in up:
|
||||
cfg.append('server=' + u)
|
||||
if ed:
|
||||
cfg.append('dhcp-range=' + rstart + ',' + rend + ',' + lease)
|
||||
cfg.append('dhcp-option=3,' + gw)
|
||||
cfg.append('dhcp-option=6,' + dns)
|
||||
out.append('cat > /etc/dnsmasq.conf <<CFG_EOF\n' + '\n'.join(cfg) + '\nCFG_EOF')
|
||||
out.append('systemctl enable --now dnsmasq')
|
||||
out.append('ss -ltnup | grep :53 || warn "port 53 not listening yet"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== OpenVPN ==========================
|
||||
class OpenVPN(Generator):
|
||||
id = "openvpn"
|
||||
title = "OpenVPN 简化部署",
|
||||
category = "network"
|
||||
icon = "🛡️"
|
||||
tags = ["openvpn", "vpn"]
|
||||
description = "安装 openvpn + easy-rsa,生成服务端 CA/密钥/客户端配置。"
|
||||
warnings = ["生产环境请先生成正式 PKI,本脚本为快速原型。"]
|
||||
fields = [
|
||||
Field("proto", "协议", "select", default="udp", options=["udp", "tcp"]),
|
||||
Field("port", "端口", "number", default="1194", min_=1, max_=65535),
|
||||
Field("subnet", "VPN 子网", "text", default="10.8.0.0/24"),
|
||||
Field("dns_servers", "推送的 DNS", "text", default="1.1.1.1, 8.8.8.8"),
|
||||
Field("cipher", "加密算法", "select", default="AES-256-GCM",
|
||||
options=["AES-256-GCM", "AES-256-CBC", "CHACHA20-POLY1305"]),
|
||||
Field("clients", "生成客户端数", "number", default="1", min_=1, max_=20),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
proto = p.get("proto", "udp")
|
||||
port = str(p.get("port", "1194"))
|
||||
subnet = p.get("subnet", "10.8.0.0/24")
|
||||
dns = p.get("dns_servers", "1.1.1.1, 8.8.8.8")
|
||||
cipher = p.get("cipher", "AES-256-GCM")
|
||||
n = int(p.get("clients", "1"))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing openvpn + easy-rsa..."')
|
||||
out.append('$PKG_INSTALL openvpn easy-rsa')
|
||||
out.append('make-cadir /etc/openvpn/easy-rsa')
|
||||
out.append('cd /etc/openvpn/easy-rsa')
|
||||
out.append('./easyrsa init-pki')
|
||||
out.append('./easyrsa build-ca nopass')
|
||||
out.append('./easyrsa gen-req server nopass')
|
||||
out.append('./easyrsa sign-req server server <<< "yes"')
|
||||
out.append('./easyrsa gen-dh')
|
||||
out.append('openvpn --genkey secret /etc/openvpn/ta.key')
|
||||
out.append('cp pki/ca.crt pki/private/server.key pki/issued/server.crt pki/dh.pem /etc/openvpn/')
|
||||
out.append('cp /etc/openvpn/easy-rsa/ta.key /etc/openvpn/')
|
||||
out.append('')
|
||||
out.append('cat > /etc/openvpn/server.conf <<CFG_EOF\n'
|
||||
'port ' + port + '\n'
|
||||
'proto ' + proto + '\n'
|
||||
'dev tun\n'
|
||||
'ca ca.crt\ncert server.crt\nkey server.key\ndh dh.pem\n'
|
||||
'auth SHA256\ncipher ' + cipher + '\n'
|
||||
'tls-auth ta.key 0\n'
|
||||
'topology subnet\n'
|
||||
'server ' + subnet + '\n'
|
||||
'push "redirect-gateway def1 bypass-dhcp"\n'
|
||||
'push "dhcp-option DNS ' + dns.split(',')[0].strip() + '"\n'
|
||||
'keepalive 10 120\npersist-key\npersist-tun\nstatus openvpn-status.log\nverb 3\n'
|
||||
'CFG_EOF')
|
||||
out.append('systemctl enable --now openvpn@server')
|
||||
out.append('echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/99-openvpn.conf && sysctl -p')
|
||||
for i in range(1, n + 1):
|
||||
cn = f'client{i}'
|
||||
out.append('cd /etc/openvpn/easy-rsa && ./easyrsa gen-req ' + cn + ' nopass')
|
||||
out.append('cd /etc/openvpn/easy-rsa && ./easyrsa sign-req client ' + cn + ' <<< "yes"')
|
||||
out.append('cat > /etc/openvpn/' + cn + '.ovpn <<OVPN_EOF\n'
|
||||
'client\ndev tun\nproto ' + proto + '\n'
|
||||
'remote YOUR_SERVER_IP ' + port + '\n'
|
||||
'resolv-retry infinite\nnobind\n'
|
||||
'persist-key\npersist-tun\nremote-cert-tls server\nauth SHA256\ncipher ' + cipher + '\n'
|
||||
'verb 3\n<ca>\n$(cat /etc/openvpn/ca.crt)\n</ca>\n<cert>\n$(cat /etc/openvpn/easy-rsa/pki/issued/' + cn + '.crt)\n</cert>\n<key>\n$(cat /etc/openvpn/easy-rsa/pki/private/' + cn + '.key)\n</key>\n<tls-auth>\n$(cat /etc/openvpn/ta.key)\n</tls-auth>\nkey-direction 1\nOVPN_EOF')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
for _g in [WireGuard, Tailscale, Dnsmasq, OpenVPN]:
|
||||
register(_g())
|
||||
@@ -0,0 +1,651 @@
|
||||
"""
|
||||
Runtime / language / compiler version installers.
|
||||
Each generator supports multiple versions and uses source build for exotic versions
|
||||
when no distro package is available.
|
||||
"""
|
||||
from . import register, Generator, Field
|
||||
from . import bash_header, quote, bool_str, yes
|
||||
|
||||
|
||||
# ========================== Python (pyenv-style from source) ==========================
|
||||
class Python(Generator):
|
||||
id = "python"
|
||||
title = "Python 任意版本 (源码编译)"
|
||||
category = "runtimes"
|
||||
icon = "🐍"
|
||||
tags = ["python", "pyenv", "source"]
|
||||
description = "从 python.org 下载源码编译,支持 2.7 / 3.6 - 3.13 任意版本,启用 SSL/zlib/sqlite3。"
|
||||
warnings = [
|
||||
"源码编译会下载 ~25MB,编译 5-15 分钟,需要 build-essential。",
|
||||
"建议先运行 gcc 编译器安装脚本(同分类)以保证编译依赖。",
|
||||
]
|
||||
fields = [
|
||||
Field("version", "Python 版本", "select", default="3.12.6",
|
||||
options=["2.7.18", "3.6.15", "3.7.17", "3.8.20", "3.9.20",
|
||||
"3.10.14", "3.11.11", "3.12.6", "3.13.0", "3.13.1"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/python",
|
||||
help="最终路径 = $install_dir/$version (如 /opt/python/3.12.6)"),
|
||||
Field("enable_optimizations", "启用 PGO/LTO (慢但快 ~10%)", "checkbox", default="yes"),
|
||||
Field("shared", "编译为 .so 共享库", "checkbox", default="no",
|
||||
help="如果其他工具(如 mod_wsgi)需要动态链接则开启。"),
|
||||
Field("install_pip", "安装 pip", "checkbox", default="yes"),
|
||||
Field("symlink_bin", "软链 bin 目录到 PATH", "checkbox", default="no",
|
||||
help="yes = 软链 /opt/python/$version/bin/* 到 /usr/local/bin/"),
|
||||
Field("ssl_backend", "SSL 后端", "select", default="openssl",
|
||||
options=["openssl", "libressl"]),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "3.12.6")
|
||||
d = p.get("install_dir", "/opt/python")
|
||||
opt = " --enable-optimizations" if bool_str(p.get("enable_optimizations", True)) else ""
|
||||
shared = " --enable-shared" if bool_str(p.get("shared")) else ""
|
||||
ssl = p.get("ssl_backend", "openssl")
|
||||
sym = bool_str(p.get("symlink_bin"))
|
||||
pip = bool_str(p.get("install_pip", True))
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Python ' + ver + ' from source..."')
|
||||
out.append('command -v gcc >/dev/null || $PKG_INSTALL gcc make build-essential \\\n'
|
||||
' libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \\\n'
|
||||
' libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev \\\n'
|
||||
' || $PKG_INSTALL gcc make openssl-devel bzip2-devel libffi-devel zlib-devel \\\n'
|
||||
' readline-devel sqlite-devel ncurses-devel xz-devel')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -o Python.tgz https://www.python.org/ftp/python/' + ver + '/Python-' + ver + '.tgz')
|
||||
out.append('tar -xzf Python.tgz')
|
||||
out.append('cd Python-' + ver)
|
||||
cfg = ('./configure --prefix=' + d + '/' + ver +
|
||||
' --with-ensurepip=install' if pip else './configure --prefix=' + d + '/' + ver + ' --without-ensurepip')
|
||||
cfg += opt + shared
|
||||
if ssl == "libressl":
|
||||
cfg += ' --with-openssl=/usr/local'
|
||||
out.append(cfg)
|
||||
out.append('make -j"$(nproc)"')
|
||||
out.append('make altinstall')
|
||||
out.append('cd /tmp && rm -rf Python-' + ver + ' Python.tgz')
|
||||
out.append('')
|
||||
out.append('PYBIN=' + d + '/' + ver + '/bin/python' + '.'.join(ver.split('.')[:2]))
|
||||
if sym:
|
||||
out.append('log "Symlinking binaries to /usr/local/bin/..."')
|
||||
out.append('for b in ' + d + '/' + ver + '/bin/*; do\n'
|
||||
' ln -sf "$b" /usr/local/bin/"$(basename "$b")" || true\n'
|
||||
'done')
|
||||
out.append('log "Verifying..."')
|
||||
out.append('$PYBIN --version')
|
||||
if pip:
|
||||
out.append('$PYBIN -m pip --version || $PYBIN -m ensurepip --upgrade')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Node.js (nvm-style from binary) ==========================
|
||||
class NodeJS(Generator):
|
||||
id = "nodejs"
|
||||
title = "Node.js 任意版本 (官方二进制)"
|
||||
category = "runtimes"
|
||||
icon = "🟢"
|
||||
tags = ["nodejs", "node", "npm"]
|
||||
description = "从 nodejs.org 下载预编译二进制,可选装 yarn / pnpm / nvm。"
|
||||
fields = [
|
||||
Field("version", "Node 版本", "select", default="24",
|
||||
options=["18", "20", "22", "24", "26"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/nodejs"),
|
||||
Field("symlink_bin", "软链 bin 目录", "checkbox", default="yes"),
|
||||
Field("install_yarn", "安装 yarn", "checkbox", default="yes"),
|
||||
Field("install_pnpm", "安装 pnpm", "checkbox", default="yes"),
|
||||
Field("npm_registry", "NPM Registry", "text",
|
||||
default="https://registry.npmmirror.com",
|
||||
help="默认走国内镜像,留空 = 官方。"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "20")
|
||||
d = p.get("install_dir", "/opt/nodejs")
|
||||
sym = bool_str(p.get("symlink_bin", True))
|
||||
yarn = bool_str(p.get("install_yarn", True))
|
||||
pnpm = bool_str(p.get("install_pnpm", True))
|
||||
reg = p.get("npm_registry", "https://registry.npmmirror.com")
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Node.js v' + ver + '..."')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -o node.tar.xz https://nodejs.org/dist/v' + ver + '.x/node-v' + ver + '.x-linux-x64.tar.xz')
|
||||
out.append('rm -rf ' + d)
|
||||
out.append('mkdir -p ' + d)
|
||||
out.append('tar -xJf node.tar.xz --strip-components=1 -C ' + d)
|
||||
out.append('rm -f node.tar.xz')
|
||||
if sym:
|
||||
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
|
||||
if reg:
|
||||
out.append('npm config set registry ' + quote(reg))
|
||||
if yarn:
|
||||
out.append('npm install -g yarn')
|
||||
if pnpm:
|
||||
out.append('npm install -g pnpm')
|
||||
out.append('node --version')
|
||||
out.append('npm --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== GCC (from source) ==========================
|
||||
class GCC(Generator):
|
||||
id = "gcc"
|
||||
title = "GCC 任意版本 (源码编译)"
|
||||
category = "runtimes"
|
||||
icon = "🛠️"
|
||||
tags = ["gcc", "compiler", "source"]
|
||||
description = "从 gcc.gnu.org 下载源码,编译并安装到 /opt/gcc-<ver>。"
|
||||
warnings = [
|
||||
"GCC 12+ 编译需 30-60 分钟,需要约 5GB 磁盘。",
|
||||
"建议另装 'system gcc' (apt/yum) 留作系统默认,本工具不替换系统 gcc。",
|
||||
]
|
||||
fields = [
|
||||
Field("version", "GCC 版本", "select", default="13.2.0",
|
||||
options=["8.5.0", "9.5.0", "10.5.0", "11.4.0", "12.3.0",
|
||||
"13.2.0", "14.1.0"]),
|
||||
Field("languages", "支持语言", "text", default="c,c++,fortran",
|
||||
help="逗号分隔,如 c,c++,objc,fortran,go"),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/gcc",
|
||||
help="最终 = $install_dir/<ver>"),
|
||||
Field("enable_lto", "启用 LTO (慢但快 ~10%)", "checkbox", default="no"),
|
||||
Field("enable_libsan", "启用 Sanitizers (ASan/TSan)", "checkbox", default="no"),
|
||||
Field("make_jobs", "make -j 任务数", "text", default="$(nproc)",
|
||||
help="默认 = CPU 核数,改小可降低内存峰值。"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "13.2.0")
|
||||
langs = p.get("languages", "c,c++,fortran")
|
||||
d = p.get("install_dir", "/opt/gcc")
|
||||
lto = " --enable-lto" if bool_str(p.get("enable_lto")) else ""
|
||||
libsan = " --enable-libsanitizer" if bool_str(p.get("enable_libsan")) else ""
|
||||
jobs = p.get("make_jobs", "$(nproc)")
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing GCC ' + ver + ' from source..."')
|
||||
out.append('$PKG_INSTALL wget gcc gcc-c++ make texinfo bison flex')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://ftp.gnu.org/gnu/gcc/gcc-' + ver + '/gcc-' + ver + '.tar.xz')
|
||||
out.append('tar -xJf gcc-' + ver + '.tar.xz')
|
||||
out.append('cd gcc-' + ver)
|
||||
out.append('./contrib/download_prerequisites')
|
||||
out.append('mkdir -p build && cd build')
|
||||
out.append('../configure --prefix=' + d + '/' + ver +
|
||||
' --enable-languages=' + langs +
|
||||
' --disable-multilib --with-system-zlib' + lto + libsan)
|
||||
out.append('make -j' + jobs)
|
||||
out.append('make install')
|
||||
out.append('cd /tmp && rm -rf gcc-' + ver + ' gcc-' + ver + '.tar.xz build')
|
||||
out.append('log "Verifying..."')
|
||||
out.append(d + '/' + ver + '/bin/gcc --version')
|
||||
out.append('echo "Add to PATH: export PATH=' + d + '/' + ver + '/bin:$PATH"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Make ==========================
|
||||
class Make(Generator):
|
||||
id = "make"
|
||||
title = "GNU Make 任意版本 (源码编译)"
|
||||
category = "runtimes"
|
||||
icon = "🔨"
|
||||
tags = ["make", "build", "gnu"]
|
||||
description = "从 gnu.org 下载 make 源码并安装到 /opt。"
|
||||
fields = [
|
||||
Field("version", "Make 版本", "select", default="4.4.1",
|
||||
options=["4.2.1", "4.3", "4.4", "4.4.1"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/make"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "4.4.1")
|
||||
d = p.get("install_dir", "/opt/make")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing make ' + ver + '..."')
|
||||
out.append('$PKG_INSTALL gcc make')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://ftp.gnu.org/gnu/make/make-' + ver + '.tar.gz')
|
||||
out.append('tar -xzf make-' + ver + '.tar.gz')
|
||||
out.append('cd make-' + ver)
|
||||
out.append('./configure --prefix=' + d + '/' + ver)
|
||||
out.append('make -j"$(nproc)"')
|
||||
out.append('make install')
|
||||
out.append('cd /tmp && rm -rf make-' + ver + ' make-' + ver + '.tar.gz')
|
||||
out.append(d + '/' + ver + '/bin/make --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== CMake ==========================
|
||||
class CMake(Generator):
|
||||
id = "cmake"
|
||||
title = "CMake 任意版本 (官方脚本安装)"
|
||||
category = "runtimes"
|
||||
icon = "🧱"
|
||||
tags = ["cmake", "build", "makefile"]
|
||||
description = "从 cmake.org 下载官方二进制,安装到 /opt。"
|
||||
fields = [
|
||||
Field("version", "CMake 版本", "select", default="3.30.0",
|
||||
options=["3.20.0", "3.22.0", "3.24.0", "3.25.0", "3.26.0",
|
||||
"3.27.0", "3.28.0", "3.29.0", "3.30.0"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/cmake"),
|
||||
Field("symlink_bin", "软链到 /usr/local/bin", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "3.30.0")
|
||||
d = p.get("install_dir", "/opt/cmake")
|
||||
sym = bool_str(p.get("symlink_bin", True))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing CMake ' + ver + '..."')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://github.com/Kitware/CMake/releases/download/v' + ver + '/cmake-' + ver + '-linux-x86_64.tar.gz')
|
||||
out.append('rm -rf ' + d)
|
||||
out.append('mkdir -p ' + d)
|
||||
out.append('tar -xzf cmake-' + ver + '-linux-x86_64.tar.gz --strip-components=1 -C ' + d)
|
||||
out.append('rm -f cmake-' + ver + '-linux-x86_64.tar.gz')
|
||||
if sym:
|
||||
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
|
||||
out.append('cmake --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Go ==========================
|
||||
class GoLang(Generator):
|
||||
id = "golang"
|
||||
title = "Go 任意版本 (官方二进制)"
|
||||
category = "runtimes"
|
||||
icon = "🐹"
|
||||
tags = ["go", "golang"]
|
||||
description = "从 go.dev 下载 Go 官方二进制,设置 GOPROXY 国内镜像。"
|
||||
fields = [
|
||||
Field("version", "Go 版本", "select", default="1.22.5",
|
||||
options=["1.18.10", "1.19.13", "1.20.14", "1.21.12", "1.22.5", "1.23.0"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/go"),
|
||||
Field("goproxy", "GOPROXY", "text", default="https://goproxy.cn,direct",
|
||||
help="默认 goproxy.cn,留空 = GOPROXY=off"),
|
||||
Field("symlink_bin", "软链到 /usr/local/bin", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "1.22.5")
|
||||
d = p.get("install_dir", "/opt/go")
|
||||
proxy = p.get("goproxy", "https://goproxy.cn,direct")
|
||||
sym = bool_str(p.get("symlink_bin", True))
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Go ' + ver + '..."')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://go.dev/dl/go' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('rm -rf ' + d)
|
||||
out.append('tar -C ' + d.rsplit('/', 1)[0] + ' -xzf go' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('mv ' + d.rsplit('/', 1)[0] + '/go ' + d)
|
||||
out.append('rm -f go' + ver + '.linux-amd64.tar.gz')
|
||||
if sym:
|
||||
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
|
||||
if proxy:
|
||||
out.append('go env -w GOPROXY=' + quote(proxy))
|
||||
out.append('go env -w GOSUMDB=sum.golang.google.cn')
|
||||
out.append('go version')
|
||||
out.append('echo "Add to PATH: export PATH=' + d + '/bin:$PATH"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== OpenJDK ==========================
|
||||
class Java(Generator):
|
||||
id = "java"
|
||||
title = "OpenJDK 任意版本 (官方二进制)"
|
||||
category = "runtimes"
|
||||
icon = "☕"
|
||||
tags = ["java", "jdk", "openjdk"]
|
||||
description = "从 Adoptium 仓库下载 OpenJDK 预编译版本,无需 apt 仓库。"
|
||||
fields = [
|
||||
Field("version", "JDK 版本", "select", default="21",
|
||||
options=["8", "11", "17", "21"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/jdk"),
|
||||
Field("symlink_bin", "软链到 /usr/local/bin", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "21")
|
||||
d = p.get("install_dir", "/opt/jdk")
|
||||
sym = bool_str(p.get("symlink_bin", True))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing OpenJDK ' + ver + '..."')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -o jdk.tar.gz "https://download.java.net/java/GA/jdk' + ver + '.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-' + ver + '.0.2_linux-x64_bin.tar.gz"')
|
||||
out.append('rm -rf ' + d)
|
||||
out.append('mkdir -p ' + d)
|
||||
out.append('tar -xzf jdk.tar.gz --strip-components=1 -C ' + d)
|
||||
out.append('rm -f jdk.tar.gz')
|
||||
if sym:
|
||||
out.append('update-alternatives --install /usr/bin/java java ' + d + '/bin/java 9999 || true')
|
||||
out.append('update-alternatives --install /usr/bin/javac javac ' + d + '/bin/javac 9999 || true')
|
||||
out.append('for b in ' + d + '/bin/*; do ln -sf "$b" /usr/local/bin/"$(basename "$b")"; done')
|
||||
out.append('export JAVA_HOME=' + d)
|
||||
out.append(d + '/bin/java --version')
|
||||
out.append('echo "Add to PATH: export PATH=' + d + '/bin:$PATH"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== PHP ==========================
|
||||
class PHP(Generator):
|
||||
id = "php"
|
||||
title = "PHP 任意版本 (源码编译)"
|
||||
category = "runtimes"
|
||||
icon = "🐘"
|
||||
tags = ["php", "apache", "nginx", "fpm"]
|
||||
description = "从 php.net 编译 PHP,启用 fpm、常用扩展。"
|
||||
fields = [
|
||||
Field("version", "PHP 版本", "select", default="8.3.10",
|
||||
options=["7.4.33", "8.0.30", "8.1.29", "8.2.22", "8.3.10"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/php"),
|
||||
Field("enable_fpm", "启用 PHP-FPM", "checkbox", default="yes"),
|
||||
Field("extensions", "编译扩展", "text",
|
||||
default="mysqli,pdo,pdo_mysql,gd,mbstring,curl,xml,zip,intl,opcache,bcmath"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "8.3.10")
|
||||
d = p.get("install_dir", "/opt/php")
|
||||
fpm = bool_str(p.get("enable_fpm", True))
|
||||
exts = p.get("extensions", "mysqli,pdo,pdo_mysql,gd,mbstring,curl,xml,zip,intl,opcache,bcmath")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing PHP ' + ver + ' from source..."')
|
||||
out.append('$PKG_INSTALL gcc make libxml2-dev libssl-dev libcurl4-openssl-dev \\\n'
|
||||
' libonig-dev libzip-dev libicu-dev libsqlite3-dev libpng-dev libjpeg-dev \\\n'
|
||||
' libfreetype6-dev libwebp-dev libxslt1-dev libreadline-dev 2>/dev/null \\\n'
|
||||
' || $PKG_INSTALL gcc make libxml2-devel openssl-devel libcurl-devel \\\n'
|
||||
' libonig-devel libzip-devel libicu-devel sqlite-devel libpng-devel \\\n'
|
||||
' libjpeg-devel freetype-devel libwebp-devel libxslt-devel readline-devel')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://www.php.net/distributions/php-' + ver + '.tar.gz')
|
||||
out.append('tar -xzf php-' + ver + '.tar.gz')
|
||||
out.append('cd php-' + ver)
|
||||
cfg = './configure --prefix=' + d + '/' + ver
|
||||
cfg += ' --with-config-file-path=' + d + '/' + ver + '/etc'
|
||||
cfg += ' --with-config-file-scan-dir=' + d + '/' + ver + '/etc/php.d'
|
||||
cfg += ' --enable-mbstring --enable-fpm' if fpm else ''
|
||||
cfg += ' --with-curl --with-openssl --with-zip --with-zlib'
|
||||
cfg += ' --with-pdo-mysql --with-mysqli'
|
||||
cfg += ' --enable-opcache'
|
||||
cfg += ' --enable-bcmath --enable-intl --enable-pcntl --enable-sockets'
|
||||
for e in exts.split(','):
|
||||
e = e.strip()
|
||||
if not e:
|
||||
continue
|
||||
if e in ("mysqli", "pdo_mysql", "pdo", "gd", "mbstring", "curl", "xml",
|
||||
"zip", "intl", "opcache", "bcmath", "fpm"):
|
||||
continue
|
||||
out.append(cfg)
|
||||
out.append('make -j"$(nproc)"')
|
||||
out.append('make install')
|
||||
out.append('mkdir -p ' + d + '/' + ver + '/etc/php.d')
|
||||
out.append('cp php.ini-production ' + d + '/' + ver + '/etc/php.ini')
|
||||
out.append('cp sapi/fpm/php-fpm.conf ' + d + '/' + ver + '/etc/ || true')
|
||||
out.append('cp sapi/fpm/www.conf.default ' + d + '/' + ver + '/etc/www.conf || true')
|
||||
out.append('cd /tmp && rm -rf php-' + ver + ' php-' + ver + '.tar.gz')
|
||||
out.append(d + '/' + ver + '/bin/php --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Ruby ==========================
|
||||
class Ruby(Generator):
|
||||
id = "ruby"
|
||||
title = "Ruby 任意版本 (源码编译)"
|
||||
category = "runtimes"
|
||||
icon = "💎"
|
||||
tags = ["ruby", "rails"]
|
||||
description = "从 cache.ruby-lang.org 编译 Ruby,启用 readline/openssl/zlib。"
|
||||
fields = [
|
||||
Field("version", "Ruby 版本", "select", default="3.3.3",
|
||||
options=["2.7.8", "3.0.7", "3.1.6", "3.2.4", "3.3.3"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/ruby"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "3.3.3")
|
||||
d = p.get("install_dir", "/opt/ruby")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Ruby ' + ver + '..."')
|
||||
out.append('$PKG_INSTALL gcc make build-essential libssl-dev libreadline-dev zlib1g-dev \\\n'
|
||||
' libyaml-dev libgmp-dev libffi-dev libgdbm-dev libdb-dev libncurses5-dev 2>/dev/null \\\n'
|
||||
' || $PKG_INSTALL gcc make openssl-devel readline-devel zlib-devel \\\n'
|
||||
' libyaml-devel gmp-devel libffi-devel gdbm-devel ncurses-devel')
|
||||
out.append('cd /tmp')
|
||||
out.append('curl -fsSL -O https://cache.ruby-lang.org/pub/ruby/' + ver.split('.')[0] + '.' + ver.split('.')[1] + '/ruby-' + ver + '.tar.gz')
|
||||
out.append('tar -xzf ruby-' + ver + '.tar.gz')
|
||||
out.append('cd ruby-' + ver)
|
||||
out.append('./configure --prefix=' + d + '/' + ver + ' --enable-shared --disable-install-doc')
|
||||
out.append('make -j"$(nproc)"')
|
||||
out.append('make install')
|
||||
out.append('cd /tmp && rm -rf ruby-' + ver + ' ruby-' + ver + '.tar.gz')
|
||||
out.append(d + '/' + ver + '/bin/ruby --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Rust ==========================
|
||||
class RustLang(Generator):
|
||||
id = "rust"
|
||||
title = "Rust 工具链 (rustup)"
|
||||
category = "runtimes"
|
||||
icon = "🦀"
|
||||
tags = ["rust", "rustup", "cargo"]
|
||||
description = "通过 rustup 安装 Rust,可指定 nightly / stable 及 toolchain 路径。"
|
||||
fields = [
|
||||
Field("channel", "Channel", "select", default="stable",
|
||||
options=["stable", "beta", "nightly"]),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/rust"),
|
||||
Field("default_toolchain", "默认 toolchain", "text", default="stable"),
|
||||
Field("install_components", "组件列表", "text",
|
||||
default="rustfmt,clippy,rust-src,rust-analyzer"),
|
||||
Field("mirror", "rsproxy 国内镜像", "checkbox", default="yes",
|
||||
help="启用后会写 RUSTUP_DIST_SERVER 环境变量。"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ch = p.get("channel", "stable")
|
||||
d = p.get("install_dir", "/opt/rust")
|
||||
tool = p.get("default_toolchain", "stable")
|
||||
comps = p.get("install_components", "rustfmt,clippy,rust-src,rust-analyzer")
|
||||
mirror = bool_str(p.get("mirror", True))
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing rustup + ' + ch + '..."')
|
||||
out.append('command -v gcc >/dev/null || $PKG_INSTALL gcc make build-essential \\\n'
|
||||
' || $PKG_INSTALL gcc make')
|
||||
if mirror:
|
||||
out.append('export RUSTUP_DIST_SERVER=https://rsproxy.cn')
|
||||
out.append('export RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup')
|
||||
out.append('curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | '
|
||||
'sh -s -- -y --default-toolchain none --no-modify-path --prefix=' + d)
|
||||
out.append('. ' + d + '/env')
|
||||
out.append('rustup default ' + tool)
|
||||
out.append('rustup toolchain install ' + ch)
|
||||
for c in comps.split(','):
|
||||
c = c.strip()
|
||||
if c:
|
||||
out.append('rustup component add ' + c + ' --toolchain ' + ch)
|
||||
out.append('rustc --version')
|
||||
out.append('cargo --version')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== PostgreSQL ==========================
|
||||
class PostgreSQL(Generator):
|
||||
id = "postgresql"
|
||||
title = "PostgreSQL 任意版本 (官方 apt/yum 源)"
|
||||
category = "databases"
|
||||
icon = "🐘"
|
||||
tags = ["postgresql", "pg", "database"]
|
||||
description = "从 PostgreSQL Global Development Group 官方源安装任意版本。"
|
||||
fields = [
|
||||
Field("version", "PG 版本", "select", default="16",
|
||||
options=["12", "13", "14", "15", "16"]),
|
||||
Field("port", "端口", "number", default="5432", min_=1, max_=65535),
|
||||
Field("listen_addresses", "listen_addresses", "text", default="*"),
|
||||
Field("admin_user", "超级用户", "text", default="postgres"),
|
||||
Field("admin_password", "超级用户密码", "password", default="changeme"),
|
||||
Field("data_dir", "数据目录", "text", default="/var/lib/pgsql/data"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "16")
|
||||
port = str(p.get("port", "5432"))
|
||||
listen = p.get("listen_addresses", "*")
|
||||
user = p.get("admin_user", "postgres")
|
||||
pwd = p.get("admin_password", "changeme")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing PostgreSQL ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL wget gnupg lsb-release ca-certificates\n'
|
||||
' curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pgdg.gpg\n'
|
||||
' echo "deb [signed-by=/usr.share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list\n'
|
||||
' apt-get update\n'
|
||||
' $PKG_INSTALL postgresql-' + ver + ' postgresql-client-' + ver + ' postgresql-contrib-' + ver + '\n'
|
||||
' ;;\n'
|
||||
' yum|dnf)\n'
|
||||
' $PKG_INSTALL https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm\n'
|
||||
' $PKG_INSTALL postgresql' + ver + '-server postgresql' + ver + '-contrib\n'
|
||||
' /usr/pgsql-' + ver + '/bin/postgresql-' + ver + '-setup initdb\n'
|
||||
' ;;\n'
|
||||
'esac')
|
||||
out.append('systemctl enable --now postgresql')
|
||||
out.append('sleep 2')
|
||||
out.append('sudo -u postgres psql -c "ALTER USER ' + user + ' WITH PASSWORD \'' + pwd + '\';"')
|
||||
out.append('echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/' + ver + '/main/pg_hba.conf 2>/dev/null || true')
|
||||
out.append('echo "host all all 0.0.0.0/0 md5" >> /var/lib/pgsql/data/pg_hba.conf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^#listen_addresses.*/listen_addresses = \'' + listen + '\'/" /etc/postgresql/' + ver + '/main/postgresql.conf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^#listen_addresses.*/listen_addresses = \'' + listen + '\'/" /var/lib/pgsql/data/postgresql.conf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^port.*/port = ' + port + '/" /etc/postgresql/' + ver + '/main/postgresql.conf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^port.*/port = ' + port + '/" /var/lib/pgsql/data/postgresql.conf 2>/dev/null || true')
|
||||
out.append('systemctl restart postgresql')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== MySQL ==========================
|
||||
class MySQL(Generator):
|
||||
id = "mysql"
|
||||
title = "MySQL / MariaDB 任意版本"
|
||||
category = "databases"
|
||||
icon = "🐬"
|
||||
tags = ["mysql", "mariadb", "database"]
|
||||
description = "支持 MySQL 5.7/8.0/8.4 或 MariaDB 10.x,自动初始化 root 密码。"
|
||||
fields = [
|
||||
Field("variant", "变体", "select", default="mysql",
|
||||
options=["mysql", "mariadb"]),
|
||||
Field("version", "版本", "select", default="8.0",
|
||||
options=["5.7", "8.0", "8.4", "10.11", "11.4"]),
|
||||
Field("port", "端口", "number", default="3306", min_=1, max_=65535),
|
||||
Field("root_password", "root 密码", "password", default="changeme"),
|
||||
Field("bind_address", "Bind 地址", "text", default="0.0.0.0"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
var = p.get("variant", "mysql")
|
||||
ver = p.get("version", "8.0")
|
||||
port = str(p.get("port", "3306"))
|
||||
pwd = p.get("root_password", "changeme")
|
||||
bind = p.get("bind_address", "0.0.0.0")
|
||||
out = [bash_header(self.title)]
|
||||
if var == "mysql":
|
||||
out.append('log "Installing MySQL ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL wget gnupg lsb-release\n'
|
||||
' wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb\n'
|
||||
' DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.29-1_all.deb || true\n'
|
||||
' apt-get update\n'
|
||||
' $PKG_INSTALL mysql-server mysql-client\n'
|
||||
' ;;\n'
|
||||
' yum|dnf)\n'
|
||||
' $PKG_INSTALL https://dev.mysql.com/get/mysql80-community-release-el$(rpm -E %{rhel})-1.noarch.rpm\n'
|
||||
' $PKG_INSTALL mysql-community-server mysql-community-client\n'
|
||||
' ;;\n'
|
||||
'esac')
|
||||
else:
|
||||
out.append('log "Installing MariaDB ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get) $PKG_INSTALL mariadb-server mariadb-client ;;\n'
|
||||
' yum|dnf) $PKG_INSTALL mariadb-server mariadb ;;\n'
|
||||
'esac')
|
||||
out.append('systemctl enable --now mysql || systemctl enable --now mariadb')
|
||||
out.append('sleep 3')
|
||||
out.append('mysql -u root -e "ALTER USER \'root\'@\'localhost\' IDENTIFIED BY \'' + pwd + '\';" 2>/dev/null || true')
|
||||
out.append('mysql -u root -p\'' + pwd + '\' -e "CREATE USER IF NOT EXISTS \'root\'@\'%\' IDENTIFIED BY \'' + pwd + '\';" 2>/dev/null || true')
|
||||
out.append('mysql -u root -p\'' + pwd + '\' -e "GRANT ALL ON *.* TO \'root\'@\'%\' WITH GRANT OPTION; FLUSH PRIVILEGES;" 2>/dev/null || true')
|
||||
out.append('sed -i "s/^bind-address.*/bind-address = ' + bind + '/" /etc/mysql/mysql.conf.d/mysqld.cnf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^bind-address.*/bind-address = ' + bind + '/" /etc/my.cnf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^port.*/port = ' + port + '/" /etc/mysql/mysql.conf.d/mysqld.cnf 2>/dev/null || true')
|
||||
out.append('sed -i "s/^port.*/port = ' + port + '/" /etc/my.cnf 2>/dev/null || true')
|
||||
out.append('systemctl restart mysql || systemctl restart mariadb')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== MongoDB ==========================
|
||||
class MongoDB(Generator):
|
||||
id = "mongodb"
|
||||
title = "MongoDB 任意版本"
|
||||
category = "databases"
|
||||
icon = "🍃"
|
||||
tags = ["mongodb", "nosql", "document"]
|
||||
description = "从 MongoDB 官方源安装任意版本,启用 replica set。"
|
||||
fields = [
|
||||
Field("version", "MongoDB 版本", "select", default="7.0",
|
||||
options=["4.4", "5.0", "6.0", "7.0"]),
|
||||
Field("port", "端口", "number", default="27017", min_=1, max_=65535),
|
||||
Field("bind_ip", "Bind IP", "text", default="0.0.0.0"),
|
||||
Field("enable_auth", "启用鉴权", "checkbox", default="yes"),
|
||||
Field("root_user", "root 用户", "text", default="root"),
|
||||
Field("root_password", "root 密码", "password", default="changeme"),
|
||||
Field("repl_set", "副本集名 (留空=单点)", "text", default=""),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ver = p.get("version", "7.0")
|
||||
port = str(p.get("port", "27017"))
|
||||
bind = p.get("bind_ip", "0.0.0.0")
|
||||
auth = bool_str(p.get("enable_auth", True))
|
||||
user = p.get("root_user", "root")
|
||||
pwd = p.get("root_password", "changeme")
|
||||
repl = p.get("repl_set", "").strip()
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing MongoDB ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL wget gnupg curl\n'
|
||||
' curl -fsSL https://www.mongodb.org/static/pgp/server-' + ver + '.asc | gpg --dearmor -o /usr/share/keyrings/mongodb.gpg\n'
|
||||
' echo "deb [signed-by=/usr/share/keyrings/mongodb.gpg] https://repo.mongodb.org/apt/ubuntu $(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)/mongodb-org/' + ver + ' multiverse" > /etc/apt/sources.list.d/mongodb-org-' + ver + '.list\n'
|
||||
' apt-get update\n'
|
||||
' $PKG_INSTALL mongodb-org\n'
|
||||
' ;;\n'
|
||||
' yum|dnf)\n'
|
||||
' cat > /etc/yum.repos.d/mongodb-org-' + ver + '.repo <<REPO_EOF\n'
|
||||
'[mongodb-org-' + ver + ']\n'
|
||||
'name=MongoDB Repository\n'
|
||||
'baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/' + ver + '/x86_64/\n'
|
||||
'gpgcheck=1\n'
|
||||
'enabled=1\n'
|
||||
'gpgkey=https://www.mongodb.org/static/pgp/server-' + ver + '.asc\n'
|
||||
'REPO_EOF\n'
|
||||
' $PKG_INSTALL mongodb-org\n'
|
||||
' ;;\n'
|
||||
'esac')
|
||||
out.append('systemctl enable --now mongod')
|
||||
out.append('sleep 3')
|
||||
if auth:
|
||||
out.append('log "Creating root user..."')
|
||||
out.append('mongosh --quiet --eval \''
|
||||
'db.getSiblingDB("admin").createUser({user:"' + user + '",pwd:"' + pwd + '",roles:[{role:"root",db:"admin"}]})\' || warn "auth may already be set"')
|
||||
out.append('sed -i "s/^#security:/{ security: { authorization: \"enabled\" },/g" /etc/mongod.conf')
|
||||
out.append('sed -i "s/^ bindIp:.*/ bindIp: ' + bind + '/" /etc/mongod.conf')
|
||||
out.append('grep -q "^ port:" /etc/mongod.conf || sed -i "/^ bindIp:/a\\ port: ' + port + '" /etc/mongod.conf')
|
||||
if repl:
|
||||
out.append('grep -q "^replication:" /etc/mongod.conf || cat >> /etc/mongod.conf <<EOF\\nreplication:\\n replSetName: ' + repl + '\\nEOF')
|
||||
out.append('systemctl restart mongod')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# Register all
|
||||
for _g in [Python, NodeJS, GCC, Make, CMake, GoLang, Java, PHP, Ruby, RustLang,
|
||||
PostgreSQL, MySQL, MongoDB]:
|
||||
register(_g())
|
||||
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
System / OS-level tools.
|
||||
- Firewall (iptables / firewalld / ufw)
|
||||
- Swap setup
|
||||
- Time sync (chrony)
|
||||
- SSH hardening
|
||||
- Hostname & hosts
|
||||
- Disk / fstab
|
||||
- Limits (ulimit)
|
||||
- Sysctl tuning
|
||||
"""
|
||||
from . import register, Generator, Field
|
||||
from . import bash_header, quote, bool_str, yes
|
||||
|
||||
|
||||
# ========================== Firewall ==========================
|
||||
class Firewall(Generator):
|
||||
id = "firewall"
|
||||
title = "防火墙 (iptables / firewalld / ufw)"
|
||||
category = "system"
|
||||
icon = "🧱"
|
||||
tags = ["firewall", "iptables", "firewalld", "ufw"]
|
||||
description = "根据发行版自动选择 firewalld (RHEL) 或 ufw (Debian),开放指定端口。"
|
||||
warnings = ["修改防火墙可能断连,建议在带外有 console 的情况下执行。"]
|
||||
fields = [
|
||||
Field("ports", "要开放的端口 (TCP)", "text", default="22,80,443",
|
||||
help="逗号或空格分隔,如 22 80 443 或 22,80,443。"),
|
||||
Field("udp_ports", "UDP 端口", "text", default=""),
|
||||
Field("trusted_ip", "完全信任的 IP/CIDR", "text", default="",
|
||||
help="填 192.168.1.0/24 可对该网段放行所有端口。"),
|
||||
Field("ssh_port", "SSH 端口", "number", default="22", min_=1, max_=65535),
|
||||
Field("deny_incoming", "默认拒绝入站", "checkbox", default="yes"),
|
||||
Field("allow_outgoing", "允许所有出站", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ports = p.get("ports", "22,80,443").replace(",", " ").split()
|
||||
udps = p.get("udp_ports", "").replace(",", " ").split() if p.get("udp_ports") else []
|
||||
trusted = p.get("trusted_ip", "").strip()
|
||||
ssh = str(p.get("ssh_port", "22"))
|
||||
deny_in = bool_str(p.get("deny_incoming", True))
|
||||
allow_out = bool_str(p.get("allow_outgoing", True))
|
||||
ports_str = " ".join([quote(p_) for p_ in ports])
|
||||
udps_str = " ".join([quote(p_) for p_ in udps]) if udps else ""
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Configuring firewall..."')
|
||||
# Build the case/esac block as a list of lines then join — avoids the
|
||||
# "implicit string concatenation" gotcha when mixing `+` with bare strings.
|
||||
fw = []
|
||||
fw.append('case "$DISTRO_ID" in')
|
||||
fw.append(' ubuntu|debian)')
|
||||
fw.append(' $PKG_INSTALL ufw')
|
||||
fw.append(' ufw --force reset')
|
||||
fw.append(' ufw default ' + ('deny' if deny_in else 'allow') + ' incoming')
|
||||
fw.append(' ufw default ' + ('allow' if allow_out else 'deny') + ' outgoing')
|
||||
for p_ in ports:
|
||||
if p_ != ssh:
|
||||
fw.append(f' ufw allow {p_}/tcp')
|
||||
for p_ in udps:
|
||||
fw.append(f' ufw allow {p_}/udp')
|
||||
if trusted:
|
||||
fw.append(f' ufw allow from {trusted} to any port 0:65535')
|
||||
fw.append(' ufw allow ' + ssh + '/tcp comment "ssh"')
|
||||
fw.append(' ufw --force enable')
|
||||
fw.append(' ;;')
|
||||
fw.append(' centos|rhel|rocky|almalinux|ol|fedora)')
|
||||
fw.append(' $PKG_INSTALL firewalld')
|
||||
fw.append(' systemctl enable --now firewalld')
|
||||
for p_ in ports:
|
||||
fw.append(f' firewall-cmd --permanent --add-port={p_}/tcp')
|
||||
for p_ in udps:
|
||||
fw.append(f' firewall-cmd --permanent --add-port={p_}/udp')
|
||||
if trusted:
|
||||
fw.append(f' firewall-cmd --permanent --add-source={trusted}')
|
||||
fw.append(' firewall-cmd --permanent --add-port=0-65535/tcp')
|
||||
fw.append(' firewall-cmd --permanent --add-port=0-65535/udp')
|
||||
fw.append(' firewall-cmd --reload')
|
||||
fw.append(' ;;')
|
||||
fw.append(' *)')
|
||||
fw.append(' warn "Auto-firewall not supported on $DISTRO_ID, falling back to iptables"')
|
||||
fw.append(' $PKG_INSTALL iptables-persistent || $PKG_INSTALL iptables-services')
|
||||
fw.append(' iptables -P INPUT ' + ('DROP' if deny_in else 'ACCEPT'))
|
||||
fw.append(' iptables -A INPUT -i lo -j ACCEPT')
|
||||
fw.append(' iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT')
|
||||
for p_ in ports:
|
||||
fw.append(f' iptables -A INPUT -p tcp --dport {p_} -j ACCEPT')
|
||||
if trusted:
|
||||
fw.append(f' iptables -A INPUT -s {trusted} -j ACCEPT')
|
||||
fw.append(' netfilter-persistent save || service iptables save')
|
||||
fw.append(' ;;')
|
||||
fw.append('esac')
|
||||
out.append('\n'.join(fw))
|
||||
out.append('log "Done."')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Swap ==========================
|
||||
class Swap(Generator):
|
||||
id = "swap"
|
||||
title = "Swap 交换分区/文件"
|
||||
category = "system"
|
||||
icon = "💾"
|
||||
tags = ["swap"]
|
||||
description = "创建 swap 文件并启用。"
|
||||
fields = [
|
||||
Field("size_gb", "大小 (GB)", "number", default="2", min_=1, max_=128),
|
||||
Field("path", "Swap 文件路径", "text", default="/swapfile"),
|
||||
Field("swappiness", "Swappiness", "number", default="10", min_=0, max_=100),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
sz = str(p.get("size_gb", "2"))
|
||||
path = p.get("path", "/swapfile")
|
||||
sw = str(p.get("swappiness", "10"))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Creating ' + sz + 'GB swap at ' + path + '..."')
|
||||
out.append('if [ -f "' + path + '" ]; then swapoff ' + path + ' && rm -f ' + path + '; fi')
|
||||
out.append('fallocate -l ' + sz + 'G ' + path + ' || dd if=/dev/zero of=' + path + ' bs=1M count=$(((' + sz + '*1024))) status=none')
|
||||
out.append('chmod 600 ' + path)
|
||||
out.append('mkswap ' + path)
|
||||
out.append('swapon ' + path)
|
||||
out.append('grep -q "' + path + ' " /etc/fstab || echo "' + path + ' none swap sw 0 0" >> /etc/fstab')
|
||||
out.append('sysctl -w vm.swappiness=' + sw)
|
||||
out.append('grep -q "vm.swappiness" /etc/sysctl.conf || echo "vm.swappiness=' + sw + '" >> /etc/sysctl.conf')
|
||||
out.append('swapon --show')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Time sync (chrony) ==========================
|
||||
class Chrony(Generator):
|
||||
id = "chrony"
|
||||
title = "时间同步 (chrony)"
|
||||
category = "system"
|
||||
icon = "🕐"
|
||||
tags = ["time", "ntp", "chrony"]
|
||||
description = "安装并配置 chrony,使用国内 NTP 源。"
|
||||
fields = [
|
||||
Field("servers", "NTP 服务器", "textarea",
|
||||
default="ntp.aliyun.com\nntp1.aliyun.com\nntp2.aliyun.com\ntime.cloudflare.com",
|
||||
help="一行一个。"),
|
||||
Field("makestep_threshold", "makestep 阈值 (秒)", "text", default="1.0 3"),
|
||||
Field("allow_subnet", "允许同步的子网", "text", default="192.168.0.0/16",
|
||||
help="仅本机同步: 留空 = 127.0.0.0/8"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
srvs = [s.strip() for s in p.get("servers", "").splitlines() if s.strip()]
|
||||
ms = p.get("makestep_threshold", "1.0 3")
|
||||
allow = p.get("allow_subnet", "192.168.0.0/16").strip()
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing chrony..."')
|
||||
out.append('$PKG_INSTALL chrony')
|
||||
out.append('cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.bak.$(date +%s) || true')
|
||||
out.append('cat > /etc/chrony/chrony.conf <<CFG_EOF\n'
|
||||
'pool ntp.ubuntu.org maxsources 4\n'
|
||||
'pool 0.pool.ntp.org maxsources 1\n'
|
||||
'pool 1.pool.ntp.org maxsources 1\n'
|
||||
'pool 2.pool.ntp.org maxsources 1\n'
|
||||
'makestep ' + ms + '\n'
|
||||
'rtcsync\n'
|
||||
'logdir /var/log/chrony\n'
|
||||
+ ('allow ' + allow + '\n' if allow else 'allow 127.0.0.1\n') +
|
||||
'CFG_EOF')
|
||||
for s in srvs:
|
||||
out.append('echo "pool ' + s + ' iburst maxsources 4" >> /etc/chrony/chrony.conf')
|
||||
out.append('systemctl enable --now chrony')
|
||||
out.append('sleep 2')
|
||||
out.append('chronyc tracking')
|
||||
out.append('chronyc sources -v')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== SSH Hardening ==========================
|
||||
class SSHHardening(Generator):
|
||||
id = "ssh-hardening"
|
||||
title = "SSH 服务加固"
|
||||
category = "security"
|
||||
icon = "🔐"
|
||||
tags = ["ssh", "security", "hardening"]
|
||||
description = "修改 sshd_config:禁 root 登录、改端口、密钥认证、登录横幅。"
|
||||
warnings = [
|
||||
"修改前请确保有可登录的用户和密钥,否则会锁死!",
|
||||
"建议保留当前 SSH 会话,新开一个窗口测试登录成功后再关闭。",
|
||||
]
|
||||
fields = [
|
||||
Field("port", "新 SSH 端口", "number", default="2222", min_=1, max_=65535),
|
||||
Field("permit_root_login", "允许 root 登录", "select", default="no",
|
||||
options=["yes", "no", "prohibit-password", "without-password", "forced-commands-only"]),
|
||||
Field("password_auth", "允许密码认证", "checkbox", default="no"),
|
||||
Field("pubkey_auth", "公钥认证", "checkbox", default="yes"),
|
||||
Field("allow_users", "允许登录的用户 (逗号分隔)", "text", default=""),
|
||||
Field("allow_groups", "允许登录的组", "text", default=""),
|
||||
Field("max_auth_tries", "最大认证尝试", "number", default="3"),
|
||||
Field("max_sessions", "最大会话数", "number", default="5"),
|
||||
Field("client_alive_interval", "心跳间隔", "number", default="300"),
|
||||
Field("client_alive_count_max", "心跳失败次数", "number", default="2"),
|
||||
Field("banner_text", "登录横幅", "textarea", default="Authorised access only. All activity is logged."),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
port = str(p.get("port", "2222"))
|
||||
proot = p.get("permit_root_login", "no")
|
||||
passw = bool_str(p.get("password_auth"))
|
||||
pubkey = bool_str(p.get("pubkey_auth", True))
|
||||
allow_users = p.get("allow_users", "").strip()
|
||||
allow_groups = p.get("allow_groups", "").strip()
|
||||
mt = str(p.get("max_auth_tries", "3"))
|
||||
ms = str(p.get("max_sessions", "5"))
|
||||
cai = str(p.get("client_alive_interval", "300"))
|
||||
cacm = str(p.get("client_alive_count_max", "2"))
|
||||
banner = p.get("banner_text", "").strip()
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Backing up /etc/ssh/sshd_config..."')
|
||||
out.append('cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%s)')
|
||||
out.append('log "Patching sshd_config..."')
|
||||
sshd_lines = [
|
||||
'Port ' + port,
|
||||
'PermitRootLogin ' + proot,
|
||||
'PasswordAuthentication ' + ('yes' if passw else 'no'),
|
||||
'PubkeyAuthentication ' + ('yes' if pubkey else 'no'),
|
||||
'MaxAuthTries ' + mt,
|
||||
'MaxSessions ' + ms,
|
||||
'ClientAliveInterval ' + cai,
|
||||
'ClientAliveCountMax ' + cacm,
|
||||
'X11Forwarding no',
|
||||
'AllowTcpForwarding no',
|
||||
'PermitEmptyPasswords no',
|
||||
'UsePAM yes',
|
||||
]
|
||||
if allow_users:
|
||||
sshd_lines.append('AllowUsers ' + allow_users)
|
||||
if allow_groups:
|
||||
sshd_lines.append('AllowGroups ' + allow_groups)
|
||||
out.append('cat > /etc/ssh/sshd_config.d/00-shellgen.conf <<CFG_EOF\n' + '\n'.join(sshd_lines) + '\nCFG_EOF')
|
||||
if banner:
|
||||
out.append('cat > /etc/ssh/banner <<BANNER_EOF\n' + banner + '\nBANNER_EOF')
|
||||
out.append('echo "Banner /etc/ssh/banner" >> /etc/ssh/sshd_config.d/00-shellgen.conf')
|
||||
out.append('sshd -t')
|
||||
out.append('systemctl reload sshd || systemctl restart sshd')
|
||||
out.append('log "Now listening on :' + port + ' — keep your session open and test from another window!"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Sysctl tuning ==========================
|
||||
class SysctlTuning(Generator):
|
||||
id = "sysctl-tuning"
|
||||
title = "Sysctl 内核参数调优"
|
||||
category = "system"
|
||||
icon = "🧬"
|
||||
tags = ["sysctl", "kernel", "tuning"]
|
||||
description = "常用 sysctl 参数:BBR 拥塞控制、TCP 缓冲区、文件句柄、IPv4 转发。"
|
||||
fields = [
|
||||
Field("enable_bbr", "启用 BBR", "checkbox", default="yes"),
|
||||
Field("tcp_tw_reuse", "TCP TIME_WAIT 复用", "checkbox", default="yes"),
|
||||
Field("tcp_fastopen", "TCP Fast Open", "checkbox", default="yes"),
|
||||
Field("ip_forward", "IPv4 转发", "checkbox", default="no"),
|
||||
Field("net_core_somaxconn", "SOMAXCONN", "number", default="4096"),
|
||||
Field("file_max", "fs.file-max", "number", default="2097152"),
|
||||
Field("max_map_count", "vm.max_map_count (ES 需 262144)", "number", default="262144"),
|
||||
Field("apply_immediately", "立即 sysctl -p", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
bbr = bool_str(p.get("enable_bbr", True))
|
||||
tw = bool_str(p.get("tcp_tw_reuse", True))
|
||||
tfo = bool_str(p.get("tcp_fastopen", True))
|
||||
fwd = bool_str(p.get("ip_forward"))
|
||||
somaxconn = str(p.get("net_core_somaxconn", "4096"))
|
||||
fmax = str(p.get("file_max", "2097152"))
|
||||
mmc = str(p.get("max_map_count", "262144"))
|
||||
apply = bool_str(p.get("apply_immediately", True))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Writing /etc/sysctl.d/99-shellgen.conf..."')
|
||||
out.append('cat > /etc/sysctl.d/99-shellgen.conf <<SYSCTL_EOF\n'
|
||||
'fs.file-max = ' + fmax + '\n'
|
||||
'vm.max_map_count = ' + mmc + '\n'
|
||||
'net.core.somaxconn = ' + somaxconn + '\n'
|
||||
'net.core.netdev_max_backlog = 16384\n'
|
||||
'net.ipv4.tcp_max_syn_backlog = 8192\n'
|
||||
'net.ipv4.tcp_slow_start_after_idle = 0\n'
|
||||
'net.ipv4.tcp_tw_reuse = ' + ('1' if tw else '0') + '\n'
|
||||
'net.ipv4.tcp_fin_timeout = 15\n'
|
||||
'net.ipv4.tcp_keepalive_time = 600\n'
|
||||
'net.ipv4.tcp_keepalive_intvl = 30\n'
|
||||
'net.ipv4.tcp_keepalive_probes = 3\n'
|
||||
'net.ipv4.tcp_fastopen = ' + ('3' if tfo else '0') + '\n'
|
||||
'net.ipv4.ip_forward = ' + ('1' if fwd else '0') + '\n'
|
||||
'net.ipv4.conf.all.rp_filter = 1\n'
|
||||
'net.ipv4.conf.default.rp_filter = 1\n'
|
||||
'net.ipv4.conf.all.accept_source_route = 0\n'
|
||||
'net.ipv4.conf.default.accept_source_route = 0\n'
|
||||
'net.ipv4.icmp_echo_ignore_broadcasts = 1\n'
|
||||
'net.ipv4.conf.all.send_redirects = 0\n'
|
||||
'net.ipv4.conf.all.accept_redirects = 0\n'
|
||||
'net.ipv4.conf.all.secure_redirects = 0\n'
|
||||
'net.ipv6.conf.all.accept_redirects = 0\n'
|
||||
'SYSCTL_EOF')
|
||||
if bbr:
|
||||
out.append('cat >> /etc/sysctl.d/99-shellgen.conf <<SYSCTL_EOF\n'
|
||||
'net.core.default_qdisc = fq\n'
|
||||
'net.ipv4.tcp_congestion_control = bbr\n'
|
||||
'SYSCTL_EOF')
|
||||
if apply:
|
||||
out.append('sysctl -p /etc/sysctl.d/99-shellgen.conf')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== ulimit / limits.conf ==========================
|
||||
class Ulimit(Generator):
|
||||
id = "ulimit"
|
||||
title = "limits.conf 资源限制"
|
||||
category = "system"
|
||||
icon = "📊"
|
||||
tags = ["ulimit", "limits"]
|
||||
description = "为指定用户/组写 /etc/security/limits.d/*.conf。"
|
||||
fields = [
|
||||
Field("target", "对象 (user 或 @group)", "text", default="@app",
|
||||
help="@ 表示组;不带前缀为用户。"),
|
||||
Field("nofile", "nofile (打开文件数)", "number", default="65536"),
|
||||
Field("nproc", "nproc (进程数)", "number", default="65536"),
|
||||
Field("memlock", "memlock (KB, 无限 = unlimited)", "text", default="unlimited"),
|
||||
Field("cpu", "cpu (分钟, 无限 = unlimited)", "text", default="unlimited"),
|
||||
Field("data", "data (KB)", "text", default="unlimited"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
t = p.get("target", "@app")
|
||||
nofile = str(p.get("nofile", "65536"))
|
||||
nproc = str(p.get("nproc", "65536"))
|
||||
memlock = p.get("memlock", "unlimited")
|
||||
cpu = p.get("cpu", "unlimited")
|
||||
data = p.get("data", "unlimited")
|
||||
out = [bash_header(self.title)]
|
||||
out.append('cat > /etc/security/limits.d/99-shellgen.conf <<LIM_EOF\n'
|
||||
+ t + ' soft nofile ' + nofile + '\n'
|
||||
+ t + ' hard nofile ' + nofile + '\n'
|
||||
+ t + ' soft nproc ' + nproc + '\n'
|
||||
+ t + ' hard nproc ' + nproc + '\n'
|
||||
+ t + ' soft memlock ' + memlock + '\n'
|
||||
+ t + ' hard memlock ' + memlock + '\n'
|
||||
+ t + ' soft cpu ' + cpu + '\n'
|
||||
+ t + ' hard cpu ' + cpu + '\n'
|
||||
+ t + ' soft data ' + data + '\n'
|
||||
+ t + ' hard data ' + data + '\n'
|
||||
'LIM_EOF')
|
||||
out.append('log "Re-login required for new limits to take effect."')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Hostname / hosts ==========================
|
||||
class Hostname(Generator):
|
||||
id = "hostname"
|
||||
title = "Hostname 与 /etc/hosts"
|
||||
category = "system"
|
||||
icon = "🏷️"
|
||||
tags = ["hostname", "hosts"]
|
||||
description = "修改主机名并更新 /etc/hosts。"
|
||||
fields = [
|
||||
Field("hostname", "新主机名", "text", default="server01",
|
||||
help="FQDN 格式: server01.example.com, 短名会自动取 . 前部分。"),
|
||||
Field("extra_hosts", "额外 hosts 行", "textarea", default="",
|
||||
help="每行: <ip> <hostname> [alias...]"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
h = p.get("hostname", "server01")
|
||||
extras = [e for e in p.get("extra_hosts", "").splitlines() if e.strip()]
|
||||
out = [bash_header(self.title)]
|
||||
out.append('hostnamectl set-hostname ' + h)
|
||||
out.append('cp /etc/hosts /etc/hosts.bak.$(date +%s)')
|
||||
out.append('# Ensure 127.0.0.1 contains this hostname')
|
||||
out.append('grep -q "' + h + '" /etc/hosts || sed -i "s/^127.0.0.1\\(\\s\\+\\)localhost.*/& ' + h + '/" /etc/hosts')
|
||||
for e in extras:
|
||||
out.append('echo "' + e + '" >> /etc/hosts')
|
||||
out.append('hostname -f')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Disk auto-mount via fstab ==========================
|
||||
class AutoMount(Generator):
|
||||
id = "automount"
|
||||
title = "磁盘自动挂载 (fstab)"
|
||||
category = "system"
|
||||
icon = "💽"
|
||||
tags = ["mount", "fstab", "disk"]
|
||||
description = "为指定磁盘创建 fstab 条目并挂载。"
|
||||
fields = [
|
||||
Field("device", "设备 (UUID=/dev/sda1 或 LABEL=...)", "text", default="UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
help="可用 lsblk -f 或 blkid 查询。"),
|
||||
Field("mount_point", "挂载点", "text", default="/data"),
|
||||
Field("fstype", "文件系统", "select", default="ext4",
|
||||
options=["ext4", "ext3", "xfs", "btrfs", "ntfs", "vfat", "nfs", "cifs"]),
|
||||
Field("mount_options", "挂载选项", "text", default="defaults,nofail"),
|
||||
Field("dump", "dump 字段", "number", default="0"),
|
||||
Field("pass", "fsck 顺序", "number", default="2"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
dev = p.get("device", "UUID=xxx")
|
||||
mp = p.get("mount_point", "/data")
|
||||
fs = p.get("fstype", "ext4")
|
||||
opts = p.get("mount_options", "defaults,nofail")
|
||||
dump = str(p.get("dump", "0"))
|
||||
pss = str(p.get("pass", "2"))
|
||||
out = [bash_header(self.title)]
|
||||
out.append('mkdir -p ' + mp)
|
||||
out.append('echo "' + dev + ' ' + mp + ' ' + fs + ' ' + opts + ' ' + dump + ' ' + pss + '" >> /etc/fstab')
|
||||
out.append('mount -a')
|
||||
out.append('df -h ' + mp)
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
for _g in [Firewall, Swap, Chrony, SSHHardening, SysctlTuning, Ulimit, Hostname, AutoMount]:
|
||||
register(_g())
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Systemd service generator — a single, generic Generator that produces a fully-configured
|
||||
systemd unit for arbitrary user-provided commands. Plus presets for common cases.
|
||||
"""
|
||||
from . import register, Generator, Field
|
||||
from . import bash_header, quote, bool_str, yes
|
||||
|
||||
|
||||
class SystemdGeneric(Generator):
|
||||
id = "systemd-generic"
|
||||
title = "通用 systemd 服务"
|
||||
category = "systemd"
|
||||
icon = "🛠️"
|
||||
tags = ["systemd", "service", "unit"]
|
||||
description = "为任意可执行命令生成 systemd unit 文件,支持开机自启、自动重启、日志轮转。"
|
||||
fields = [
|
||||
Field("service_name", "服务名", "text", default="myapp",
|
||||
help="将创建 /etc/systemd/system/<name>.service"),
|
||||
Field("description", "Description", "text", default="My custom app"),
|
||||
Field("exec_start", "ExecStart 命令", "textarea", required=True,
|
||||
default="/opt/myapp/bin/server --config /etc/myapp/server.conf",
|
||||
help="要执行的命令,可以有多行(用 ; 串接)或用绝对路径。"),
|
||||
Field("exec_stop", "ExecStop 命令", "text", default="",
|
||||
help="留空则发 SIGTERM;填入完整命令。"),
|
||||
Field("exec_reload", "ExecReload 命令", "text", default="",
|
||||
help="通常是 'kill -HUP $MAINPID' 或 nginx -s reload。"),
|
||||
Field("type", "Type", "select", default="simple",
|
||||
options=["simple", "forking", "oneshot", "notify", "exec"]),
|
||||
Field("user", "运行用户", "text", default="root"),
|
||||
Field("group", "运行组", "text", default="root"),
|
||||
Field("working_dir", "WorkingDirectory", "text", default="/"),
|
||||
Field("env_vars", "环境变量", "textarea", default="NODE_ENV=production\nLOG_LEVEL=info",
|
||||
help="每行 KEY=VALUE。"),
|
||||
Field("restart", "Restart 策略", "select", default="on-failure",
|
||||
options=["no", "always", "on-success", "on-failure", "on-abnormal",
|
||||
"on-abort", "on-watchdog"]),
|
||||
Field("restart_sec", "RestartSec (秒)", "number", default="5"),
|
||||
Field("limit_nofile", "LimitNOFILE", "number", default="65536"),
|
||||
Field("limit_nproc", "LimitNPROC", "number", default="65536"),
|
||||
Field("kill_mode", "KillMode", "select", default="mixed",
|
||||
options=["control-group", "process", "mixed", "none"]),
|
||||
Field("enable_after_network", "After=network.target", "checkbox", default="yes"),
|
||||
Field("enable_log_dir", "创建日志目录 /var/log/<name>", "checkbox", default="yes"),
|
||||
Field("auto_start", "脚本执行完自动 enable+start", "checkbox", default="yes"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
name = p.get("service_name", "myapp").replace(" ", "-")
|
||||
desc = p.get("description", "My custom app")
|
||||
exec_start = p.get("exec_start", "").strip()
|
||||
if not exec_start:
|
||||
raise ValueError("exec_start 不能为空")
|
||||
exec_stop = p.get("exec_stop", "").strip()
|
||||
exec_reload = p.get("exec_reload", "").strip()
|
||||
type_ = p.get("type", "simple")
|
||||
user = p.get("user", "root")
|
||||
group = p.get("group", "root")
|
||||
wd = p.get("working_dir", "/")
|
||||
envs = [e.strip() for e in (p.get("env_vars", "") or "").splitlines() if e.strip() and "=" in e]
|
||||
restart = p.get("restart", "on-failure")
|
||||
restart_sec = str(p.get("restart_sec", "5"))
|
||||
nofile = str(p.get("limit_nofile", "65536"))
|
||||
nproc = str(p.get("limit_nproc", "65536"))
|
||||
kill = p.get("kill_mode", "mixed")
|
||||
after_nw = bool_str(p.get("enable_after_network", True))
|
||||
log_dir = bool_str(p.get("enable_log_dir", True))
|
||||
auto = bool_str(p.get("auto_start", True))
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Writing unit file /etc/systemd/system/' + name + '.service"')
|
||||
out.append('mkdir -p /etc/systemd/system')
|
||||
if log_dir:
|
||||
out.append('mkdir -p /var/log/' + name)
|
||||
unit = ['[Unit]']
|
||||
unit.append('Description=' + desc)
|
||||
if after_nw:
|
||||
unit.append('After=network.target network-online.target')
|
||||
unit.append('Wants=network-online.target')
|
||||
unit.append('')
|
||||
unit.append('[Service]')
|
||||
unit.append('Type=' + type_)
|
||||
unit.append('User=' + user)
|
||||
unit.append('Group=' + group)
|
||||
unit.append('WorkingDirectory=' + wd)
|
||||
for env in envs:
|
||||
unit.append('Environment="' + env.replace('"', '\\"') + '"')
|
||||
unit.append('ExecStart=' + exec_start.replace('\n', ' && '))
|
||||
if exec_stop:
|
||||
unit.append('ExecStop=' + exec_stop)
|
||||
if exec_reload:
|
||||
unit.append('ExecReload=' + exec_reload)
|
||||
unit.append('Restart=' + restart)
|
||||
unit.append('RestartSec=' + restart_sec)
|
||||
unit.append('LimitNOFILE=' + nofile)
|
||||
unit.append('LimitNPROC=' + nproc)
|
||||
unit.append('KillMode=' + kill)
|
||||
unit.append('StandardOutput=journal')
|
||||
unit.append('StandardError=journal')
|
||||
if log_dir:
|
||||
unit.append('StandardOutput=append:/var/log/' + name + '/stdout.log')
|
||||
unit.append('StandardError=append:/var/log/' + name + '/stderr.log')
|
||||
unit.append('')
|
||||
unit.append('[Install]')
|
||||
unit.append('WantedBy=multi-user.target')
|
||||
out.append('cat > /etc/systemd/system/' + name + '.service <<UNIT_EOF\n' + '\n'.join(unit) + '\nUNIT_EOF')
|
||||
out.append('systemctl daemon-reload')
|
||||
if auto:
|
||||
out.append('systemctl enable --now ' + name)
|
||||
out.append('sleep 1')
|
||||
out.append('systemctl --no-pager status ' + name + ' || true')
|
||||
else:
|
||||
out.append('log "Unit installed but not started. Run: systemctl enable --now ' + name + '"')
|
||||
out.append('log "Useful commands:')
|
||||
out.append(' systemctl status ' + name)
|
||||
out.append(' journalctl -u ' + name + ' -f')
|
||||
out.append(' systemctl restart ' + name + '"')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# A few presets that wrap the same engine with sensible defaults
|
||||
def _make_preset(id_, title, icon, description, defaults, tags=()):
|
||||
class G(SystemdGeneric):
|
||||
pass
|
||||
G.id = id_
|
||||
G.title = title
|
||||
G.icon = icon
|
||||
G.description = description
|
||||
G.tags = list(tags)
|
||||
G.fields = [] # CRITICAL: avoid mutating the parent's shared list
|
||||
for f in SystemdGeneric.fields:
|
||||
f2 = Field(f.name, f.label, f.type, f.default, f.placeholder, f.help,
|
||||
f.options, f.required, f.min, f.max, f.step, f.pattern, f.group)
|
||||
if f.name in defaults:
|
||||
f2.default = defaults[f.name]
|
||||
G.fields.append(f2)
|
||||
return G
|
||||
|
||||
|
||||
# preset: Python uvicorn/gunicorn web app
|
||||
GunicornSvc = _make_preset(
|
||||
id_="svc-gunicorn",
|
||||
title="Gunicorn (Python) systemd 服务",
|
||||
icon="🐍",
|
||||
description="为 gunicorn 应用生成 systemd unit,自动设置 GUNICORN_CMD_ARGS、日志目录。",
|
||||
defaults={
|
||||
"service_name": "myapp",
|
||||
"description": "My Python web app (gunicorn)",
|
||||
"exec_start": "/opt/myapp/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 app:app",
|
||||
"type": "simple",
|
||||
"user": "app",
|
||||
"working_dir": "/opt/myapp",
|
||||
"env_vars": "PYTHONUNBUFFERED=1\nLOG_LEVEL=info",
|
||||
},
|
||||
tags=("python", "gunicorn", "flask", "django", "fastapi"),
|
||||
)
|
||||
register(GunicornSvc())
|
||||
|
||||
|
||||
# preset: Node.js pm2-style systemd
|
||||
NodeSvc = _make_preset(
|
||||
id_="svc-nodejs",
|
||||
title="Node.js (npm start) systemd 服务",
|
||||
icon="🟢",
|
||||
description="为 Node.js 应用生成 systemd unit,支持 npm start / node 直接启动。",
|
||||
defaults={
|
||||
"service_name": "nodeapp",
|
||||
"description": "Node.js application",
|
||||
"exec_start": "/usr/bin/node /opt/nodeapp/server.js",
|
||||
"user": "app",
|
||||
"working_dir": "/opt/nodeapp",
|
||||
"env_vars": "NODE_ENV=production\nPORT=3000",
|
||||
},
|
||||
tags=("nodejs", "node", "express", "next"),
|
||||
)
|
||||
register(NodeSvc())
|
||||
|
||||
|
||||
# preset: Java -jar
|
||||
JavaSvc = _make_preset(
|
||||
id_="svc-java",
|
||||
title="Java -jar systemd 服务",
|
||||
icon="☕",
|
||||
description="为 SpringBoot/任意 Java -jar 应用生成 systemd unit,自动设置 JAVA_HOME。",
|
||||
defaults={
|
||||
"service_name": "javaapp",
|
||||
"description": "Java application",
|
||||
"exec_start": "/usr/bin/java -Xms512m -Xmx1024m -jar /opt/javaapp/app.jar --server.port=8080",
|
||||
"user": "app",
|
||||
"working_dir": "/opt/javaapp",
|
||||
"env_vars": "JAVA_HOME=/usr/lib/jvm/java-17-openjdk",
|
||||
},
|
||||
tags=("java", "springboot", "jar"),
|
||||
)
|
||||
register(JavaSvc())
|
||||
|
||||
|
||||
# preset: Go binary
|
||||
GoSvc = _make_preset(
|
||||
id_="svc-go",
|
||||
title="Go binary systemd 服务",
|
||||
icon="🐹",
|
||||
description="为编译后的 Go 二进制生成 systemd unit。",
|
||||
defaults={
|
||||
"service_name": "goapp",
|
||||
"description": "Go application",
|
||||
"exec_start": "/opt/goapp/bin/server -config /etc/goapp/server.yaml",
|
||||
"user": "app",
|
||||
"working_dir": "/opt/goapp",
|
||||
},
|
||||
tags=("go", "golang"),
|
||||
)
|
||||
register(GoSvc())
|
||||
|
||||
|
||||
# preset: shell script
|
||||
ShellSvc = _make_preset(
|
||||
id_="svc-shell",
|
||||
title="Shell 脚本 systemd 服务",
|
||||
icon="📜",
|
||||
description="为任意 shell 脚本生成 systemd unit,适合定时/常驻脚本。",
|
||||
defaults={
|
||||
"service_name": "myscript",
|
||||
"description": "Custom shell script service",
|
||||
"exec_start": "/opt/myscript/run.sh",
|
||||
"user": "root",
|
||||
"working_dir": "/opt/myscript",
|
||||
},
|
||||
tags=("bash", "script"),
|
||||
)
|
||||
register(ShellSvc())
|
||||
|
||||
|
||||
# Register the generic one as well
|
||||
register(SystemdGeneric())
|
||||
Reference in New Issue
Block a user