fix: 修复主机组多项bug - 组列表/成员数/主机保存/前端显示
This commit is contained in:
Binary file not shown.
@@ -47,10 +47,30 @@ func NewAnsibleService(cfg *Config) *AnsibleService {
|
|||||||
|
|
||||||
// 加载现有数据
|
// 加载现有数据
|
||||||
svc.loadHosts()
|
svc.loadHosts()
|
||||||
|
svc.loadGroups()
|
||||||
|
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadGroups 加载主机组列表
|
||||||
|
func (s *AnsibleService) loadGroups() {
|
||||||
|
groupsFile := filepath.Join(s.config.InventoryDir, "groups.json")
|
||||||
|
data, err := os.ReadFile(groupsFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups map[string]models.HostGroup
|
||||||
|
if err := json.Unmarshal(data, &groups); err == nil {
|
||||||
|
for name, g := range groups {
|
||||||
|
if name != "all" && name != "ungrouped" {
|
||||||
|
gcopy := g
|
||||||
|
s.groups[name] = &gcopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// generateID 生成唯一ID
|
// generateID 生成唯一ID
|
||||||
func (s *AnsibleService) generateID() string {
|
func (s *AnsibleService) generateID() string {
|
||||||
hash := md5.New()
|
hash := md5.New()
|
||||||
@@ -121,22 +141,22 @@ func (s *AnsibleService) loadHosts() {
|
|||||||
|
|
||||||
var hosts []models.Host
|
var hosts []models.Host
|
||||||
if err := json.Unmarshal(data, &hosts); err == nil {
|
if err := json.Unmarshal(data, &hosts); err == nil {
|
||||||
for _, h := range hosts {
|
for _, h := range hosts {
|
||||||
hcopy := h
|
host := h // 避免循环变量指针问题
|
||||||
if hcopy.ID == "" {
|
if host.ID == "" {
|
||||||
hcopy.ID = s.generateID()
|
host.ID = s.generateID()
|
||||||
}
|
|
||||||
if hcopy.Port == 0 {
|
|
||||||
hcopy.Port = 22
|
|
||||||
}
|
|
||||||
if hcopy.Username == "" {
|
|
||||||
hcopy.Username = "root"
|
|
||||||
}
|
|
||||||
if hcopy.Status == "" {
|
|
||||||
hcopy.Status = "pending"
|
|
||||||
}
|
|
||||||
s.hosts[hcopy.ID] = &hcopy
|
|
||||||
}
|
}
|
||||||
|
if host.Port == 0 {
|
||||||
|
host.Port = 22
|
||||||
|
}
|
||||||
|
if host.Username == "" {
|
||||||
|
host.Username = "root"
|
||||||
|
}
|
||||||
|
if host.Status == "" {
|
||||||
|
host.Status = "pending"
|
||||||
|
}
|
||||||
|
s.hosts[host.ID] = &host
|
||||||
|
}
|
||||||
// 保存以持久化补全的字段
|
// 保存以持久化补全的字段
|
||||||
s.saveHosts()
|
s.saveHosts()
|
||||||
}
|
}
|
||||||
@@ -147,7 +167,13 @@ func (s *AnsibleService) saveHosts() error {
|
|||||||
hostsFile := filepath.Join(s.config.InventoryDir, "hosts.json")
|
hostsFile := filepath.Join(s.config.InventoryDir, "hosts.json")
|
||||||
var hosts []models.Host
|
var hosts []models.Host
|
||||||
for _, h := range s.hosts {
|
for _, h := range s.hosts {
|
||||||
hosts = append(hosts, *h)
|
hcopy := *h
|
||||||
|
// 确保每个主机都有ID,并更新map中的指针
|
||||||
|
if hcopy.ID == "" {
|
||||||
|
hcopy.ID = s.generateID()
|
||||||
|
h.ID = hcopy.ID // 更新map中的指针
|
||||||
|
}
|
||||||
|
hosts = append(hosts, hcopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, _ := json.MarshalIndent(hosts, "", " ")
|
data, _ := json.MarshalIndent(hosts, "", " ")
|
||||||
@@ -246,15 +272,21 @@ func (s *AnsibleService) ListGroups() []models.HostGroup {
|
|||||||
var groups []models.HostGroup
|
var groups []models.HostGroup
|
||||||
for _, g := range s.groups {
|
for _, g := range s.groups {
|
||||||
gcopy := *g
|
gcopy := *g
|
||||||
// 展开组内主机的详细信息
|
// 动态展开组内主机(通过 host.Groups 字段关联,而非 group.Hosts)
|
||||||
var hostList []models.Host
|
var hostList []models.Host
|
||||||
for _, hName := range g.Hosts {
|
for _, h := range s.hosts {
|
||||||
for _, h := range s.hosts {
|
for _, hGroup := range h.Groups {
|
||||||
if h.Name == hName {
|
if hGroup == gcopy.Name {
|
||||||
hostList = append(hostList, *h)
|
hcopy := *h
|
||||||
|
hostList = append(hostList, hcopy)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 也检查主机的默认组(all/ungrouped)
|
||||||
|
if len(h.Groups) == 0 && gcopy.Name == "ungrouped" {
|
||||||
|
hcopy := *h
|
||||||
|
hostList = append(hostList, hcopy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gcopy.HostList = hostList
|
gcopy.HostList = hostList
|
||||||
groups = append(groups, gcopy)
|
groups = append(groups, gcopy)
|
||||||
|
|||||||
+2
-3
@@ -1,11 +1,10 @@
|
|||||||
# Ansible Inventory File
|
# Ansible Inventory File
|
||||||
# Generated by ansible-deploy
|
# Generated by ansible-deploy
|
||||||
|
|
||||||
[ungrouped]
|
|
||||||
scmp47 ansible_host=172.16.11.44 ansible_user=root
|
|
||||||
|
|
||||||
[all]
|
[all]
|
||||||
nas ansible_host=10.168.1.209 ansible_user=root
|
nas ansible_host=10.168.1.209 ansible_user=root
|
||||||
|
|
||||||
[zebu_user01]
|
[zebu_user01]
|
||||||
|
scmp47 ansible_host=172.16.11.44 ansible_user=root
|
||||||
|
scmp48 ansible_host=172.16.11.46 ansible_user=root
|
||||||
scmp46 ansible_host=172.16.11.42 ansible_user=root
|
scmp46 ansible_host=172.16.11.42 ansible_user=root
|
||||||
|
|||||||
+45
-27
@@ -1,31 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"id": "c3e976cb",
|
|
||||||
"name": "scmp46",
|
|
||||||
"ip": "172.16.11.42",
|
|
||||||
"port": 22,
|
|
||||||
"username": "root",
|
|
||||||
"password": "STC#scmp%0818",
|
|
||||||
"groups": [
|
|
||||||
"zebu_user01"
|
|
||||||
],
|
|
||||||
"status": "online",
|
|
||||||
"last_check": "2026-05-13T17:43:47.863884597+08:00",
|
|
||||||
"created_at": "2026-05-13T16:19:53.979745105+08:00",
|
|
||||||
"updated_at": "2026-05-13T16:19:53.979745185+08:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5ef41aa3",
|
|
||||||
"name": "scmp47",
|
|
||||||
"ip": "172.16.11.44",
|
|
||||||
"port": 22,
|
|
||||||
"username": "root",
|
|
||||||
"groups": [],
|
|
||||||
"status": "pending",
|
|
||||||
"last_check": "0001-01-01T00:00:00Z",
|
|
||||||
"created_at": "0001-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2026-05-13T17:40:47.833904109+08:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "706f8ce7",
|
"id": "706f8ce7",
|
||||||
"name": "nas",
|
"name": "nas",
|
||||||
@@ -40,5 +13,50 @@
|
|||||||
"last_check": "2026-05-13T17:34:10.808052527+08:00",
|
"last_check": "2026-05-13T17:34:10.808052527+08:00",
|
||||||
"created_at": "2026-05-13T16:03:45.265250935+08:00",
|
"created_at": "2026-05-13T16:03:45.265250935+08:00",
|
||||||
"updated_at": "2026-05-13T16:03:45.265251013+08:00"
|
"updated_at": "2026-05-13T16:03:45.265251013+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4d7a1f03",
|
||||||
|
"name": "scmp48",
|
||||||
|
"ip": "172.16.11.46",
|
||||||
|
"port": 22,
|
||||||
|
"username": "root",
|
||||||
|
"password": "STC#scmp%0818",
|
||||||
|
"groups": [
|
||||||
|
"zebu_user01"
|
||||||
|
],
|
||||||
|
"status": "online",
|
||||||
|
"last_check": "2026-05-13T18:27:19.272543838+08:00",
|
||||||
|
"created_at": "0001-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-05-13T18:23:27.433461112+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "57884720",
|
||||||
|
"name": "scmp46",
|
||||||
|
"ip": "172.16.11.42",
|
||||||
|
"port": 22,
|
||||||
|
"username": "root",
|
||||||
|
"password": "STC#scmp%0818",
|
||||||
|
"groups": [
|
||||||
|
"zebu_user01"
|
||||||
|
],
|
||||||
|
"status": "online",
|
||||||
|
"last_check": "2026-05-13T18:30:38.103119534+08:00",
|
||||||
|
"created_at": "0001-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-05-13T18:28:16.71131472+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5150c740",
|
||||||
|
"name": "scmp47",
|
||||||
|
"ip": "172.16.11.44",
|
||||||
|
"port": 22,
|
||||||
|
"username": "root",
|
||||||
|
"password": "STC#scmp%0818",
|
||||||
|
"groups": [
|
||||||
|
"zebu_user01"
|
||||||
|
],
|
||||||
|
"status": "online",
|
||||||
|
"last_check": "2026-05-13T18:30:33.343216547+08:00",
|
||||||
|
"created_at": "0001-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-05-13T18:28:08.656450224+08:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Vendored
+14
-12
@@ -389,6 +389,7 @@
|
|||||||
<th>端口</th>
|
<th>端口</th>
|
||||||
<th>用户名</th>
|
<th>用户名</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
|
<th>主机组</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -789,7 +790,7 @@
|
|||||||
function renderHostsTable() {
|
function renderHostsTable() {
|
||||||
const tbody = document.getElementById('hostTableBody');
|
const tbody = document.getElementById('hostTableBody');
|
||||||
if (hosts.length === 0) {
|
if (hosts.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#8899a6;padding:30px;">暂无主机,点击右上角添加</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#8899a6;padding:30px;">暂无主机,点击右上角添加</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = hosts.map(h => `
|
tbody.innerHTML = hosts.map(h => `
|
||||||
@@ -799,6 +800,7 @@
|
|||||||
<td>${h.port || 22}</td>
|
<td>${h.port || 22}</td>
|
||||||
<td>${h.username}</td>
|
<td>${h.username}</td>
|
||||||
<td><span class="status ${h.status || 'pending'}">${statusText(h.status)}</span></td>
|
<td><span class="status ${h.status || 'pending'}">${statusText(h.status)}</span></td>
|
||||||
|
<td>${(h.groups || []).join(', ') || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<button class="btn btn-info" id="testBtn_${h.id}" onclick="testConnection('${h.id}')">🔌 测试</button>
|
<button class="btn btn-info" id="testBtn_${h.id}" onclick="testConnection('${h.id}')">🔌 测试</button>
|
||||||
@@ -819,7 +821,7 @@
|
|||||||
const groupsList = document.getElementById('groupsList');
|
const groupsList = document.getElementById('groupsList');
|
||||||
groupsList.innerHTML = groups.map(g => `
|
groupsList.innerHTML = groups.map(g => `
|
||||||
<div style="display: inline-block; background: #2d3748; padding: 10px 15px; border-radius: 8px; margin: 5px;">
|
<div style="display: inline-block; background: #2d3748; padding: 10px 15px; border-radius: 8px; margin: 5px;">
|
||||||
<strong>${g.name}</strong> (${g.hosts?.length || 0}台)
|
<strong>${g.name}</strong> (${g.host_list?.length || 0}台)
|
||||||
<button class="btn btn-danger" onclick="deleteGroup('${g.name}')" style="margin-left: 10px;">删除</button>
|
<button class="btn btn-danger" onclick="deleteGroup('${g.name}')" style="margin-left: 10px;">删除</button>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -1136,7 +1138,7 @@
|
|||||||
<div class="group-checkbox-item" data-group="${g.name}" data-type="playbook">
|
<div class="group-checkbox-item" data-group="${g.name}" data-type="playbook">
|
||||||
<label class="checkbox-item">
|
<label class="checkbox-item">
|
||||||
<input type="checkbox" name="playbookGroups" value="${g.name}" onchange="toggleGroupHosts('${g.name}', 'playbook', this.checked)">
|
<input type="checkbox" name="playbookGroups" value="${g.name}" onchange="toggleGroupHosts('${g.name}', 'playbook', this.checked)">
|
||||||
<span class="group-toggle" onclick="toggleGroupExpand('${g.name}', 'playbook')">▶ ${g.name} (${g.hosts?.length || 0}台)</span>
|
<span class="group-toggle" onclick="toggleGroupExpand('${g.name}', 'playbook')">▶ ${g.name} (${g.host_list?.length || 0}台)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="group-hosts" id="playbook_group_${g.name}" style="display:none;margin-left:20px;border-left:2px solid #38444d;padding-left:10px;">
|
<div class="group-hosts" id="playbook_group_${g.name}" style="display:none;margin-left:20px;border-left:2px solid #38444d;padding-left:10px;">
|
||||||
${(g.host_list || []).map(h => `
|
${(g.host_list || []).map(h => `
|
||||||
@@ -1157,7 +1159,7 @@
|
|||||||
<div class="group-checkbox-item" data-group="${g.name}" data-type="cmd">
|
<div class="group-checkbox-item" data-group="${g.name}" data-type="cmd">
|
||||||
<label class="checkbox-item">
|
<label class="checkbox-item">
|
||||||
<input type="checkbox" name="cmdGroups" value="${g.name}" onchange="toggleGroupHosts('${g.name}', 'cmd', this.checked)">
|
<input type="checkbox" name="cmdGroups" value="${g.name}" onchange="toggleGroupHosts('${g.name}', 'cmd', this.checked)">
|
||||||
<span class="group-toggle" onclick="toggleGroupExpand('${g.name}', 'cmd')">▶ ${g.name} (${g.hosts?.length || 0}台)</span>
|
<span class="group-toggle" onclick="toggleGroupExpand('${g.name}', 'cmd')">▶ ${g.name} (${g.host_list?.length || 0}台)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="group-hosts" id="cmd_group_${g.name}" style="display:none;margin-left:20px;border-left:2px solid #38444d;padding-left:10px;">
|
<div class="group-hosts" id="cmd_group_${g.name}" style="display:none;margin-left:20px;border-left:2px solid #38444d;padding-left:10px;">
|
||||||
${(g.host_list || []).map(h => `
|
${(g.host_list || []).map(h => `
|
||||||
@@ -1178,10 +1180,10 @@
|
|||||||
const toggle = div.previousElementSibling.querySelector('.group-toggle');
|
const toggle = div.previousElementSibling.querySelector('.group-toggle');
|
||||||
if (div.style.display === 'none') {
|
if (div.style.display === 'none') {
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
toggle.textContent = '▼ ' + groupName + ' (' + (groups.find(g => g.name === groupName)?.hosts?.length || 0) + '台)';
|
toggle.textContent = '▼ ' + groupName + ' (' + (groups.find(g => g.name === groupName)?.host_list?.length || 0) + '台)';
|
||||||
} else {
|
} else {
|
||||||
div.style.display = 'none';
|
div.style.display = 'none';
|
||||||
toggle.textContent = '▶ ' + groupName + ' (' + (groups.find(g => g.name === groupName)?.hosts?.length || 0) + '台)';
|
toggle.textContent = '▶ ' + groupName + ' (' + (groups.find(g => g.name === groupName)?.host_list?.length || 0) + '台)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,9 +1401,9 @@
|
|||||||
if (groupList.length > 0) {
|
if (groupList.length > 0) {
|
||||||
groupList.forEach(gName => {
|
groupList.forEach(gName => {
|
||||||
const g = groups.find(gr => gr.name === gName);
|
const g = groups.find(gr => gr.name === gName);
|
||||||
if (g && g.hosts) {
|
if (g && g.host_list) {
|
||||||
g.hosts.forEach(hName => {
|
g.host_list.forEach(h => {
|
||||||
if (!hostList.includes(hName)) hostList.push(hName);
|
if (!hostList.includes(h.name)) hostList.push(h.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1462,9 +1464,9 @@
|
|||||||
if (groupList.length > 0) {
|
if (groupList.length > 0) {
|
||||||
groupList.forEach(gName => {
|
groupList.forEach(gName => {
|
||||||
const g = groups.find(gr => gr.name === gName);
|
const g = groups.find(gr => gr.name === gName);
|
||||||
if (g && g.hosts) {
|
if (g && g.host_list) {
|
||||||
g.hosts.forEach(hName => {
|
g.host_list.forEach(h => {
|
||||||
if (!hostList.includes(hName)) hostList.push(hName);
|
if (!hostList.includes(h.name)) hostList.push(h.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user