Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 956797f75b | |||
| 893f5f5955 | |||
| cb1566c400 | |||
| 1a328864f5 | |||
| e854137681 | |||
| 66aefd6161 | |||
| bee37565e1 | |||
| 0b2ab34ac2 | |||
| 6f4d071bf3 | |||
| b9bd50c869 |
@@ -0,0 +1,30 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Git & CI
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Flask instance (SQLite + uploads) — 运行时生成,不进镜像
|
||||
instance/
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# 交付物 & 生成测试脚本(非源码)
|
||||
# NOTE: never use a bare '*.sh' here — that would also exclude the project's
|
||||
# own start.sh which the Dockerfile COPYs in. Be specific about which .sh
|
||||
# files are user-generated artifacts.
|
||||
deliverables/
|
||||
deliverables/*.sh
|
||||
/tmp/
|
||||
|
||||
# 构建中间产物
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# =====================================================================
|
||||
# shell-gen - Linux 一键部署脚本生成器 (Flask Web 应用)
|
||||
#
|
||||
# 零 pip 依赖设计:除了 Flask,仅使用 Python 标准库。
|
||||
# 通过 PORT 环境变量控制监听端口(默认 5099)。
|
||||
#
|
||||
# 构建:
|
||||
# docker build -t shell-gen .
|
||||
#
|
||||
# 运行:
|
||||
# docker run -d --name shell-gen -p 5099:5099 \
|
||||
# -e PORT=5099 \
|
||||
# -e SHELLGEN_SECRET='change-me' \
|
||||
# shell-gen
|
||||
#
|
||||
# 验证:
|
||||
# curl http://localhost:5099/healthz
|
||||
# # -> {"ok":true,"generators":50}
|
||||
# =====================================================================
|
||||
|
||||
# ---------- 基础镜像 ----------
|
||||
# slim 版足够(项目只用 Flask + 标准库,无需编译工具)
|
||||
FROM python:3.11-slim
|
||||
|
||||
# ---------- 元信息 ----------
|
||||
LABEL org.opencontainers.image.title="shell-gen" \
|
||||
org.opencontainers.image.description="Linux 一键部署脚本生成器 (Flask)" \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
|
||||
# 环境变量:缓冲输出便于 docker logs 实时查看
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PORT=5099 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
# ---------- 工作目录 ----------
|
||||
WORKDIR /app
|
||||
|
||||
# ---------- 安装依赖 ----------
|
||||
# 唯一第三方依赖是 Flask;先复制 requirements 以利用层缓存
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# ---------- 复制应用代码 ----------
|
||||
COPY app.py /app/app.py
|
||||
COPY generators/ /app/generators/
|
||||
COPY templates/ /app/templates/
|
||||
COPY static/ /app/static/
|
||||
COPY README.md /app/README.md
|
||||
COPY start.sh /app/start.sh
|
||||
|
||||
# ---------- 非 root 运行 ----------
|
||||
# 创建专有运行用户,降低容器内权限风险
|
||||
RUN useradd --create-home --uid 10001 appuser \
|
||||
&& mkdir -p /app/instance && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# ---------- 端口 & 健康检查 ----------
|
||||
EXPOSE 5099
|
||||
# 依赖 /healthz 路由做容器健康探针(无需额外工具)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:'+__import__('os').environ.get('PORT','5099')+'/healthz',timeout=3).status==200 else 1)" || exit 1
|
||||
|
||||
# ---------- 启动 ----------
|
||||
# 用 Flask 自带开发服务器(保持与项目 start.sh 一致的零依赖启动方式)
|
||||
# 若要生产级并发,可改用 gunicorn:见下方注释
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
# ---------------- 生产并发方案(可选) ----------------
|
||||
# 如果希望多 worker 并发处理,取消下面注释并将上面的 CMD 替换:
|
||||
#
|
||||
# 1) 在 requirements.txt 增加一行:gunicorn
|
||||
# 2) 将 CMD 改为:
|
||||
# CMD ["gunicorn", "--bind", "0.0.0.0:5099", "--workers", "2", \
|
||||
# "--threads", "4", "--timeout", "60", "app:app"]
|
||||
@@ -23,6 +23,7 @@ from generators.system import * # noqa: F401
|
||||
from generators.network import * # noqa: F401
|
||||
from generators.databases import * # noqa: F401
|
||||
from generators.monitoring import * # noqa: F401
|
||||
from generators.ci import * # noqa: F401
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
app = Flask(__name__, instance_path=str(BASE_DIR / "instance"))
|
||||
|
||||
+20
-5
@@ -41,6 +41,7 @@ category_labels = {
|
||||
"network": "网络工具",
|
||||
"monitoring": "监控告警",
|
||||
"security": "安全加固",
|
||||
"ci": "CI/CD 平台",
|
||||
}
|
||||
|
||||
category_icons = {
|
||||
@@ -52,6 +53,7 @@ category_icons = {
|
||||
"network": "🌐",
|
||||
"monitoring": "📊",
|
||||
"security": "🔒",
|
||||
"ci": "🚀",
|
||||
}
|
||||
|
||||
|
||||
@@ -137,14 +139,27 @@ die() {{ echo "${{LOG_PREFIX}} ERROR: $*" >&2 ; exit 1 ; }}
|
||||
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" ;;
|
||||
ubuntu|debian)
|
||||
PKG="apt-get"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
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"
|
||||
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"
|
||||
PKG="apt-get"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
PKG_INSTALL() {{ DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"; }}
|
||||
else
|
||||
die "Unsupported distribution: $DISTRO_ID"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
CI/CD platform generators (GitLab CE, Jenkins LTS).
|
||||
|
||||
Both install a self-contained service via systemd. The rendered scripts
|
||||
follow the same conventions as the rest of the project:
|
||||
|
||||
- bash_header() prologue with set -euo pipefail + distro detection
|
||||
- PKG_INSTALL() function (NOT \$PKG_INSTALL — set -u safe)
|
||||
- heredocs are quoted (<<'TAG') to keep tool-specific placeholders
|
||||
like nginx \$host / systemd %i intact
|
||||
"""
|
||||
from . import Generator, Field, register, bash_header, bool_str
|
||||
|
||||
|
||||
# ========================== GitLab CE ==========================
|
||||
class GitLab(Generator):
|
||||
id = "gitlab"
|
||||
title = "GitLab CE (代码仓库 + CI)"
|
||||
category = "ci"
|
||||
icon = "🦊"
|
||||
tags = ["gitlab", "git", "ci", "devops"]
|
||||
description = (
|
||||
"通过官方 Omnibus 包安装 GitLab Community Edition,"
|
||||
"自动配置 external_url、可选 SMTP。"
|
||||
)
|
||||
warnings = [
|
||||
"GitLab 占用资源较大,推荐至少 4GB RAM / 4 CPU。",
|
||||
"生产环境务必配置 SMTP,否则用户收不到邮件通知。",
|
||||
]
|
||||
post_steps = [
|
||||
"浏览器访问 external_url,首次用 root 账号登录并立即改密码。",
|
||||
"cat /etc/gitlab/initial_root_password # 查看初始 root 密码(24h 后失效)",
|
||||
]
|
||||
fields = [
|
||||
Field("external_url", "External URL", "text",
|
||||
default="http://gitlab.example.com",
|
||||
placeholder="http://gitlab.your-domain.com",
|
||||
required=True,
|
||||
help="用户访问 GitLab 的地址;必须是 http(s):// 开头,"
|
||||
"会自动写入 nginx vhost。"),
|
||||
Field("version", "版本", "text", default="latest",
|
||||
placeholder="latest 或 16.11.0-ce.0",
|
||||
help="留空/填 latest 装最新版;"
|
||||
"固定版本号形如 16.11.0-ce.0。"),
|
||||
Field("data_dir", "数据目录", "text", default="/var/opt/gitlab",
|
||||
help="GitLab 数据存放位置;挂独立盘请改成大空间路径。"),
|
||||
Field("configure_smtp", "配置 SMTP", "checkbox", default="no",
|
||||
help="勾选后会写入 SMTP 配置,需要填下面 3 个字段。"
|
||||
"不勾选则保留 GitLab 默认(无邮件功能)。"),
|
||||
Field("smtp_address", "SMTP 地址", "text",
|
||||
default="smtp.gmail.com:587",
|
||||
placeholder="smtp.example.com:587",
|
||||
help="仅 configure_smtp=yes 时生效。"),
|
||||
Field("smtp_user", "SMTP 用户名", "text", default="",
|
||||
placeholder="[email protected]",
|
||||
help="仅 configure_smtp=yes 时生效。"),
|
||||
Field("smtp_password", "SMTP 密码", "text", default="",
|
||||
placeholder="app password (推荐)",
|
||||
help="仅 configure_smtp=yes 时生效;"
|
||||
"推荐用 Gmail / QQ 的 app password 而非账号密码。"),
|
||||
]
|
||||
|
||||
def render(self, p):
|
||||
ext_url = p.get("external_url", "http://gitlab.example.com").strip()
|
||||
if not ext_url.startswith(("http://", "https://")):
|
||||
raise ValueError(
|
||||
"external_url 必须以 http:// 或 https:// 开头"
|
||||
)
|
||||
version = p.get("version", "latest").strip() or "latest"
|
||||
data_dir = p.get("data_dir", "/var/opt/gitlab").strip()
|
||||
smtp_on = bool_str(p.get("configure_smtp"))
|
||||
smtp_addr = p.get("smtp_address", "smtp.gmail.com:587").strip()
|
||||
smtp_user = p.get("smtp_user", "").strip()
|
||||
smtp_pass = p.get("smtp_password", "").strip()
|
||||
if smtp_on and (not smtp_user or not smtp_pass):
|
||||
raise ValueError(
|
||||
"configure_smtp=yes 时必须填写 smtp_user 和 smtp_password"
|
||||
)
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing GitLab CE..."')
|
||||
out.append('# GitLab Omnibus 自带 postfix / nginx / openssh 等依赖,'
|
||||
'脚本不需单独装。')
|
||||
out.append('PKG_INSTALL curl openssl ca-certificates perl')
|
||||
out.append('')
|
||||
# Download GitLab's official apt/yum repo setup script.
|
||||
# The script picks the right package manager based on detected distro.
|
||||
# Using 'curl | bash' is GitLab's officially documented install path.
|
||||
out.append('log "Adding GitLab apt/yum repository..."')
|
||||
if version == "latest":
|
||||
pkg_args = ""
|
||||
else:
|
||||
pkg_args = '?version=' + version
|
||||
out.append('curl -fsSL https://packages.gitlab.com/install/repositories/'
|
||||
'gitlab/gitlab-ce/script.deb.sh' + pkg_args
|
||||
+ ' -o /tmp/gl-repo.sh')
|
||||
# The .rpm.sh variant is what GitLab's deb.sh script also supports;
|
||||
# but the deb.sh handles apt only. For RHEL family we fall back to rpm.sh.
|
||||
# To keep things simple and reliable, we just attempt deb.sh first;
|
||||
# if the host is RHEL-like, GitLab's script will detect and abort with
|
||||
# a hint pointing to the rpm.sh variant. We surface that as an error.
|
||||
out.append('bash /tmp/gl-repo.sh')
|
||||
out.append('rm -f /tmp/gl-repo.sh')
|
||||
out.append('')
|
||||
# Install the package. For specific version we use =<ver>, otherwise bare.
|
||||
if version == "latest":
|
||||
install_cmd = 'gitlab-ce'
|
||||
else:
|
||||
install_cmd = 'gitlab-ce=' + version
|
||||
out.append('log "Installing GitLab CE package (this may take a few minutes)..."')
|
||||
out.append('PKG_INSTALL ' + install_cmd)
|
||||
out.append('')
|
||||
# Generate /etc/gitlab/gitlab.rb
|
||||
# NOTE: heredoc is QUOTED — keeps any literal $ / % in our config
|
||||
# intact for reconfigure to interpret, and is safe under 'set -u'.
|
||||
out.append('log "Writing /etc/gitlab/gitlab.rb..."')
|
||||
rb_lines = [
|
||||
"external_url '" + ext_url.replace("'", "'\\''") + "'",
|
||||
"git_data_dir '" + data_dir + "'",
|
||||
]
|
||||
if smtp_on:
|
||||
# GitLab rb SMTP block (escaped single quotes inside heredoc are fine
|
||||
# because the heredoc delimiter is quoted; nothing will be expanded).
|
||||
rb_lines += [
|
||||
"gitlab_rails['smtp_enable'] = true",
|
||||
"gitlab_rails['smtp_address'] = '" + smtp_addr.replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_port'] = 587",
|
||||
"gitlab_rails['smtp_user_name'] = '" + smtp_user.replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_password'] = '" + smtp_pass.replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_domain'] = '" +
|
||||
smtp_addr.split(":")[0].replace("'", "'\\''") + "'",
|
||||
"gitlab_rails['smtp_authentication'] = 'login'",
|
||||
"gitlab_rails['smtp_enable_starttls_auto'] = true",
|
||||
"gitlab_rails['smtp_tls'] = false",
|
||||
]
|
||||
out.append("cat > /etc/gitlab/gitlab.rb <<'GITLAB_RB_EOF'\n"
|
||||
+ "\n".join(rb_lines) + "\n"
|
||||
"GITLAB_RB_EOF")
|
||||
out.append('')
|
||||
out.append('log "Running gitlab-ctl reconfigure (this takes 3-5 min)..."')
|
||||
out.append('gitlab-ctl reconfigure')
|
||||
out.append('')
|
||||
# Extract bound port from external_url (best effort: default 80/443)
|
||||
bound_port = "80"
|
||||
if ext_url.startswith("https://"):
|
||||
bound_port = "443"
|
||||
out.append('log "Verifying GitLab is reachable..."')
|
||||
out.append('sleep 5')
|
||||
out.append('gitlab-ctl status >/dev/null && log "GitLab services are running" '
|
||||
'|| warn "Some GitLab services may not be up yet — check: gitlab-ctl status"')
|
||||
out.append('curl -fsS -o /dev/null -w "HTTP %{http_code} on :' + bound_port
|
||||
+ '\\n" http://127.0.0.1:' + bound_port + '/')
|
||||
out.append('log "Done. Open ' + ext_url + ' in your browser."')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ========================== Jenkins LTS ==========================
|
||||
class Jenkins(Generator):
|
||||
id = "jenkins"
|
||||
title = "Jenkins LTS (持续集成)"
|
||||
category = "ci"
|
||||
icon = "🤖"
|
||||
tags = ["jenkins", "ci", "cd", "pipeline"]
|
||||
description = (
|
||||
"下载 Jenkins LTS WAR 包,systemd 跑 java -jar,"
|
||||
"默认 JDK 17 / 21。"
|
||||
)
|
||||
warnings = [
|
||||
"首次启动会在 /var/lib/jenkins/secrets/initialAdminPassword 生成初始密码。",
|
||||
"推荐在 Manage Jenkins → Configure Global Security 关掉 'Anyone can do anything'。",
|
||||
]
|
||||
post_steps = [
|
||||
"浏览器访问 http://<host>:<port>",
|
||||
"cat /var/lib/jenkins/secrets/initialAdminPassword",
|
||||
"解锁后选 'Install suggested plugins'",
|
||||
]
|
||||
fields = [
|
||||
Field("version", "Jenkins LTS 版本", "select", default="2.541.3",
|
||||
options=["2.541.3", "2.528.2", "2.568.2", "latest"],
|
||||
help="2.541.3/2.528.2/2.568.2 是 LTS;"
|
||||
"latest 是滚动版(可能不稳定)。"),
|
||||
Field("port", "HTTP 端口", "number", default="8080", min_=1, max_=65535),
|
||||
Field("java_opts", "JVM 参数", "text",
|
||||
default="-Xms512m -Xmx1024m -Djenkins.install.runSetupWizard=false",
|
||||
help="JAVA_OPTS。-Xms/-Xmx 控制堆内存。"),
|
||||
Field("install_dir", "安装目录", "text", default="/opt/jenkins"),
|
||||
Field("jenkins_home", "JENKINS_HOME", "text", default="/var/lib/jenkins",
|
||||
help="jobs / plugins / 配置存放目录。挂独立盘请改这里。"),
|
||||
Field("user", "运行用户", "text", default="jenkins"),
|
||||
]
|
||||
|
||||
# Pre-validated URLs. Always pin to a real release; if 'latest' is selected
|
||||
# we use the rolling URL. Listed URLs were all confirmed 200/302 at design
|
||||
# time (see PR notes); Jenkins get.jenkins.io redirects 302 to the CDN.
|
||||
_WAR_URLS = {
|
||||
"2.541.3": "https://get.jenkins.io/war-stable/2.541.3/jenkins.war",
|
||||
"2.528.2": "https://get.jenkins.io/war-stable/2.528.2/jenkins.war",
|
||||
"2.568.2": "https://get.jenkins.io/war-stable/2.568.2/jenkins.war",
|
||||
"latest": "https://get.jenkins.io/war/latest/jenkins.war",
|
||||
}
|
||||
|
||||
def render(self, p):
|
||||
version = p.get("version", "2.541.3")
|
||||
port = str(p.get("port", "8080"))
|
||||
java_opts = p.get(
|
||||
"java_opts",
|
||||
"-Xms512m -Xmx1024m -Djenkins.install.runSetupWizard=false",
|
||||
)
|
||||
install_dir = p.get("install_dir", "/opt/jenkins")
|
||||
jh = p.get("jenkins_home", "/var/lib/jenkins")
|
||||
user = p.get("user", "jenkins")
|
||||
|
||||
if version not in self._WAR_URLS:
|
||||
raise ValueError(
|
||||
f"未知 Jenkins 版本: {version}。可选: "
|
||||
+ ", ".join(sorted(self._WAR_URLS.keys()))
|
||||
)
|
||||
url = self._WAR_URLS[version]
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing JDK + Jenkins LTS..."')
|
||||
out.append('command -v java >/dev/null || '
|
||||
'PKG_INSTALL openjdk-17-jdk-headless || '
|
||||
'PKG_INSTALL java-17-openjdk-devel || '
|
||||
'PKG_INSTALL default-jdk-headless || '
|
||||
'PKG_INSTALL default-jdk')
|
||||
out.append('command -v java >/dev/null || die "Java not found after install — pick a different JDK"')
|
||||
out.append('')
|
||||
# User + dirs
|
||||
out.append(f'useradd -r -s /bin/false -d {jh} {user} 2>/dev/null || true')
|
||||
out.append(f'mkdir -p {install_dir} {jh} /var/log/jenkins')
|
||||
out.append(f'chown -R {user}:{user} {jh} /var/log/jenkins')
|
||||
out.append('')
|
||||
# Download WAR
|
||||
out.append(f'log "Downloading Jenkins WAR ({version})..."')
|
||||
out.append(f'curl -fsSL -o {install_dir}/jenkins.war "{url}"')
|
||||
out.append(f'chown {user}:{user} {install_dir}/jenkins.war')
|
||||
out.append('')
|
||||
# systemd unit — QUOTED heredoc so %i / $PORT placeholders stay intact
|
||||
out.append('log "Writing systemd unit..."')
|
||||
# Quote java_opts for systemd Environment= line — it must be a single
|
||||
# value, and embedded '"' or '\' in user input would otherwise break
|
||||
# the unit file. We use double-quote escaping (\\" within the value).
|
||||
safe_java_opts = java_opts.replace("\\", "\\\\").replace('"', '\\"')
|
||||
out.append(
|
||||
f"cat > /etc/systemd/system/jenkins.service <<'UNIT_EOF'\n"
|
||||
'[Unit]\n'
|
||||
'Description=Jenkins LTS Continuous Integration Server\n'
|
||||
'After=network.target\n\n'
|
||||
'[Service]\n'
|
||||
'Type=simple\n'
|
||||
f'User={user}\n'
|
||||
f'Group={user}\n'
|
||||
f'Environment="JENKINS_HOME={jh}"\n'
|
||||
f'Environment="JAVA_OPTS={safe_java_opts}"\n'
|
||||
f'Environment="JENKINS_PORT={port}"\n'
|
||||
f'WorkingDirectory={jh}\n'
|
||||
f'ExecStart=/usr/bin/java $JAVA_OPTS -jar {install_dir}/jenkins.war '
|
||||
f'--httpPort=$JENKINS_PORT\n'
|
||||
'Restart=on-failure\n'
|
||||
'RestartSec=10\n\n'
|
||||
'[Install]\n'
|
||||
'WantedBy=multi-user.target\n'
|
||||
'UNIT_EOF'
|
||||
)
|
||||
out.append('systemctl daemon-reload')
|
||||
out.append('systemctl enable --now jenkins')
|
||||
out.append('log "Waiting for Jenkins to come up..."')
|
||||
# Jenkins can take 30-90s on first start while extracting plugins
|
||||
out.append('for i in $(seq 1 30); do')
|
||||
out.append(' if curl -fsS -o /dev/null http://127.0.0.1:' + port + '/ ; then')
|
||||
out.append(' log "Jenkins is responding on :' + port + '"')
|
||||
out.append(' break')
|
||||
out.append(' fi')
|
||||
out.append(' sleep 3')
|
||||
out.append('done')
|
||||
out.append('if [ ! -f ' + jh + '/secrets/initialAdminPassword ]; then')
|
||||
out.append(' warn "Initial admin password not found yet — Jenkins may still be starting."')
|
||||
out.append(' warn "Check: systemctl status jenkins / journalctl -u jenkins -n 50"')
|
||||
out.append('else')
|
||||
out.append(' log "Initial admin password (copy this):"')
|
||||
out.append(' cat ' + jh + '/secrets/initialAdminPassword')
|
||||
out.append('fi')
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
register(GitLab())
|
||||
register(Jenkins())
|
||||
@@ -27,7 +27,7 @@ class Memcached(Generator):
|
||||
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('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')
|
||||
@@ -54,9 +54,9 @@ class SqliteTools(Generator):
|
||||
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')
|
||||
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('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"
|
||||
@@ -143,7 +143,7 @@ class ClickHouse(Generator):
|
||||
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('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')
|
||||
|
||||
+173
-26
@@ -59,6 +59,14 @@ class VNCSrv(Generator):
|
||||
"对 VNC 连接无影响,只影响原生 X11 客户端直连能力。"),
|
||||
Field("use_xvfb", "使用 Xvfb (无显卡)", "checkbox", default="yes",
|
||||
help="Headless 服务器必须启用 — VNC 通过 Xvfb 渲染,无需物理显卡。"),
|
||||
Field("disable_wayland", "禁用 GDM Wayland (强制 X11)", "checkbox", default="yes",
|
||||
help="强烈建议启用 — Ubuntu 22.04 默认用 Wayland + GDM,但 Wayland 下 x11vnc "
|
||||
"会出现键盘映射异常、剪贴板双向不通、gnome-shell 偶发闪退等问题。"
|
||||
"此选项写 /etc/gdm3/custom.conf 把 WaylandEnable 设为 false,所有 GDM session "
|
||||
"(包括物理登录和锁屏)都强制走 X11。注意:在有活动的物理桌面登录时会断开一次。"),
|
||||
Field("restart_gdm", "立即重启 GDM 应用 Wayland 更改", "checkbox", default="no",
|
||||
help="yes = 立刻 systemctl restart gdm 让 Wayland 禁用生效(会断开当前物理会话);"
|
||||
"no = 仅改配置,下次重启后生效。生产机器建议 no,自己本地实验可以 yes。"),
|
||||
]
|
||||
|
||||
# ---- per-DE helpers (chosen at render time) ----
|
||||
@@ -134,6 +142,8 @@ class VNCSrv(Generator):
|
||||
nolisten_unix = bool_str(p.get("nolisten_unix", True))
|
||||
nolisten_tcp_local = bool_str(p.get("nolisten_tcp_local"))
|
||||
use_xvfb = bool_str(p.get("use_xvfb", True))
|
||||
disable_wayland = bool_str(p.get("disable_wayland", True))
|
||||
restart_gdm = bool_str(p.get("restart_gdm"))
|
||||
|
||||
if de not in self.DE_PACKAGES:
|
||||
raise ValueError(f"Unknown desktop_env: {de}. Choose one of: {', '.join(self.DE_PACKAGES)}")
|
||||
@@ -184,6 +194,48 @@ class VNCSrv(Generator):
|
||||
' gnome-tweaks')
|
||||
out.append(install)
|
||||
|
||||
# GDM Wayland disable — only relevant for GNOME on a machine that
|
||||
# has a physical GDM (i.e. ubuntu-desktop-01, not a headless server).
|
||||
# Ubuntu 22.04 defaults to WaylandEnable=true in /etc/gdm3/custom.conf.
|
||||
# Wayland + x11vnc has known issues: keyboard remapping gets
|
||||
# confused, clipboard isn't bidirectional, gnome-shell occasionally
|
||||
# crashes on X client events. Forcing X11 makes the VNC session
|
||||
# behave like a normal X11 desktop.
|
||||
if de == "gnome" and disable_wayland:
|
||||
out.append('log "Disabling GDM Wayland (forcing X11 session)..."')
|
||||
# custom.conf may not exist on a server (no gdm3 installed) — guard
|
||||
out.append('if [ -d /etc/gdm3 ] && [ -f /etc/gdm3/custom.conf ] || [ ! -f /etc/gdm3/custom.conf ]; then')
|
||||
out.append(' mkdir -p /etc/gdm3')
|
||||
out.append(' if [ -f /etc/gdm3/custom.conf ]; then')
|
||||
# Replace any existing WaylandEnable line, else add under [daemon]
|
||||
out.append(' if grep -q "^[[:space:]]*WaylandEnable" /etc/gdm3/custom.conf; then')
|
||||
out.append(' sed -i "s/^[[:space:]]*WaylandEnable.*/WaylandEnable=false/" /etc/gdm3/custom.conf')
|
||||
out.append(' else')
|
||||
out.append(' sed -i "/^[[:space:]]*\\[daemon\\]/a WaylandEnable=false" /etc/gdm3/custom.conf')
|
||||
out.append(' fi')
|
||||
out.append(' else')
|
||||
# No custom.conf yet — write a minimal one
|
||||
out.append(' cat > /etc/gdm3/custom.conf <<GDM_EOF')
|
||||
out.append('[daemon]')
|
||||
out.append('WaylandEnable=false')
|
||||
out.append('')
|
||||
out.append('[security]')
|
||||
out.append('')
|
||||
out.append('[xdmcp]')
|
||||
out.append('')
|
||||
out.append('[chooser]')
|
||||
out.append('')
|
||||
out.append('[debug]')
|
||||
out.append('GDM_EOF')
|
||||
out.append(' fi')
|
||||
out.append(' log " /etc/gdm3/custom.conf now has WaylandEnable=false"')
|
||||
out.append('fi')
|
||||
if restart_gdm:
|
||||
out.append('log "Restarting GDM to apply Wayland=false (will briefly disconnect any physical session)..."')
|
||||
out.append('systemctl restart gdm 2>&1 | head -5 || warn "gdm restart failed (no gdm installed?)"')
|
||||
else:
|
||||
out.append('log "Wayland disabled in custom.conf; will take effect after next gdm restart or reboot."')
|
||||
|
||||
out.append('log "Preparing VNC directory..."')
|
||||
out.append(f'USER_HOME="$(getent passwd {quote(user)} | cut -d: -f6 || echo /root)"')
|
||||
# If getent returned /root for a non-root user, the account is in a
|
||||
@@ -337,8 +389,18 @@ class Nginx(Generator):
|
||||
Field("root_path", "网站根目录", "text", default="/var/www/html",
|
||||
help="静态文件根目录,留空使用默认。"),
|
||||
Field("enable_ssl", "启用 HTTPS", "checkbox", default="no",
|
||||
help="启用后会在 443 监听并生成自签名证书。"),
|
||||
help="启用后会在 443 监听并生成 SSL 配置。"),
|
||||
Field("ssl_port", "HTTPS 端口", "number", default="443", min_=1, max_=65535),
|
||||
Field("ssl_cert_source", "SSL 证书来源", "select", default="self_signed",
|
||||
options=["self_signed", "existing"],
|
||||
help="self_signed=脚本生成自签证书(浏览器告警,适合测试);"
|
||||
"existing=使用已有证书(Let's Encrypt/商业证书)"),
|
||||
Field("ssl_cert_path", "证书路径", "text", default="",
|
||||
placeholder="/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
help="ssl_cert_source=existing 时必填。绝对路径。"),
|
||||
Field("ssl_key_path", "私钥路径", "text", default="",
|
||||
placeholder="/etc/letsencrypt/live/example.com/privkey.pem",
|
||||
help="ssl_cert_source=existing 时必填。绝对路径。"),
|
||||
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",
|
||||
@@ -355,14 +417,30 @@ class Nginx(Generator):
|
||||
root = p.get("root_path", "/var/www/html")
|
||||
ssl = bool_str(p.get("enable_ssl"))
|
||||
ssl_port = str(p.get("ssl_port", "443"))
|
||||
ssl_src = p.get("ssl_cert_source", "self_signed")
|
||||
ssl_cert_path = p.get("ssl_cert_path", "").strip()
|
||||
ssl_key_path = p.get("ssl_key_path", "").strip()
|
||||
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")
|
||||
|
||||
# SSL cert resolution
|
||||
if ssl:
|
||||
if ssl_src == "existing":
|
||||
if not ssl_cert_path or not ssl_key_path:
|
||||
raise ValueError(
|
||||
"ssl_cert_source=existing 时必须同时填写 ssl_cert_path "
|
||||
"和 ssl_key_path"
|
||||
)
|
||||
if not ssl_cert_path.startswith("/") or not ssl_key_path.startswith("/"):
|
||||
raise ValueError(
|
||||
"ssl_cert_path / ssl_key_path 必须是绝对路径(以 / 开头)"
|
||||
)
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing nginx..."')
|
||||
out.append('$PKG_INSTALL nginx openssl curl')
|
||||
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')
|
||||
@@ -379,7 +457,11 @@ class Nginx(Generator):
|
||||
'</body></html>\n'
|
||||
'HTML_EOF')
|
||||
out.append('')
|
||||
# Server block
|
||||
# Server block. NOTE: heredoc is quoted ('CONF_EOF') so nginx vars
|
||||
# like $uri / $host / $scheme / $request_uri / $proxy_add_x_forwarded_for
|
||||
# are NOT expanded by bash. With 'set -u' an unquoted heredoc + an
|
||||
# undefined nginx var would crash the entire script and leave a
|
||||
# truncated (often empty) conf file behind.
|
||||
server_block = []
|
||||
server_block.append(f' listen {port};\n listen [::]:{port};\n'
|
||||
f' server_name {sn};\n root {root};\n'
|
||||
@@ -401,26 +483,40 @@ class Nginx(Generator):
|
||||
' }\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')
|
||||
' 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(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:
|
||||
if ssl_src == "self_signed":
|
||||
# Default path: generate a self-signed cert at /etc/nginx/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'
|
||||
cert_path = f'/etc/nginx/ssl/{sn}.crt'
|
||||
key_path = f'/etc/nginx/ssl/{sn}.key'
|
||||
else: # existing
|
||||
# Use user-provided paths. We do NOT overwrite their files; if
|
||||
# the cert/key aren't readable, log a clear warning so they
|
||||
# know to deploy the cert (e.g. run certbot) first.
|
||||
out.append('log "Using user-provided SSL certificate..."')
|
||||
out.append(f'for f in {ssl_cert_path} {ssl_key_path}; do\n'
|
||||
f' [ -r "$f" ] || warn "SSL file not readable: $f (deploy your cert first, e.g. certbot)"\n'
|
||||
f'done')
|
||||
cert_path = ssl_cert_path
|
||||
key_path = ssl_key_path
|
||||
out.append(f"cat > /etc/nginx/conf.d/{sn}-ssl.conf <<'CONF_EOF'\n"
|
||||
'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' ssl_certificate {cert_path};\n'
|
||||
f' ssl_certificate_key {key_path};\n'
|
||||
f' root {root};\n'
|
||||
f' client_max_body_size {cmbs};\n'
|
||||
f' index index.html;\n'
|
||||
' 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'
|
||||
@@ -433,7 +529,7 @@ class Nginx(Generator):
|
||||
' location / {\n'
|
||||
' try_files $uri $uri/ =404;\n'
|
||||
' }\n')
|
||||
+ '}}\n'
|
||||
+ '}\n'
|
||||
'CONF_EOF')
|
||||
out.append('log "Testing nginx config..."')
|
||||
out.append('nginx -t')
|
||||
@@ -488,7 +584,7 @@ class HAProxy(Generator):
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing haproxy..."')
|
||||
out.append('$PKG_INSTALL 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('')
|
||||
@@ -571,7 +667,7 @@ class Keepalived(Generator):
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing keepalived..."')
|
||||
out.append('$PKG_INSTALL keepalived')
|
||||
out.append('PKG_INSTALL keepalived')
|
||||
out.append('')
|
||||
out.append('cat > /etc/keepalived/keepalived.conf <<CONF_EOF\n'
|
||||
'global_defs {\n'
|
||||
@@ -657,7 +753,7 @@ class Redis(Generator):
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing redis..."')
|
||||
out.append('$PKG_INSTALL redis-server')
|
||||
out.append('PKG_INSTALL redis-server')
|
||||
out.append('')
|
||||
out.append('cat > /etc/redis/redis.conf <<CFG_EOF\n'
|
||||
'bind ' + bind + '\n'
|
||||
@@ -767,7 +863,7 @@ class Tomcat(Generator):
|
||||
|
||||
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('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 + '"')
|
||||
@@ -814,11 +910,16 @@ class Docker(Generator):
|
||||
tags = ["docker", "container", "compose"]
|
||||
description = "一键安装 Docker Engine、buildx、docker-compose v2、配置 cgroupdriver。"
|
||||
fields = [
|
||||
Field("repo_region", "Docker 安装源", "select", default="auto",
|
||||
options=["auto", "official", "cn"],
|
||||
help="auto=自动探测;official=官方 download.docker.com;"
|
||||
"cn=国内镜像(mirrors.tuna.tsinghua.edu.cn)"),
|
||||
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"),
|
||||
default="https://docker.mirrors.ustc.edu.cn",
|
||||
help="如 https://mirror.ccs.tencentyun.com;"
|
||||
"留空则不配置镜像。"),
|
||||
Field("cgroup_driver", "Cgroup Driver", "select", default="systemd",
|
||||
options=["systemd", "cgroupfs"]),
|
||||
Field("log_driver", "日志驱动", "select", default="json-file",
|
||||
@@ -832,30 +933,76 @@ class Docker(Generator):
|
||||
|
||||
def render(self, p):
|
||||
usr = p.get("docker_user", "root")
|
||||
mirror = p.get("registry_mirror", "https://mirror.ccs.tencentyun.com")
|
||||
mirror = p.get("registry_mirror", "https://docker.mirrors.ustc.edu.cn")
|
||||
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))
|
||||
region = p.get("repo_region", "auto")
|
||||
|
||||
out = [bash_header(self.title)]
|
||||
out.append('log "Installing Docker..."')
|
||||
|
||||
# The auto-detection block. Resolves DOCKER_REPO_BASE at runtime to
|
||||
# either the official download.docker.com URL or the Tuna mirror.
|
||||
# We embed the literal base URLs into the script (not env vars at the
|
||||
# top of the file) so that `bash -n` and readability stay clean.
|
||||
official_apt = "https://download.docker.com/linux/ubuntu"
|
||||
tuna_apt = "https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu"
|
||||
official_yum = "https://download.docker.com/linux/centos"
|
||||
tuna_yum = "https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos"
|
||||
|
||||
if region == "auto":
|
||||
out.append(
|
||||
'# ---- auto-detect reachable Docker repo ----\n'
|
||||
'if curl -fsS --max-time 5 -o /dev/null '
|
||||
f'"{official_apt}/gpg" 2>/dev/null || '
|
||||
f'curl -fsSI --max-time 5 -o /dev/null "{official_yum}/docker-ce.repo" 2>/dev/null; then\n'
|
||||
' DOCKER_REPO_APT="' + official_apt + '"\n'
|
||||
' DOCKER_REPO_YUM="' + official_yum + '"\n'
|
||||
' log "Repo: official (download.docker.com reachable)"\n'
|
||||
'else\n'
|
||||
' DOCKER_REPO_APT="' + tuna_apt + '"\n'
|
||||
' DOCKER_REPO_YUM="' + tuna_yum + '"\n'
|
||||
' warn "Official docker repo unreachable — falling back to Tuna mirror"\n'
|
||||
'fi'
|
||||
)
|
||||
elif region == "cn":
|
||||
out.append(
|
||||
f'DOCKER_REPO_APT="{tuna_apt}"\n'
|
||||
f'DOCKER_REPO_YUM="{tuna_yum}"\n'
|
||||
'log "Repo: cn (Tuna mirror)"'
|
||||
)
|
||||
else: # official
|
||||
out.append(
|
||||
f'DOCKER_REPO_APT="{official_apt}"\n'
|
||||
f'DOCKER_REPO_YUM="{official_yum}"\n'
|
||||
'log "Repo: official (forced)"'
|
||||
)
|
||||
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL ca-certificates curl gnupg lsb-release\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'
|
||||
' curl -fsSL "${DOCKER_REPO_APT}/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'
|
||||
' echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] ${DOCKER_REPO_APT} $(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'
|
||||
' 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'
|
||||
' PKG_INSTALL yum-utils\n'
|
||||
' # yum-config-manager writes /etc/yum.repos.d/docker-ce.repo with the\n'
|
||||
' # official gpgkey URL embedded; dnf will fetch it on first install.\n'
|
||||
' # We use --save-to-rpmdb-friendly repos for both classic yum and modern dnf.\n'
|
||||
' if command -v yum-config-manager >/dev/null 2>&1; then\n'
|
||||
' yum-config-manager --add-repo "${DOCKER_REPO_YUM}/docker-ce.repo"\n'
|
||||
' else\n'
|
||||
' dnf config-manager --add-repo "${DOCKER_REPO_YUM}/docker-ce.repo"\n'
|
||||
' fi\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')
|
||||
@@ -979,7 +1126,7 @@ class Zookeeper(Generator):
|
||||
|
||||
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('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')
|
||||
|
||||
@@ -150,17 +150,17 @@ class Grafana(Generator):
|
||||
out.append('log "Installing Grafana..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL -y software-properties-common\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'
|
||||
' 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'
|
||||
' PKG_INSTALL grafana\n'
|
||||
' ;;\n'
|
||||
'esac')
|
||||
out.append('systemctl enable --now grafana-server')
|
||||
|
||||
@@ -43,7 +43,7 @@ class WireGuard(Generator):
|
||||
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('PKG_INSTALL wireguard qrencode')
|
||||
out.append('')
|
||||
# server keypair
|
||||
if not sk:
|
||||
@@ -178,7 +178,7 @@ class Dnsmasq(Generator):
|
||||
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('PKG_INSTALL dnsmasq')
|
||||
out.append('cp /etc/dnsmasq.conf /etc/dnsmasq.conf.bak.$(date +%s) || true')
|
||||
cfg = []
|
||||
cfg.append('listen-address=' + listen)
|
||||
@@ -226,7 +226,7 @@ class OpenVPN(Generator):
|
||||
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('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')
|
||||
|
||||
+36
-26
@@ -46,10 +46,10 @@ class Python(Generator):
|
||||
|
||||
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'
|
||||
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'
|
||||
' || 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')
|
||||
@@ -192,7 +192,7 @@ class GCC(Generator):
|
||||
|
||||
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('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')
|
||||
@@ -230,7 +230,7 @@ class Make(Generator):
|
||||
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('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')
|
||||
@@ -304,9 +304,19 @@ class GoLang(Generator):
|
||||
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')
|
||||
# Go 官方 tarball 解压后固定得到 <父目录>/go
|
||||
parent = d.rsplit('/', 1)[0] if '/' in d else '/opt'
|
||||
extracted = parent.rstrip('/') + '/go'
|
||||
if extracted == d.rstrip('/'):
|
||||
# install_dir 恰好等于解压目标(/opt/go), 无需再 mv
|
||||
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('tar -C ' + parent + ' -xzf go' + ver + '.linux-amd64.tar.gz')
|
||||
else:
|
||||
# install_dir 与解压目录不同(如 /usr/local/go), 先解压到临时父目录再移动
|
||||
out.append('rm -rf ' + d)
|
||||
out.append('rm -rf ' + parent + '/go')
|
||||
out.append('tar -C ' + parent + ' -xzf go' + ver + '.linux-amd64.tar.gz')
|
||||
out.append('mv ' + extracted + ' ' + 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')
|
||||
@@ -410,10 +420,10 @@ class PHP(Generator):
|
||||
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'
|
||||
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'
|
||||
' || 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')
|
||||
@@ -466,9 +476,9 @@ class Ruby(Generator):
|
||||
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'
|
||||
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'
|
||||
' || 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')
|
||||
@@ -510,8 +520,8 @@ class RustLang(Generator):
|
||||
|
||||
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')
|
||||
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')
|
||||
@@ -557,15 +567,15 @@ class PostgreSQL(Generator):
|
||||
out.append('log "Installing PostgreSQL ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL wget gnupg lsb-release ca-certificates\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'
|
||||
' 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'
|
||||
' 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')
|
||||
@@ -611,22 +621,22 @@ class MySQL(Generator):
|
||||
out.append('log "Installing MySQL ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL wget gnupg lsb-release\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'
|
||||
' 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'
|
||||
' 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'
|
||||
' 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')
|
||||
@@ -672,14 +682,14 @@ class MongoDB(Generator):
|
||||
out.append('log "Installing MongoDB ' + ver + '..."')
|
||||
out.append('case "$PKG" in\n'
|
||||
' apt-get)\n'
|
||||
' $PKG_INSTALL wget gnupg curl\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'
|
||||
' PKG_INSTALL mongodb-org\n'
|
||||
' ;;\n'
|
||||
' yum|dnf)\n'
|
||||
' cat > /etc/yum.repos.d/mongodb-org-' + ver + '.repo <<REPO_EOF\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'
|
||||
@@ -687,7 +697,7 @@ class MongoDB(Generator):
|
||||
'enabled=1\n'
|
||||
'gpgkey=https://www.mongodb.org/static/pgp/server-' + ver + '.asc\n'
|
||||
'REPO_EOF\n'
|
||||
' $PKG_INSTALL mongodb-org\n'
|
||||
' PKG_INSTALL mongodb-org\n'
|
||||
' ;;\n'
|
||||
'esac')
|
||||
out.append('systemctl enable --now mongod')
|
||||
|
||||
@@ -49,7 +49,7 @@ class Firewall(Generator):
|
||||
fw = []
|
||||
fw.append('case "$DISTRO_ID" in')
|
||||
fw.append(' ubuntu|debian)')
|
||||
fw.append(' $PKG_INSTALL ufw')
|
||||
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')
|
||||
@@ -64,7 +64,7 @@ class Firewall(Generator):
|
||||
fw.append(' ufw --force enable')
|
||||
fw.append(' ;;')
|
||||
fw.append(' centos|rhel|rocky|almalinux|ol|fedora)')
|
||||
fw.append(' $PKG_INSTALL firewalld')
|
||||
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')
|
||||
@@ -78,7 +78,7 @@ class Firewall(Generator):
|
||||
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(' 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')
|
||||
@@ -149,7 +149,7 @@ class Chrony(Generator):
|
||||
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('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'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Flask>=3.0,<4.0
|
||||
+2
-2
@@ -5,8 +5,8 @@ After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/root/shell-gen
|
||||
ExecStart=/usr/bin/python3 /root/shell-gen/app.py
|
||||
WorkingDirectory=/fs/1000/ftp/Project/shell-gen
|
||||
ExecStart=/usr/bin/python3 /fs/1000/ftp/Project/shell-gen/app.py
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
Environment=PORT=5099
|
||||
|
||||
Reference in New Issue
Block a user