Files
shell-gen/generators/monitoring.py
T
Your Name 66aefd6161 fix(generators): call PKG_INSTALL without '$' under 'set -u'
In bash with 'set -u', an undefined variable triggers an unbound-variable
error. Functions don't satisfy '$FUNCNAME' expansion under set -u, so
the previous fix that kept '$PKG_INSTALL pkg...' as the call syntax
broke every call site:

    docker.sh: line 53: PKG_INSTALL: unbound variable

Fix: drop the leading '$' in all 47 call sites across 6 generator
files. 'PKG_INSTALL pkg...' is a normal command/function lookup and
behaves identically under set -u or not.

Verified: web UI served docker.sh now has 'PKG_INSTALL' (no $);
'bash -n' passes; smoke run with stubbed PATH hits real line-53 call
site, mock apt-get receives correct args, exit 0, no unbound variable.
Sampled 6 other generators also syntax-OK with $PKG_INSTALL count 0.
2026-08-07 13:20:18 +08:00

227 lines
11 KiB
Python

"""
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())