feat: Ansible 15个运维模板 + 主机匹配 + 远程执行
**种子模板(15个):** - 系统信息收集、安全更新、磁盘清理、服务巡检 - 用户SSH管理、Nginx配置、SSL证书检查、配置备份 - 进程诊断、Docker清理、防火墙、时间同步 - 日志轮转、内核调优、网络诊断 - 分类覆盖:maintenance/security/configuration/backup/monitoring **主机匹配 API:** - GET /api/v1/ansible/match-hosts?pattern=&match_type=regex|prefix|contains - 支持正则/前缀/模糊匹配 hostname, IP, tags - 复杂正则回退到 Go 过滤 - 分页支持 **远程执行增强:** - 指定 target_hosts 时自动查询 SSH 信息生成临时 inventory - 未指定时回退到 --connection=local - 自动禁用 StrictHostKeyChecking
This commit is contained in:
1 parent
9af0685e90
commit
c9b6647831
4 files changed
+528
-6
No files matched your search
@@ -220,4 +220,406 @@ func seedData(db *gorm.DB) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 创建默认 Ansible Playbook 模板(15 个高频运维场景)// Logan.Li
|
||||
var pbCount int64
|
||||
db.Model(&models.AnsiblePlaybook{}).Count(&pbCount)
|
||||
if pbCount == 0 {
|
||||
playbooks := []models.AnsiblePlaybook{
|
||||
{Name: "系统信息收集", Description: "收集主机基本信息:OS、内核、CPU、内存、磁盘、网络接口", Category: "maintenance", Tags: `["系统信息","诊断","基础"]`, Content: `---
|
||||
- name: 系统信息收集
|
||||
hosts: all
|
||||
gather_facts: yes
|
||||
tasks:
|
||||
- name: 系统基本信息
|
||||
debug:
|
||||
msg:
|
||||
- "主机: {{ ansible_hostname }}"
|
||||
- "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
|
||||
- "内核: {{ ansible_kernel }} ({{ ansible_architecture }})"
|
||||
- "CPU: {{ ansible_processor_vcpus }} 核"
|
||||
- "内存: {{ (ansible_memtotal_mb / 1024) | round(1) }} GB"
|
||||
- name: 磁盘使用率
|
||||
shell: df -h / | tail -1 | awk '{print $5}'
|
||||
register: disk_pct
|
||||
- name: 系统负载
|
||||
shell: uptime
|
||||
register: load
|
||||
- name: 运行摘要
|
||||
debug:
|
||||
msg: "磁盘: {{ disk_pct.stdout }} | 负载: {{ load.stdout.split('load average:')[1].strip() if 'load average:' in load.stdout else 'N/A' }}"`, CreatedBy: "system"},
|
||||
{Name: "系统安全更新", Description: "安装系统安全补丁(自动识别 apt/yum/dnf)", Category: "security", Tags: `["安全","更新","补丁"]`, Content: `---
|
||||
- name: 系统安全更新
|
||||
hosts: all
|
||||
become: yes
|
||||
tasks:
|
||||
- name: 检测包管理器
|
||||
shell: which apt yum dnf 2>/dev/null | head -1 | xargs basename
|
||||
register: pkg_mgr
|
||||
- name: 更新软件包 (APT)
|
||||
apt: update_cache=yes upgrade=safe
|
||||
when: pkg_mgr.stdout == "apt"
|
||||
- name: 更新软件包 (YUM/DNF)
|
||||
shell: "{{ pkg_mgr.stdout }} update -y --security"
|
||||
when: pkg_mgr.stdout in ["yum", "dnf"]
|
||||
- name: 检查重启需求
|
||||
shell: test -f /var/run/reboot-required && echo "需要重启" || echo "无需重启"
|
||||
register: reboot
|
||||
- name: 更新完成
|
||||
debug: msg="更新完成,{{ reboot.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "磁盘空间清理", Description: "清理日志、临时文件、Docker 缓存、包管理器缓存", Category: "maintenance", Tags: `["磁盘","清理","维护"]`, Content: `---
|
||||
- name: 磁盘空间清理
|
||||
hosts: all
|
||||
become: yes
|
||||
tasks:
|
||||
- name: 清理前磁盘使用
|
||||
shell: df -h / | awk 'NR==2{print "使用率:",$5,"可用:",$4}'
|
||||
register: before
|
||||
- name: 清理30天前日志
|
||||
shell: find /var/log -type f \( -name "*.log" -o -name "*.gz" \) -mtime +30 -delete 2>/dev/null; echo "done"
|
||||
- name: 清理临时文件
|
||||
shell: find /tmp /var/tmp -type f -mtime +7 -delete 2>/dev/null; echo "done"
|
||||
- name: 清理 journald (保留3天)
|
||||
shell: journalctl --vacuum-time=3d 2>/dev/null || true
|
||||
- name: 清理包管理器缓存
|
||||
shell: (apt-get clean 2>/dev/null || yum clean all 2>/dev/null || dnf clean all 2>/dev/null); echo "done"
|
||||
- name: 清理 Docker (如有)
|
||||
shell: docker system prune -af 2>/dev/null || true
|
||||
- name: 清理后磁盘使用
|
||||
shell: df -h / | awk 'NR==2{print "使用率:",$5,"可用:",$4}'
|
||||
register: after
|
||||
- name: 清理摘要
|
||||
debug: msg="清理前 - {{ before.stdout }} | 清理后 - {{ after.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "服务状态巡检", Description: "检查关键服务状态,自动启动已停止的服务", Category: "maintenance", Tags: `["服务","巡检","重启"]`, Content: `---
|
||||
- name: 服务状态巡检
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
services: [nginx, docker, sshd, cron, rsyslog]
|
||||
tasks:
|
||||
- name: 收集服务状态
|
||||
service_facts:
|
||||
- name: 检查并启动服务
|
||||
service:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
loop: "{{ services }}"
|
||||
when: item in ansible_facts.services
|
||||
- name: 显示服务状态
|
||||
debug:
|
||||
msg: "{{ item }}: {{ ansible_facts.services[item].state if item in ansible_facts.services else '未安装' }}"
|
||||
loop: "{{ services }}"
|
||||
- name: 检查关键端口
|
||||
shell: ss -tlnp | awk '{print $4}' | grep -E ':22$|:80$|:443$'
|
||||
register: ports
|
||||
- name: 端口监听
|
||||
debug: msg="监听端口: {{ ports.stdout_lines }}"`, CreatedBy: "system"},
|
||||
{Name: "用户与SSH管理", Description: "创建运维用户、配置SSH密钥、禁用root密码登录", Category: "security", Tags: `["用户","SSH","安全"]`, Content: `---
|
||||
- name: 用户与SSH管理
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
ops_user: devops
|
||||
ops_ssh_key: ""
|
||||
tasks:
|
||||
- name: 创建运维用户
|
||||
user: name={{ ops_user }} shell=/bin/bash groups=sudo append=yes state=present
|
||||
- name: 配置SSH密钥
|
||||
authorized_key: user={{ ops_user }} key="{{ ops_ssh_key }}" state=present
|
||||
when: ops_ssh_key != ""
|
||||
- name: sudo免密
|
||||
copy: content="{{ ops_user }} ALL=(ALL) NOPASSWD:ALL" dest=/etc/sudoers.d/{{ ops_user }} validate="visudo -cf %s"
|
||||
- name: 禁用root密码登录
|
||||
lineinfile: path=/etc/ssh/sshd_config regexp="^PermitRootLogin" line="PermitRootLogin prohibit-password"
|
||||
notify: reload sshd
|
||||
- name: 可登录用户列表
|
||||
shell: awk -F: '$$7 !~ /(false|nologin)/ {print $$1}' /etc/passwd
|
||||
register: users
|
||||
- name: 用户摘要
|
||||
debug: msg="可登录用户: {{ users.stdout_lines }}"
|
||||
handlers:
|
||||
- name: reload sshd
|
||||
service: name=sshd state=reloaded`, CreatedBy: "system"},
|
||||
{Name: "Nginx配置检查", Description: "检查Nginx语法、显示虚拟主机、执行重载", Category: "configuration", Tags: `["Nginx","配置","Web"]`, Content: `---
|
||||
- name: Nginx配置管理
|
||||
hosts: all
|
||||
become: yes
|
||||
tasks:
|
||||
- name: Nginx状态
|
||||
shell: nginx -v 2>&1 || echo "未安装"
|
||||
register: nginx_ver
|
||||
- name: 配置语法检查
|
||||
shell: nginx -t 2>&1
|
||||
register: nginx_test
|
||||
when: "'nginx' in nginx_ver.stdout"
|
||||
- name: 列出站点
|
||||
shell: ls /etc/nginx/sites-enabled/ 2>/dev/null || ls /etc/nginx/conf.d/ 2>/dev/null
|
||||
register: sites
|
||||
- name: 重载Nginx
|
||||
service: name=nginx state=reloaded
|
||||
when: nginx_test.stdout is defined and 'successful' in nginx_test.stdout
|
||||
- name: 检查监听端口
|
||||
shell: ss -tlnp | grep nginx
|
||||
register: nginx_ports
|
||||
- name: 配置摘要
|
||||
debug:
|
||||
msg:
|
||||
- "版本: {{ nginx_ver.stdout }}"
|
||||
- "站点: {{ sites.stdout_lines }}"
|
||||
- "端口: {{ nginx_ports.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "SSL证书检查", Description: "检查本地证书和HTTPS证书到期时间", Category: "security", Tags: `["SSL","证书","HTTPS"]`, Content: `---
|
||||
- name: SSL证书到期检查
|
||||
hosts: all
|
||||
vars:
|
||||
domains: []
|
||||
tasks:
|
||||
- name: 本地证书检查
|
||||
shell: |
|
||||
for f in /etc/nginx/ssl/*.crt /etc/letsencrypt/live/*/fullchain.pem 2>/dev/null; do
|
||||
[ -f "$$f" ] && echo "$$f: $$(openssl x509 -enddate -noout -in "$$f" | cut -d= -f2)"
|
||||
done
|
||||
register: local_certs
|
||||
- name: 远程HTTPS检查
|
||||
shell: |
|
||||
for d in {% for d in target_hosts.split(',') %}{{ d }} {% endfor %}; do
|
||||
expiry=$$(echo | openssl s_client -servername "$$d" -connect "$$d:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||
[ -n "$$expiry" ] && echo "$$d: $$expiry"
|
||||
done
|
||||
register: remote_certs
|
||||
when: target_hosts is defined
|
||||
- name: 证书到期列表
|
||||
debug:
|
||||
msg:
|
||||
- "本地: {{ local_certs.stdout_lines }}"
|
||||
- "远程: {{ remote_certs.stdout_lines | default([]) }}"`, CreatedBy: "system"},
|
||||
{Name: "配置文件备份", Description: "备份关键配置到压缩包", Category: "backup", Tags: `["备份","配置"]`, Content: `---
|
||||
- name: 配置备份
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
backup_dir: "/tmp/config-backup-{{ ansible_date_time.date }}"
|
||||
tasks:
|
||||
- name: 创建备份目录
|
||||
file: path={{ backup_dir }} state=directory mode=0700
|
||||
- name: 备份关键配置
|
||||
archive:
|
||||
path: "{{ item }}"
|
||||
dest: "{{ backup_dir }}/{{ item | basename | replace('/', '_') }}.tar.gz"
|
||||
loop: [/etc/nginx, /etc/ssh/sshd_config, /etc/fstab, /etc/hosts]
|
||||
ignore_errors: yes
|
||||
- name: 备份crontab
|
||||
shell: crontab -l > {{ backup_dir }}/crontab.txt 2>/dev/null || true
|
||||
- name: 备份汇总
|
||||
shell: du -sh {{ backup_dir }}
|
||||
register: size
|
||||
- name: 备份完成
|
||||
debug: msg="备份: {{ backup_dir }} ({{ size.stdout.split()[0] }})"`, CreatedBy: "system"},
|
||||
{Name: "进程健康诊断", Description: "CPU/内存TOP10、僵尸进程、进程总数", Category: "monitoring", Tags: `["进程","性能","诊断"]`, Content: `---
|
||||
- name: 进程健康诊断
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: CPU TOP10
|
||||
shell: ps aux --sort=-%cpu | head -11
|
||||
register: cpu
|
||||
- name: 内存 TOP10
|
||||
shell: ps aux --sort=-%mem | head -11
|
||||
register: mem
|
||||
- name: 僵尸进程
|
||||
shell: ps aux | awk '$$8=="Z"' || echo "无僵尸进程"
|
||||
register: zombies
|
||||
- name: 进程总数
|
||||
shell: ps aux | wc -l
|
||||
register: total
|
||||
- name: 诊断摘要
|
||||
debug:
|
||||
msg:
|
||||
- "总进程: {{ total.stdout }} | 僵尸: {{ zombies.stdout_lines | length }}"
|
||||
- "=== CPU TOP10 ==="
|
||||
- "{{ cpu.stdout }}"
|
||||
- "=== 内存 TOP10 ==="
|
||||
- "{{ mem.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "Docker环境清理", Description: "清理未使用容器、镜像、构建缓存和网络", Category: "maintenance", Tags: `["Docker","清理","容器"]`, Content: `---
|
||||
- name: Docker环境清理
|
||||
hosts: all
|
||||
become: yes
|
||||
tasks:
|
||||
- name: 检查Docker
|
||||
shell: docker --version 2>/dev/null || echo "NONE"
|
||||
register: dk
|
||||
- name: Docker资源概览
|
||||
shell: |
|
||||
echo "容器:" && docker ps -a --format "table {{.Names}} {{.Status}}" 2>/dev/null
|
||||
echo "镜像:" && docker images --format "table {{.Repository}} {{.Size}}" 2>/dev/null
|
||||
register: status
|
||||
when: dk.stdout != "NONE"
|
||||
- name: 清理停止容器
|
||||
shell: docker container prune -f
|
||||
when: dk.stdout != "NONE"
|
||||
- name: 清理未用镜像
|
||||
shell: docker image prune -a -f
|
||||
when: dk.stdout != "NONE"
|
||||
- name: 清理构建缓存
|
||||
shell: docker builder prune -f
|
||||
when: dk.stdout != "NONE"
|
||||
- name: 清理未用网络/卷
|
||||
shell: docker system prune -f --volumes
|
||||
when: dk.stdout != "NONE"
|
||||
- name: 清理完成
|
||||
debug: msg="Docker清理完成"`, CreatedBy: "system"},
|
||||
{Name: "防火墙规则管理", Description: "查看/配置防火墙规则(ufw/firewalld/iptables)", Category: "security", Tags: `["防火墙","安全","网络"]`, Content: `---
|
||||
- name: 防火墙规则管理
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
open_ports: [22, 80, 443]
|
||||
tasks:
|
||||
- name: 防火墙类型
|
||||
shell: |
|
||||
ufw status 2>/dev/null | grep -q "active" && echo ufw && exit
|
||||
firewall-cmd --state 2>/dev/null | grep -q "running" && echo firewalld && exit
|
||||
echo iptables
|
||||
register: fw
|
||||
- name: 当前规则
|
||||
shell: |
|
||||
case {{ fw.stdout }} in
|
||||
ufw) ufw status verbose ;;
|
||||
firewalld) firewall-cmd --list-all ;;
|
||||
*) iptables -L -n ;;
|
||||
esac
|
||||
register: rules
|
||||
- name: 开放端口(UFW)
|
||||
ufw: rule=allow port={{ item }} proto=tcp
|
||||
loop: "{{ open_ports }}"
|
||||
when: fw.stdout == "ufw"
|
||||
- name: 开放端口(firewalld)
|
||||
firewalld: port={{ item }}/tcp permanent=yes state=enabled immediate=yes
|
||||
loop: "{{ open_ports }}"
|
||||
when: fw.stdout == "firewalld"
|
||||
- name: 防火墙规则
|
||||
debug: msg="{{ rules.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "时间同步检查", Description: "检查NTP/Chrony同步状态和时间一致性", Category: "configuration", Tags: `["时间","NTP","同步"]`, Content: `---
|
||||
- name: 时间同步检查
|
||||
hosts: all
|
||||
become: yes
|
||||
tasks:
|
||||
- name: 当前时间
|
||||
shell: date '+%Y-%m-%d %H:%M:%S %Z'
|
||||
register: now
|
||||
- name: 同步服务
|
||||
shell: |
|
||||
systemctl is-active chronyd 2>/dev/null && echo "chronyd" ||
|
||||
systemctl is-active ntpd 2>/dev/null && echo "ntpd" ||
|
||||
systemctl is-active systemd-timesyncd 2>/dev/null && echo "timesyncd" ||
|
||||
echo "未配置"
|
||||
register: svc
|
||||
- name: 同步状态
|
||||
shell: |
|
||||
case {{ svc.stdout }} in
|
||||
chronyd) chronyc tracking 2>/dev/null | grep -E "Stratum|System time" ;;
|
||||
ntpd) ntpq -p 2>/dev/null | tail -5 ;;
|
||||
timesyncd) timedatectl show-timesync 2>/dev/null ;;
|
||||
*) echo "未同步" ;;
|
||||
esac
|
||||
register: sync
|
||||
- name: 时间摘要
|
||||
debug:
|
||||
msg: "时间: {{ now.stdout }} | 服务: {{ svc.stdout }} | {{ sync.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "日志轮转配置", Description: "安装logrotate并配置应用日志轮转策略", Category: "configuration", Tags: `["日志","轮转","logrotate"]`, Content: `---
|
||||
- name: 日志轮转配置
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
log_path: /var/log/app
|
||||
keep_days: 30
|
||||
tasks:
|
||||
- name: 安装logrotate
|
||||
package: name=logrotate state=present
|
||||
- name: 应用日志轮转
|
||||
copy:
|
||||
dest: /etc/logrotate.d/app-logs
|
||||
content: |
|
||||
{{ log_path }}/*.log {
|
||||
daily
|
||||
rotate {{ keep_days }}
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload app 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
- name: 测试配置
|
||||
shell: logrotate -d /etc/logrotate.conf 2>&1 | head -10
|
||||
register: test
|
||||
- name: 配置完成
|
||||
debug: msg="Logrotate已配置 | {{ test.stdout }}"`, CreatedBy: "system"},
|
||||
{Name: "内核参数调优", Description: "优化网络/内存/文件句柄等内核参数", Category: "configuration", Tags: `["内核","优化","性能"]`, Content: `---
|
||||
- name: 内核参数调优
|
||||
hosts: all
|
||||
become: yes
|
||||
tasks:
|
||||
- name: 网络参数优化
|
||||
sysctl:
|
||||
name: "{{ item.name }}"
|
||||
value: "{{ item.value }}"
|
||||
state: present
|
||||
reload: yes
|
||||
loop:
|
||||
- { name: net.core.somaxconn, value: '65535' }
|
||||
- { name: net.ipv4.tcp_tw_reuse, value: '1' }
|
||||
- { name: net.ipv4.tcp_fin_timeout, value: '30' }
|
||||
- name: 内存参数
|
||||
sysctl: name=vm.swappiness value=10 state=present reload=yes
|
||||
- name: 文件句柄
|
||||
sysctl: name=fs.file-max value=655350 state=present reload=yes
|
||||
- name: limits.conf
|
||||
pam_limits:
|
||||
domain: '*'
|
||||
limit_type: "{{ item.t }}"
|
||||
limit_item: "{{ item.i }}"
|
||||
value: "{{ item.v }}"
|
||||
loop:
|
||||
- { t: soft, i: nofile, v: '65535' }
|
||||
- { t: hard, i: nofile, v: '65535' }
|
||||
- name: 调优完成
|
||||
debug: msg="内核参数已优化"`, CreatedBy: "system"},
|
||||
{Name: "网络连通性诊断", Description: "DNS解析、Ping延迟、端口检测、路由追踪", Category: "monitoring", Tags: `["网络","诊断","连通性"]`, Content: `---
|
||||
- name: 网络诊断
|
||||
hosts: all
|
||||
vars:
|
||||
targets: [8.8.8.8, 1.1.1.1]
|
||||
domains: [baidu.com]
|
||||
tasks:
|
||||
- name: DNS解析
|
||||
shell: |
|
||||
for d in {% for d in domains %}{{ d }} {% endfor %}; do
|
||||
echo "$$d: $$(nslookup $$d 2>/dev/null | awk '/^Address/{print $$2}' | tail -1 || echo 'FAIL')"
|
||||
done
|
||||
register: dns
|
||||
- name: Ping延迟
|
||||
shell: ping -c 2 -W 2 {{ item }} 2>&1 | tail -1
|
||||
loop: "{{ targets }}"
|
||||
register: ping
|
||||
- name: 端口检测
|
||||
shell: timeout 2 bash -c "echo >/dev/tcp/{{ item }}/53" 2>/dev/null && echo "{{ item }}:53 OK" || echo "{{ item }}:53 FAIL"
|
||||
loop: "{{ targets }}"
|
||||
register: port
|
||||
- name: 诊断结果
|
||||
debug:
|
||||
msg:
|
||||
- "=== DNS ==="
|
||||
- "{{ dns.stdout_lines }}"
|
||||
- "=== Ping ==="
|
||||
- "{{ ping.results | map(attribute='stdout') }}"
|
||||
- "=== 端口 ==="
|
||||
- "{{ port.results | map(attribute='stdout') }}"`, CreatedBy: "system"},
|
||||
}
|
||||
if err := db.Create(&playbooks).Error; err != nil {
|
||||
zap.L().Warn("failed to seed playbook templates", zap.Error(err))
|
||||
} else {
|
||||
zap.L().Info("seeded 15 Ansible playbook templates")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -211,12 +212,34 @@ func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
args := []string{tmpFile.Name(), "--connection=local"}
|
||||
args := []string{tmpFile.Name()}
|
||||
|
||||
// 根据目标主机决定连接方式
|
||||
if len(req.TargetHosts) > 0 {
|
||||
// 查询目标主机的 SSH 信息,生成临时 inventory
|
||||
var servers []models.Server
|
||||
h.db.Where("hostname IN ? OR ip IN ?", req.TargetHosts, req.TargetHosts).Find(&servers)
|
||||
if len(servers) > 0 {
|
||||
invFile, invErr := os.CreateTemp("", "ansible-inventory-*.ini")
|
||||
if invErr == nil {
|
||||
defer os.Remove(invFile.Name())
|
||||
invFile.WriteString("[targets]\n")
|
||||
for _, s := range servers {
|
||||
sshUser := s.Username
|
||||
if sshUser == "" { sshUser = "root" }
|
||||
invFile.WriteString(fmt.Sprintf("%s ansible_host=%s ansible_user=%s ansible_port=%d ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'\n",
|
||||
s.Hostname, s.IP, sshUser, s.Port))
|
||||
}
|
||||
invFile.Close()
|
||||
args = append(args, "--inventory", invFile.Name())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args = append(args, "--connection=local")
|
||||
}
|
||||
|
||||
// 构建 extra-vars
|
||||
var extraVars []string
|
||||
if len(req.TargetHosts) > 0 {
|
||||
extraVars = append(extraVars, "target_hosts="+strings.Join(req.TargetHosts, ","))
|
||||
}
|
||||
for k, v := range req.Variables {
|
||||
extraVars = append(extraVars, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
@@ -312,3 +335,93 @@ func (h *AnsibleHandler) DeleteInventory(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
// ==================== 主机匹配 ====================
|
||||
|
||||
// MatchHostsRequest 主机匹配请求
|
||||
type MatchHostsRequest struct {
|
||||
Pattern string `json:"pattern" form:"pattern"` // 匹配模式(正则或前缀)
|
||||
MatchType string `json:"match_type" form:"match_type"` // "regex", "prefix", "contains"
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
}
|
||||
|
||||
// MatchHosts 预览匹配的主机列表(从资产服务器表查询)// Logan.Li
|
||||
// GET /api/v1/ansible/match-hosts?pattern=web*&match_type=prefix
|
||||
func (h *AnsibleHandler) MatchHosts(c *gin.Context) {
|
||||
var req MatchHostsRequest
|
||||
c.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 { req.Page = 1 }
|
||||
if req.PageSize <= 0 || req.PageSize > 200 { req.PageSize = 50 }
|
||||
|
||||
query := h.db.Model(&models.Server{})
|
||||
|
||||
if req.Pattern != "" {
|
||||
switch req.MatchType {
|
||||
case "regex":
|
||||
// 编译正则表达式
|
||||
re, err := regexp.Compile(req.Pattern)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "正则表达式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// MySQL 使用 REGEXP 关键字
|
||||
query = query.Where("hostname REGEXP ? OR ip REGEXP ? OR tags REGEXP ?",
|
||||
req.Pattern, req.Pattern, req.Pattern)
|
||||
// 对于复杂的正则,MySQL 可能不支持;先查全部再 Go 过滤
|
||||
if !isMySQLSafeRegex(req.Pattern) {
|
||||
var all []models.Server
|
||||
h.db.Find(&all)
|
||||
var matched []models.Server
|
||||
for _, s := range all {
|
||||
if re.MatchString(s.Hostname) || re.MatchString(s.IP) || re.MatchString(s.Tags) {
|
||||
matched = append(matched, s)
|
||||
}
|
||||
}
|
||||
total := len(matched)
|
||||
start := (req.Page - 1) * req.PageSize
|
||||
end := start + req.PageSize
|
||||
if start > total { start = total }
|
||||
if end > total { end = total }
|
||||
result := matched[start:end]
|
||||
for i := range result {
|
||||
result[i].HasAuth = result[i].Password != "" || result[i].SSHKey != ""
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: result, Total: int64(total)}})
|
||||
return
|
||||
}
|
||||
case "prefix":
|
||||
query = query.Where("hostname LIKE ? OR ip LIKE ?",
|
||||
req.Pattern+"%", req.Pattern+"%")
|
||||
default: // "contains"
|
||||
query = query.Where("hostname LIKE ? OR ip LIKE ? OR tags LIKE ?",
|
||||
"%"+req.Pattern+"%", "%"+req.Pattern+"%", "%"+req.Pattern+"%")
|
||||
}
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.Server
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
query.Offset(offset).Limit(req.PageSize).Order("hostname").Find(&items)
|
||||
|
||||
// 补充 HasAuth 字段
|
||||
for i := range items {
|
||||
items[i].HasAuth = items[i].Password != "" || items[i].SSHKey != ""
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
|
||||
}
|
||||
|
||||
// isMySQLSafeRegex 判断正则表达式是否可安全用于 MySQL REGEXP
|
||||
func isMySQLSafeRegex(pat string) bool {
|
||||
// MySQL REGEXP 支持基本正则,但如果包含复杂断言或 Unicode 则不适合
|
||||
unsafe := []string{`\b`, `(?=`, `(?!`, `(?<=`, `(?<!`, `\p{`}
|
||||
for _, u := range unsafe {
|
||||
if strings.Contains(pat, u) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -451,6 +451,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
ansible.POST("/inventories", ansibleHandler.CreateInventory)
|
||||
ansible.PUT("/inventories/:id", ansibleHandler.UpdateInventory)
|
||||
ansible.DELETE("/inventories/:id", ansibleHandler.DeleteInventory)
|
||||
|
||||
// 主机匹配(资产服务器查询)// Logan.Li
|
||||
ansible.GET("/match-hosts", ansibleHandler.MatchHosts)
|
||||
}
|
||||
|
||||
// 日志监控路由 // Logan.Li
|
||||
|
||||
Reference in new issue
Block a user