Prechádzať zdrojové kódy

fix: 修复主机组多项bug - 组列表/成员数/主机保存/前端显示

Hermes Agent 1 týždeň pred
rodič
commit
6a7f74aac5
5 zmenil súbory, kde vykonal 108 pridanie a 57 odobranie
  1. BIN
      ansible-deploy
  2. 53 21
      internal/services/ansible.go
  3. 2 3
      inventory/hosts
  4. 39 21
      inventory/hosts.json
  5. 14 12
      web/dist/index.html

BIN
ansible-deploy


+ 53 - 21
internal/services/ansible.go

@@ -47,10 +47,30 @@ func NewAnsibleService(cfg *Config) *AnsibleService {
 
 	// 加载现有数据
 	svc.loadHosts()
+	svc.loadGroups()
 
 	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
 func (s *AnsibleService) generateID() string {
 	hash := md5.New()
@@ -121,22 +141,22 @@ func (s *AnsibleService) loadHosts() {
 
 	var hosts []models.Host
 	if err := json.Unmarshal(data, &hosts); err == nil {
-		for _, h := range hosts {
-			hcopy := h
-			if hcopy.ID == "" {
-				hcopy.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
+	for _, h := range hosts {
+		host := h // 避免循环变量指针问题
+		if host.ID == "" {
+			host.ID = s.generateID()
+		}
+		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()
 	}
@@ -147,7 +167,13 @@ func (s *AnsibleService) saveHosts() error {
 	hostsFile := filepath.Join(s.config.InventoryDir, "hosts.json")
 	var hosts []models.Host
 	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, "", "  ")
@@ -246,15 +272,21 @@ func (s *AnsibleService) ListGroups() []models.HostGroup {
 	var groups []models.HostGroup
 	for _, g := range s.groups {
 		gcopy := *g
-		// 展开组内主机的详细信息
+		// 动态展开组内主机(通过 host.Groups 字段关联,而非 group.Hosts)
 		var hostList []models.Host
-		for _, hName := range g.Hosts {
-			for _, h := range s.hosts {
-				if h.Name == hName {
-					hostList = append(hostList, *h)
+		for _, h := range s.hosts {
+			for _, hGroup := range h.Groups {
+				if hGroup == gcopy.Name {
+					hcopy := *h
+					hostList = append(hostList, hcopy)
 					break
 				}
 			}
+			// 也检查主机的默认组(all/ungrouped)
+			if len(h.Groups) == 0 && gcopy.Name == "ungrouped" {
+				hcopy := *h
+				hostList = append(hostList, hcopy)
+			}
 		}
 		gcopy.HostList = hostList
 		groups = append(groups, gcopy)

+ 2 - 3
inventory/hosts

@@ -1,11 +1,10 @@
 # Ansible Inventory File
 # Generated by ansible-deploy
 
-[ungrouped]
-  scmp47 ansible_host=172.16.11.44 ansible_user=root
-
 [all]
   nas ansible_host=10.168.1.209 ansible_user=root
 
 [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

+ 39 - 21
inventory/hosts.json

@@ -1,8 +1,23 @@
 [
   {
-    "id": "c3e976cb",
-    "name": "scmp46",
-    "ip": "172.16.11.42",
+    "id": "706f8ce7",
+    "name": "nas",
+    "ip": "10.168.1.209",
+    "port": 22,
+    "username": "root",
+    "password": "WXJwxj91612!!",
+    "groups": [
+      "all"
+    ],
+    "status": "online",
+    "last_check": "2026-05-13T17:34:10.808052527+08:00",
+    "created_at": "2026-05-13T16:03:45.265250935+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",
@@ -10,35 +25,38 @@
       "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"
+    "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": "5ef41aa3",
-    "name": "scmp47",
-    "ip": "172.16.11.44",
+    "id": "57884720",
+    "name": "scmp46",
+    "ip": "172.16.11.42",
     "port": 22,
     "username": "root",
-    "groups": [],
-    "status": "pending",
-    "last_check": "0001-01-01T00:00:00Z",
+    "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-13T17:40:47.833904109+08:00"
+    "updated_at": "2026-05-13T18:28:16.71131472+08:00"
   },
   {
-    "id": "706f8ce7",
-    "name": "nas",
-    "ip": "10.168.1.209",
+    "id": "5150c740",
+    "name": "scmp47",
+    "ip": "172.16.11.44",
     "port": 22,
     "username": "root",
-    "password": "WXJwxj91612!!",
+    "password": "STC#scmp%0818",
     "groups": [
-      "all"
+      "zebu_user01"
     ],
     "status": "online",
-    "last_check": "2026-05-13T17:34:10.808052527+08:00",
-    "created_at": "2026-05-13T16:03:45.265250935+08:00",
-    "updated_at": "2026-05-13T16:03:45.265251013+08:00"
+    "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"
   }
 ]

+ 14 - 12
web/dist/index.html

@@ -389,6 +389,7 @@
                             <th>端口</th>
                             <th>用户名</th>
                             <th>状态</th>
+                            <th>主机组</th>
                             <th>操作</th>
                         </tr>
                     </thead>
@@ -789,7 +790,7 @@
         function renderHostsTable() {
             const tbody = document.getElementById('hostTableBody');
             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;
             }
             tbody.innerHTML = hosts.map(h => `
@@ -799,6 +800,7 @@
                     <td>${h.port || 22}</td>
                     <td>${h.username}</td>
                     <td><span class="status ${h.status || 'pending'}">${statusText(h.status)}</span></td>
+                    <td>${(h.groups || []).join(', ') || '-'}</td>
                     <td>
                         <div class="action-group">
                             <button class="btn btn-info" id="testBtn_${h.id}" onclick="testConnection('${h.id}')">🔌 测试</button>
@@ -819,7 +821,7 @@
             const groupsList = document.getElementById('groupsList');
             groupsList.innerHTML = groups.map(g => `
                 <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>
                 </div>
             `).join('');
@@ -1136,7 +1138,7 @@
                     <div class="group-checkbox-item" data-group="${g.name}" data-type="playbook">
                         <label class="checkbox-item">
                             <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>
                         <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 => `
@@ -1157,7 +1159,7 @@
                     <div class="group-checkbox-item" data-group="${g.name}" data-type="cmd">
                         <label class="checkbox-item">
                             <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>
                         <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 => `
@@ -1178,10 +1180,10 @@
             const toggle = div.previousElementSibling.querySelector('.group-toggle');
             if (div.style.display === 'none') {
                 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 {
                 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) {
                 groupList.forEach(gName => {
                     const g = groups.find(gr => gr.name === gName);
-                    if (g && g.hosts) {
-                        g.hosts.forEach(hName => {
-                            if (!hostList.includes(hName)) hostList.push(hName);
+                    if (g && g.host_list) {
+                        g.host_list.forEach(h => {
+                            if (!hostList.includes(h.name)) hostList.push(h.name);
                         });
                     }
                 });
@@ -1462,9 +1464,9 @@
             if (groupList.length > 0) {
                 groupList.forEach(gName => {
                     const g = groups.find(gr => gr.name === gName);
-                    if (g && g.hosts) {
-                        g.hosts.forEach(hName => {
-                            if (!hostList.includes(hName)) hostList.push(hName);
+                    if (g && g.host_list) {
+                        g.host_list.forEach(h => {
+                            if (!hostList.includes(h.name)) hostList.push(h.name);
                         });
                     }
                 });