From bb966dcea25c092fc6afefe89d9dc2eb8dcb7f42 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Thu, 11 Sep 2025 18:49:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20proxcluster:=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BE=20CPU/RA?= =?UTF-8?q?M=20=D0=B8=20=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81=D0=B0=D1=85?= =?UTF-8?q?=20=D0=BD=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена проверка онлайн статуса нод через pvesh get /nodes - Реализованы альтернативные методы получения данных о ресурсах через /nodes/{node}/rrddata и /nodes/{node}/capabilities - Улучшено получение информации о VM и контейнерах с детальными ресурсами - Добавлены fallback методы для получения данных о железе через /nodes/{node}/hardware - Исправлена проблема с нулевыми значениями CPU/RAM Автор: Сергей Антропов, сайт: https://devops.org.ru --- src/collectors/proxcluster/main.go | 5 +- .../proxcluster/proxcluster_linux.go | 2013 ++++++----------- .../proxcluster/proxcluster_unsupported.go | 6 +- 3 files changed, 726 insertions(+), 1298 deletions(-) diff --git a/src/collectors/proxcluster/main.go b/src/collectors/proxcluster/main.go index e6c89ab..aeea340 100644 --- a/src/collectors/proxcluster/main.go +++ b/src/collectors/proxcluster/main.go @@ -1,8 +1,7 @@ package main // Автор: Сергей Антропов, сайт: https://devops.org.ru -// Коллектор proxcluster на Go. Собирает информацию о Proxmox кластере. -// Реализация платформозависима (linux), на остальных платформах возвращает пустой JSON. +// Коллектор proxcluster - собирает информацию о Proxmox кластере import ( "context" @@ -13,8 +12,6 @@ import ( "time" ) -// collectProxCluster реализуется в файлах с билд-тегами под конкретные ОС. - func main() { // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second) diff --git a/src/collectors/proxcluster/proxcluster_linux.go b/src/collectors/proxcluster/proxcluster_linux.go index 9fa7cfb..5f901ff 100644 --- a/src/collectors/proxcluster/proxcluster_linux.go +++ b/src/collectors/proxcluster/proxcluster_linux.go @@ -3,137 +3,123 @@ package main // Автор: Сергей Антропов, сайт: https://devops.org.ru -// Сбор информации о Proxmox кластере для Linux. +// Коллектор proxcluster для Linux - собирает информацию о Proxmox кластере import ( - "bufio" "context" "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "os" "os/exec" + "regexp" "strconv" "strings" - "time" ) -// collectProxCluster собирает подробную информацию о Proxmox кластере: -// Структура вывода: -// 1. summary - вся информация по кластеру -// 2. nodes - вся информация по нодам -// Примечание: services, storages, logs, gpu, disks, network, vms вынесены в отдельные коллекторы -func collectProxCluster(ctx context.Context) (map[string]any, error) { - result := map[string]any{ +// collectProxCluster собирает информацию о Proxmox кластере +// Возвращает структуру в формате: +// { +// "collector_name": "proxcluster", +// "summary": { +// "cluster_id": "...", +// "cluster_uuid": "...", +// "name": "...", +// "version": "...", +// "cluster_resources": { ... }, +// "quorum": { ... }, +// "corosync": { ... } +// }, +// "nodes": [ ... ] +// } +func collectProxCluster(ctx context.Context) (map[string]interface{}, error) { + result := map[string]interface{}{ "collector_name": "proxcluster", } - // Основная информация о кластере из corosync.conf - clusterInfo, err := collectClusterInfo(ctx) - if err == nil { - for k, v := range clusterInfo { - result[k] = v + // Получаем информацию о кластере через pvesh + clusterInfo, err := getClusterInfo(ctx) + if err != nil { + clusterInfo = map[string]interface{}{ + "cluster_id": "unknown", + "cluster_uuid": "unknown", + "name": "unknown", + "version": "unknown", } } - // Получаем данные для агрегированных ресурсов - clusterUUID := "" - clusterName := "" - if uuid, ok := result["cluster_uuid"].(string); ok { - clusterUUID = uuid + // Получаем информацию о нодах + nodesInfo, err := getNodesInfo(ctx) + if err != nil { + nodesInfo = []map[string]interface{}{} } - if name, ok := result["name"].(string); ok { - clusterName = name + + // Создаем summary + summary := map[string]interface{}{ + "cluster_id": clusterInfo["cluster_id"], + "cluster_uuid": clusterInfo["cluster_uuid"], + "name": clusterInfo["name"], + "version": clusterInfo["version"], } - - // Собираем информацию о нодах для агрегации - nodesInfo, err := collectDetailedNodesInfo(ctx, clusterName, clusterUUID) - - // Создаем блок summary с информацией о кластере - summary := map[string]any{} - - // Копируем основную информацию о кластере - for k, v := range result { - if k != "collector_name" && k != "nodes" { - summary[k] = v - } - } - - // Агрегированная информация о ресурсах кластера - if nodesInfo != nil { - clusterResources, err := calculateClusterResources(nodesInfo, nil) - if err == nil { + + // Агрегируем ресурсы кластера + clusterResources := calculateClusterResources(nodesInfo) summary["cluster_resources"] = clusterResources + + // Получаем информацию о кворуме + quorumInfo, err := getQuorumInfo(ctx) + if err != nil { + quorumInfo = map[string]interface{}{ + "quorate": false, + "members": 0, + "total_votes": 0, + "expected_votes": 0, } } - - // Информация о кворуме - quorumInfo, err := collectQuorumInfo(ctx) - if err == nil { summary["quorum"] = quorumInfo - } - // Информация о corosync - corosyncInfo, err := collectCorosyncInfo(ctx) - if err == nil { - summary["corosync"] = corosyncInfo + // Получаем информацию о corosync + corosyncInfo, err := getCorosyncInfo(ctx) + if err != nil { + corosyncInfo = map[string]interface{}{} } + summary["corosync"] = corosyncInfo - // Формируем финальный результат result["summary"] = summary - - // Подробная информация о нодах - if nodesInfo != nil { result["nodes"] = nodesInfo - } - - if len(result) == 0 { - return nil, errors.New("no cluster data found") - } return result, nil } -// collectClusterInfo читает основную информацию о кластере из corosync.conf и pvesh -func collectClusterInfo(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // Читаем corosync.conf - corosyncPath := "/etc/corosync/corosync.conf" - if _, err := os.Stat(corosyncPath); os.IsNotExist(err) { - // Пробуем альтернативные пути - altPaths := []string{ - "/etc/pve/corosync.conf", - "/var/lib/pve-cluster/corosync.conf", - } - for _, path := range altPaths { - if _, err := os.Stat(path); err == nil { - corosyncPath = path - break - } - } - } - - clusterName, clusterUUID, err := parseCorosyncConf(corosyncPath) - if err != nil { - return result, fmt.Errorf("failed to parse corosync.conf: %w", err) +// getClusterInfo получает основную информацию о кластере +func getClusterInfo(ctx context.Context) (map[string]interface{}, error) { + result := map[string]interface{}{ + "cluster_id": "unknown", + "cluster_uuid": "unknown", + "name": "unknown", + "version": "unknown", } + // Читаем информацию из corosync.conf + clusterName, clusterUUID, err := parseCorosyncConf("/etc/pve/corosync.conf") + if err == nil { result["name"] = clusterName result["cluster_uuid"] = clusterUUID - result["cluster_id"] = generateClusterID(clusterName, clusterUUID) - - // Версия кластера - if version, err := getClusterVersion(ctx); err == nil { - result["version"] = version + result["cluster_id"] = generateClusterID(clusterUUID) } - // Дополнительная информация о кластере через pvesh - if pveshInfo, err := getClusterInfoFromPvesh(ctx); err == nil { - for k, v := range pveshInfo { - result[k] = v + // Получаем версию через pvesh + if _, err := exec.LookPath("pvesh"); err == nil { + cmd := exec.CommandContext(ctx, "pvesh", "get", "/version", "--output-format", "json") + out, err := cmd.Output() + if err == nil { + var versionData map[string]interface{} + if err := json.Unmarshal(out, &versionData); err == nil { + if version, ok := versionData["version"].(string); ok { + result["version"] = version + } + } } } @@ -142,24 +128,20 @@ func collectClusterInfo(ctx context.Context) (map[string]any, error) { // parseCorosyncConf парсит corosync.conf и извлекает cluster_name и cluster_uuid func parseCorosyncConf(path string) (string, string, error) { - file, err := os.Open(path) + data, err := os.ReadFile(path) if err != nil { return "", "", err } - defer file.Close() var clusterName, clusterUUID string - scanner := bufio.NewScanner(file) + lines := strings.Split(string(data), "\n") - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - - // Пропускаем комментарии и пустые строки + for _, line := range lines { + line = strings.TrimSpace(line) if strings.HasPrefix(line, "#") || line == "" { continue } - // Ищем cluster_name if strings.HasPrefix(line, "cluster_name:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { @@ -167,7 +149,6 @@ func parseCorosyncConf(path string) (string, string, error) { } } - // Ищем cluster_uuid if strings.HasPrefix(line, "cluster_uuid:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { @@ -176,215 +157,401 @@ func parseCorosyncConf(path string) (string, string, error) { } } - if err := scanner.Err(); err != nil { - return "", "", err - } - if clusterName == "" { - return "", "", errors.New("cluster_name not found in corosync.conf") + // Если cluster_name не найден, пробуем получить из /etc/pve/.members + if members, err := getClusterMembers(); err == nil && len(members) > 0 { + // Используем имя первой ноды как основу для имени кластера + clusterName = "cluster-" + members[0] + } else { + clusterName = "unknown-cluster" + } } - - // cluster_uuid может быть пустым, это нормально return clusterName, clusterUUID, nil } -// generateClusterID создает уникальный ID кластера на основе cluster_name + cluster_uuid -func generateClusterID(clusterName, clusterUUID string) string { - base := clusterName + ":" + clusterUUID - hash := sha256.Sum256([]byte(base)) - return hex.EncodeToString(hash[:])[:16] +// getNodesInfo получает информацию о нодах кластера +func getNodesInfo(ctx context.Context) ([]map[string]interface{}, error) { + var nodes []map[string]interface{} + + // Получаем информацию о кластере + clusterInfo, err := getClusterInfo(ctx) + if err != nil { + return nodes, err + } + + clusterID := clusterInfo["cluster_id"].(string) + clusterUUID := clusterInfo["cluster_uuid"].(string) + + // Читаем /etc/pve/.members для получения списка нод + members, err := getClusterMembers() + if err != nil { + return nodes, err + } + + // Получаем nodeid для каждой ноды из corosync.conf + nodeIDs, err := getNodeIDsFromCorosync("/etc/pve/corosync.conf", members) + if err != nil { + return nodes, err + } + + // Обрабатываем каждую ноду + for _, member := range members { + nodeName := member + nodeID := 0 + if id, ok := nodeIDs[member]; ok { + nodeID = id + } + + node := map[string]interface{}{ + "node_id": nodeID, + "name": nodeName, + "online": false, // будет обновлено через pvesh + "cluster_id": clusterID, + "cluster_uuid": clusterUUID, + "node_uid": generateNodeUID(clusterUUID, strconv.Itoa(nodeID)), + } + + // Получаем дополнительную информацию о ноде через pvesh + nodeInfo, err := getNodeDetails(ctx, nodeName) + + // Всегда заполняем базовые поля + node["corosync_ip"] = "" + node["real_ips"] = []string{} + node["machine_id"] = "" + node["product_uuid"] = "" + node["os"] = map[string]interface{}{ + "kernel": "unknown", + "pve_version": "unknown", + "uptime_sec": 0, + } + node["resources"] = map[string]interface{}{ + "cpu_usage_percent": 0, + "memory_used_mb": 0, + "swap_used_mb": 0, + "loadavg": []float64{0, 0, 0}, + } + node["hardware"] = map[string]interface{}{ + "cpu_model": "unknown", + "cpu_cores": 0, + "sockets": 0, + "threads": 0, + "memory_total_mb": 0, + } + node["vm_summary"] = map[string]interface{}{ + "total_vms": 0, + "running_vms": 0, + "stopped_vms": 0, + "total_containers": 0, + "running_containers": 0, + "stopped_containers": 0, + "total_cpu_cores": 0, + "total_memory_mb": 0, + "used_cpu_cores": 0, + "used_memory_mb": 0, + } + + // Если удалось получить информацию, перезаписываем + if err == nil { + for k, v := range nodeInfo { + node[k] = v + } + } + + nodes = append(nodes, node) + } + + return nodes, nil } +// getNodeDetails получает детальную информацию о конкретной ноде +func getNodeDetails(ctx context.Context, nodeName string) (map[string]interface{}, error) { + result := map[string]interface{}{} -// getClusterVersion получает версию кластера Proxmox -func getClusterVersion(ctx context.Context) (string, error) { - // Пробуем pveversion - if _, err := exec.LookPath("pveversion"); err == nil { - cmd := exec.CommandContext(ctx, "pveversion", "-v") - out, err := cmd.Output() - if err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - if strings.Contains(line, "pve-manager") { - parts := strings.Fields(line) - if len(parts) >= 2 { - return parts[1], nil + // Сначала проверяем онлайн статус ноды + online, err := checkNodeOnline(ctx, nodeName) + if err != nil || !online { + // Если нода офлайн, возвращаем базовую информацию + result["online"] = false + return result, nil + } + result["online"] = true + + // Получаем статус ноды + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/status", "--output-format", "json") + out, err := cmd.Output() + if err != nil { + return result, err + } + + var statusData map[string]interface{} + if err := json.Unmarshal(out, &statusData); err != nil { + return result, err + } + + // Извлекаем информацию об ОС + osInfo := map[string]interface{}{ + "kernel": "unknown", + "pve_version": "unknown", + "uptime_sec": 0, + } + + if uptime, ok := statusData["uptime"].(float64); ok { + osInfo["uptime_sec"] = int64(uptime) + } + + if kversion, ok := statusData["kversion"].(string); ok { + osInfo["kernel"] = kversion + } + + if pveversion, ok := statusData["pveversion"].(string); ok { + osInfo["pve_version"] = pveversion + } + + result["os"] = osInfo + + // Извлекаем информацию о ресурсах - используем более надежные методы + resources := map[string]interface{}{ + "cpu_usage_percent": 0, + "memory_used_mb": 0, + "swap_used_mb": 0, + "loadavg": []float64{0, 0, 0}, + } + + // Получаем данные о CPU и памяти из status + if cpu, ok := statusData["cpu"].(float64); ok { + resources["cpu_usage_percent"] = cpu * 100 // конвертируем в проценты + } + + if mem, ok := statusData["memory"].(float64); ok { + resources["memory_used_mb"] = int(mem / 1024 / 1024) // конвертируем в МБ + } + + if swap, ok := statusData["swap"].(float64); ok { + resources["swap_used_mb"] = int(swap / 1024 / 1024) // конвертируем в МБ + } + + if loadavg, ok := statusData["loadavg"].([]interface{}); ok && len(loadavg) >= 3 { + load := make([]float64, 3) + for i := 0; i < 3 && i < len(loadavg); i++ { + if val, ok := loadavg[i].(float64); ok { + load[i] = val + } + } + resources["loadavg"] = load + } + + // Если данные о ресурсах пустые, пробуем альтернативные методы + if resources["cpu_usage_percent"].(float64) == 0 && resources["memory_used_mb"].(int) == 0 { + if altResources, err := getAlternativeResources(ctx, nodeName); err == nil { + for k, v := range altResources { + resources[k] = v + } + } + } + + result["resources"] = resources + + // Извлекаем информацию о железе + hardware := map[string]interface{}{ + "cpu_model": "unknown", + "cpu_cores": 0, + "sockets": 0, + "threads": 0, + "memory_total_mb": 0, + } + + if maxcpu, ok := statusData["maxcpu"].(float64); ok { + hardware["cpu_cores"] = int(maxcpu) + hardware["threads"] = int(maxcpu) // упрощенно + } + + if maxmem, ok := statusData["maxmem"].(float64); ok { + hardware["memory_total_mb"] = int(maxmem / 1024 / 1024) // конвертируем в МБ + } + + // Если данные о железе пустые, пробуем альтернативные методы + if hardware["cpu_cores"].(int) == 0 && hardware["memory_total_mb"].(int) == 0 { + if altHardware, err := getAlternativeHardware(ctx, nodeName); err == nil { + for k, v := range altHardware { + hardware[k] = v + } + } + } + + result["hardware"] = hardware + + // Получаем информацию о VM и контейнерах + if vmSummary, err := getVMSummary(ctx, nodeName); err == nil { + result["vm_summary"] = vmSummary + } + + // Получаем corosync IP (пробуем через pvesh) + if corosyncIP, err := getCorosyncIP(ctx, nodeName); err == nil { + result["corosync_ip"] = corosyncIP + } + + // Получаем реальные IP адреса + if realIPs, err := getRealIPs(ctx, nodeName); err == nil { + result["real_ips"] = realIPs + } + + // Получаем machine_id и product_uuid (только для локальной ноды) + if machineID, err := getMachineID(); err == nil { + result["machine_id"] = machineID + } + + if productUUID, err := getProductUUID(); err == nil { + result["product_uuid"] = productUUID + } + + return result, nil +} + +// getVMSummary получает сводную информацию о VM и контейнерах на ноде +func getVMSummary(ctx context.Context, nodeName string) (map[string]interface{}, error) { + result := map[string]interface{}{ + "total_vms": 0, + "running_vms": 0, + "stopped_vms": 0, + "total_containers": 0, + "running_containers": 0, + "stopped_containers": 0, + "total_cpu_cores": 0, + "total_memory_mb": 0, + "used_cpu_cores": 0, + "used_memory_mb": 0, + } + + // Получаем VM с детальной информацией о ресурсах + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/qemu", "--output-format", "json") + out, err := cmd.Output() + if err == nil { + var vms []map[string]interface{} + if err := json.Unmarshal(out, &vms); err == nil { + result["total_vms"] = len(vms) + for _, vm := range vms { + if status, ok := vm["status"].(string); ok { + if status == "running" { + result["running_vms"] = result["running_vms"].(int) + 1 + } else { + result["stopped_vms"] = result["stopped_vms"].(int) + 1 + } + } + + // Суммируем ресурсы VM + if cpus, ok := vm["cpus"].(float64); ok { + result["total_cpu_cores"] = result["total_cpu_cores"].(int) + int(cpus) + if status, ok := vm["status"].(string); ok && status == "running" { + result["used_cpu_cores"] = result["used_cpu_cores"].(int) + int(cpus) + } + } + if mem, ok := vm["maxmem"].(float64); ok { + result["total_memory_mb"] = result["total_memory_mb"].(int) + int(mem/1024/1024) + if status, ok := vm["status"].(string); ok && status == "running" { + result["used_memory_mb"] = result["used_memory_mb"].(int) + int(mem/1024/1024) } } } } } - // Fallback: читаем из файла - versionFile := "/usr/share/pve-manager/version" - if data, err := os.ReadFile(versionFile); err == nil { - return strings.TrimSpace(string(data)), nil - } - - return "", errors.New("cluster version not found") -} - -// getClusterInfoFromPvesh получает дополнительную информацию о кластере через pvesh -func getClusterInfoFromPvesh(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // Проверяем наличие pvesh - if _, err := exec.LookPath("pvesh"); err != nil { - return result, fmt.Errorf("pvesh not found: %w", err) - } - - // Список всех endpoints для сбора информации о кластере - clusterEndpoints := []string{ - "/cluster/config/nodes", - "/cluster/status", - "/cluster/config/totem", - "/cluster/config/corosync", - "/cluster/options", - "/cluster/resources", - "/cluster/ha/groups", - "/cluster/ha/resources", - "/cluster/ha/status", - "/cluster/backup", - "/cluster/firewall/groups", - "/cluster/firewall/options", - "/cluster/firewall/rules", - "/cluster/log", - "/cluster/tasks", - "/cluster/nextid", - "/cluster/config/join", - "/cluster/config/apiclient", - "/cluster/config/totem", - "/cluster/config/corosync", - } - - // Собираем данные со всех endpoints - for _, endpoint := range clusterEndpoints { - cmd := exec.CommandContext(ctx, "pvesh", "get", endpoint, "--output-format", "json") - out, err := cmd.Output() - if err != nil { - // Пропускаем endpoints, которые недоступны или требуют прав - continue - } - - // Определяем имя поля на основе endpoint - fieldName := strings.ReplaceAll(endpoint, "/cluster/", "cluster_") - fieldName = strings.ReplaceAll(fieldName, "/", "_") - - // Парсим JSON ответ - var jsonData any - if err := json.Unmarshal(out, &jsonData); err == nil { - result[fieldName] = jsonData - } - } - - // Получаем информацию о нодах через /nodes - nodesInfo, err := getNodesInfoFromPvesh(ctx) + // Получаем контейнеры с детальной информацией о ресурсах + cmd = exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/lxc", "--output-format", "json") + out, err = cmd.Output() if err == nil { - result["nodes_info"] = nodesInfo + var containers []map[string]interface{} + if err := json.Unmarshal(out, &containers); err == nil { + result["total_containers"] = len(containers) + for _, container := range containers { + if status, ok := container["status"].(string); ok { + if status == "running" { + result["running_containers"] = result["running_containers"].(int) + 1 + } else { + result["stopped_containers"] = result["stopped_containers"].(int) + 1 + } + } + + // Суммируем ресурсы контейнеров + if cpus, ok := container["cpus"].(float64); ok { + result["total_cpu_cores"] = result["total_cpu_cores"].(int) + int(cpus) + if status, ok := container["status"].(string); ok && status == "running" { + result["used_cpu_cores"] = result["used_cpu_cores"].(int) + int(cpus) + } + } + if mem, ok := container["maxmem"].(float64); ok { + result["total_memory_mb"] = result["total_memory_mb"].(int) + int(mem/1024/1024) + if status, ok := container["status"].(string); ok && status == "running" { + result["used_memory_mb"] = result["used_memory_mb"].(int) + int(mem/1024/1024) + } + } + } + } } return result, nil } -// getNodesInfoFromPvesh получает информацию о нодах через pvesh get /nodes -func getNodesInfoFromPvesh(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // Получаем список нод - cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes", "--output-format", "json") - out, err := cmd.Output() - if err != nil { - return result, fmt.Errorf("failed to get nodes list: %w", err) +// calculateClusterResources вычисляет агрегированные ресурсы кластера +func calculateClusterResources(nodes []map[string]interface{}) map[string]interface{} { + result := map[string]interface{}{ + "cpu": map[string]interface{}{ + "total_cores": 0, + "online_cores": 0, + }, + "memory": map[string]interface{}{ + "total_mb": 0, + "used_mb": 0, + }, + "nodes": map[string]interface{}{ + "total": len(nodes), + "online": 0, + }, } - // Парсим JSON ответ - var nodesData []map[string]any - if err := json.Unmarshal(out, &nodesData); err != nil { - return result, fmt.Errorf("failed to parse nodes JSON: %w", err) - } - - // Обрабатываем каждую ноду - var nodesInfo []map[string]any - for _, node := range nodesData { - nodeName := "" - if name, ok := node["node"].(string); ok { - nodeName = name + totalCores := 0 + onlineCores := 0 + totalMemory := 0 + usedMemory := 0 + onlineNodes := 0 + + for _, node := range nodes { + if online, ok := node["online"].(bool); ok && online { + onlineNodes++ } - nodeInfo := map[string]any{ - "node": node["node"], - "status": node["status"], - "cpu": node["cpu"], - "level": node["level"], - "id": node["id"], - "type": node["type"], - "maxcpu": node["maxcpu"], - "maxmem": node["maxmem"], - "mem": node["mem"], - "disk": node["disk"], - "maxdisk": node["maxdisk"], - "uptime": node["uptime"], - } - - // Получаем дополнительную информацию о ноде - if nodeName != "" { - // Статус ноды - statusCmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", nodeName), "--output-format", "json") - if statusOut, err := statusCmd.Output(); err == nil { - var statusData map[string]any - if err := json.Unmarshal(statusOut, &statusData); err == nil { - nodeInfo["status_details"] = statusData + // Агрегируем CPU и память (упрощенно) + if hardware, ok := node["hardware"].(map[string]interface{}); ok { + if cores, ok := hardware["cpu_cores"].(int); ok { + totalCores += cores + if online, ok := node["online"].(bool); ok && online { + onlineCores += cores } } - - // Ресурсы ноды - resourceCmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/resources", nodeName), "--output-format", "json") - if resourceOut, err := resourceCmd.Output(); err == nil { - var resourceData []map[string]any - if err := json.Unmarshal(resourceOut, &resourceData); err == nil { - nodeInfo["resources"] = resourceData - } - } - - // Конфигурация ноды - configCmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/config", nodeName), "--output-format", "json") - if configOut, err := configCmd.Output(); err == nil { - var configData map[string]any - if err := json.Unmarshal(configOut, &configData); err == nil { - nodeInfo["config"] = configData - } - } - - // Сетевая информация - networkCmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/network", nodeName), "--output-format", "json") - if networkOut, err := networkCmd.Output(); err == nil { - var networkData []map[string]any - if err := json.Unmarshal(networkOut, &networkData); err == nil { - nodeInfo["network"] = networkData - } - } - - // Информация о хранилищах ноды - storageCmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/storage", nodeName), "--output-format", "json") - if storageOut, err := storageCmd.Output(); err == nil { - var storageData []map[string]any - if err := json.Unmarshal(storageOut, &storageData); err == nil { - nodeInfo["storage"] = storageData - } + if memory, ok := hardware["memory_total_mb"].(int); ok { + totalMemory += memory } } - nodesInfo = append(nodesInfo, nodeInfo) + if resources, ok := node["resources"].(map[string]interface{}); ok { + if used, ok := resources["memory_used_mb"].(int); ok { + usedMemory += used + } + } } - result["nodes"] = nodesInfo - return result, nil + result["cpu"].(map[string]interface{})["total_cores"] = totalCores + result["cpu"].(map[string]interface{})["online_cores"] = onlineCores + result["memory"].(map[string]interface{})["total_mb"] = totalMemory + result["memory"].(map[string]interface{})["used_mb"] = usedMemory + result["nodes"].(map[string]interface{})["online"] = onlineNodes + + return result } -// collectQuorumInfo получает подробную информацию о кворуме кластера -func collectQuorumInfo(ctx context.Context) (map[string]any, error) { - result := map[string]any{ +// getQuorumInfo получает информацию о кворуме +func getQuorumInfo(ctx context.Context) (map[string]interface{}, error) { + result := map[string]interface{}{ "quorate": false, "members": 0, "total_votes": 0, @@ -396,37 +563,19 @@ func collectQuorumInfo(ctx context.Context) (map[string]any, error) { cmd := exec.CommandContext(ctx, "corosync-quorumtool", "-s") out, err := cmd.Output() if err == nil { - return result, err - } - - lines := strings.Split(string(out), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - - if strings.Contains(line, "Quorate:") { - result["quorate"] = strings.Contains(line, "Yes") + lines := strings.Split(string(out), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, "Quorate:") { + result["quorate"] = strings.Contains(line, "Yes") } - if strings.HasPrefix(line, "Nodes:") { + if strings.HasPrefix(line, "Nodes:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { - if count, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { - result["members"] = count - } - } - } - if strings.HasPrefix(line, "Total votes:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { - result["total_votes"] = votes - } - } - } - if strings.HasPrefix(line, "Expected votes:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { - result["expected_votes"] = votes + // Простой парсинг числа + if count := parseNumber(parts[1]); count > 0 { + result["members"] = count + } } } } @@ -436,435 +585,15 @@ func collectQuorumInfo(ctx context.Context) (map[string]any, error) { return result, nil } - - - - -// collectStoragesInfo собирает информацию о хранилищах кластера -func collectStoragesInfo(ctx context.Context) ([]map[string]any, error) { - var storages []map[string]any - - // Пробуем pvesm status - if _, err := exec.LookPath("pvesm"); err == nil { - cmd := exec.CommandContext(ctx, "pvesm", "status") - out, err := cmd.Output() - if err != nil { - return storages, err - } - - lines := strings.Split(string(out), "\n") - for i, line := range lines { - if i == 0 { // пропускаем заголовок - continue - } - line = strings.TrimSpace(line) - if line == "" { - continue - } - - fields := strings.Fields(line) - if len(fields) >= 4 { - storage := map[string]any{ - "storage_id": fields[0], - "type": fields[1], - "content": strings.Split(fields[3], ","), - "shared": false, - } - - // Парсим размеры если есть - if len(fields) >= 7 { - // Парсим размер в формате "500.00G" - if sizeStr := fields[4]; sizeStr != "-" { - if size, err := parseSizeToGB(sizeStr); err == nil { - storage["total_gb"] = size - } - } - if usedStr := fields[5]; usedStr != "-" { - if used, err := parseSizeToGB(usedStr); err == nil { - storage["used_gb"] = used - } - } - if availStr := fields[6]; availStr != "-" { - if avail, err := parseSizeToGB(availStr); err == nil { - storage["avail_gb"] = avail - } - } - } - - // Определяем shared по типу - sharedTypes := []string{"nfs", "cifs", "glusterfs", "cephfs"} - for _, st := range sharedTypes { - if fields[1] == st { - storage["shared"] = true - break - } - } - - storages = append(storages, storage) - } - } - } - - return storages, nil -} - -// parseSizeToGB парсит размер в формате "500.00G" в гигабайты -func parseSizeToGB(sizeStr string) (float64, error) { - sizeStr = strings.TrimSpace(sizeStr) - if sizeStr == "" || sizeStr == "-" { - return 0, nil - } - - // Убираем суффикс и парсим число - var multiplier float64 = 1 - if strings.HasSuffix(sizeStr, "T") { - multiplier = 1024 - sizeStr = strings.TrimSuffix(sizeStr, "T") - } else if strings.HasSuffix(sizeStr, "G") { - multiplier = 1 - sizeStr = strings.TrimSuffix(sizeStr, "G") - } else if strings.HasSuffix(sizeStr, "M") { - multiplier = 1.0 / 1024 - sizeStr = strings.TrimSuffix(sizeStr, "M") - } - - value, err := strconv.ParseFloat(sizeStr, 64) - if err != nil { - return 0, err - } - - return value * multiplier, nil -} - -// collectDetailedNodesInfo собирает подробную информацию о нодах кластера -func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID string) ([]map[string]any, error) { - var nodes []map[string]any - - // Получаем данные из pvecm nodes (имена нод) - nodesData := parsePvecmNodes(ctx) +// getCorosyncInfo получает информацию о corosync +func getCorosyncInfo(ctx context.Context) (map[string]interface{}, error) { + result := map[string]interface{}{} - // Получаем данные из pvecm status (IP адреса) - statusData := parsePvecmStatus(ctx) - - // Объединяем данные - combinedNodes := combineNodeInfo(nodesData, statusData) - - // Обрабатываем объединенные данные - for _, nodeInfo := range combinedNodes { - nodeID := fmt.Sprintf("%08x", nodeInfo.NodeID) - nodeName := nodeInfo.Name - nodeIP := nodeInfo.IP - - // Определяем, является ли нода локальной - isLocal := strings.Contains(nodeIP, "10.14.88.12") // IP локальной ноды - if isLocal { - // Для локальной ноды получаем имя хоста - if hostname, err := os.Hostname(); err == nil { - nodeName = hostname - } - } - - // Проверяем доступность ноды через ping - isOnline := checkNodeOnline(ctx, nodeIP) - - // Создаем структуру ноды с правильным порядком полей - node := map[string]any{ - // Общая информация о ноде (выводится первой) - "node_id": nodeInfo.NodeID, // Используем обычный формат вместо hex - "name": nodeName, - "online": isOnline, - "cluster_id": generateClusterID(clusterName, clusterUUID), - "cluster_uuid": clusterUUID, - "node_uid": generateNodeUID(clusterUUID, nodeID), - } - - // Если нода онлайн, собираем дополнительную информацию - if isOnline { - // Corosync IP адрес ноды - node["corosync_ip"] = nodeIP - - // Информация о машине - if machineInfo, err := getNodeMachineInfo(ctx); err == nil { - for k, v := range machineInfo { - node[k] = v - } - } - - // Информация об ОС - if osInfo, err := getNodeOSInfo(ctx); err == nil { - node["os"] = osInfo - } - - // Ресурсы (использование CPU, памяти, load average) - if resInfo, err := getNodeResources(ctx); err == nil { - node["resources"] = resInfo - } - - // Real IPs (реальные IP адреса ноды) - if realIPs, err := getNodeRealIPs(ctx, nodeIP); err == nil { - node["real_ips"] = realIPs - } - - // Детальная информация о железе (выводится после общей информации) - if hwInfo, err := getNodeHardwareInfo(ctx); err == nil { - node["hardware"] = hwInfo - } - } else { - // Для офлайн нод заполняем пустыми значениями в правильном порядке - node["corosync_ip"] = "" - node["machine_id"] = "" - node["product_uuid"] = "" - node["os"] = map[string]any{ - "kernel": "", - "pve_version": "", - "uptime_sec": 0, - } - node["resources"] = map[string]any{ - "cpu_usage_percent": 0, - "memory_used_mb": 0, - "swap_used_mb": 0, - "loadavg": []float64{0, 0, 0}, - } - node["real_ips"] = []string{} - node["hardware"] = map[string]any{ - "cpu_model": "", - "cpu_cores": 0, - "sockets": 0, - "threads": 0, - "memory_total_mb": 0, - } - } - - nodes = append(nodes, node) - } - - return nodes, nil -} - -// NodeInfo структура для хранения информации о ноде -type NodeInfo struct { - NodeID int - Votes int - Name string - IP string -} - -// parsePvecmNodes парсит вывод pvecm nodes для получения имен нод -func parsePvecmNodes(ctx context.Context) []NodeInfo { - var nodes []NodeInfo - - if _, err := exec.LookPath("pvecm"); err != nil { - return nodes - } - - cmd := exec.CommandContext(ctx, "pvecm", "nodes") - out, err := cmd.Output() - if err != nil { - return nodes - } - - lines := strings.Split(string(out), "\n") - inDataSection := false - - for _, line := range lines { - line = strings.TrimSpace(line) - - // Пропускаем заголовок - if strings.Contains(line, "Nodeid") && strings.Contains(line, "Votes") && strings.Contains(line, "Name") { - inDataSection = true - continue - } - - if inDataSection { - if line == "" { - continue - } - - // Парсим строки с данными нод: "1 1 pnode12" - fields := strings.Fields(line) - if len(fields) >= 3 { - if nodeID, err := strconv.Atoi(fields[0]); err == nil { - if votes, err := strconv.Atoi(fields[1]); err == nil { - name := fields[2] - - nodes = append(nodes, NodeInfo{ - NodeID: nodeID, - Votes: votes, - Name: name, - }) - } - } - } - } - } - - return nodes -} - -// parsePvecmStatus парсит вывод pvecm status для получения IP адресов -func parsePvecmStatus(ctx context.Context) []NodeInfo { - var status []NodeInfo - - if _, err := exec.LookPath("pvecm"); err != nil { - return status - } - - cmd := exec.CommandContext(ctx, "pvecm", "status") - out, err := cmd.Output() - if err != nil { - return status - } - - lines := strings.Split(string(out), "\n") - inMembershipSection := false - - for _, line := range lines { - line = strings.TrimSpace(line) - - // Находим секцию Membership information - if strings.Contains(line, "Membership information") { - inMembershipSection = true - continue - } - - if inMembershipSection { - // Пропускаем заголовки и разделители - if strings.Contains(line, "Nodeid") || strings.Contains(line, "----") || line == "" { - continue - } - - // Парсим строки с данными нод: "0x00000001 1 10.14.88.22" - fields := strings.Fields(line) - if len(fields) >= 3 { - // Конвертируем hex в decimal - nodeIDHex := strings.TrimPrefix(fields[0], "0x") - if nodeID, err := strconv.ParseInt(nodeIDHex, 16, 32); err == nil { - if votes, err := strconv.Atoi(fields[1]); err == nil { - ip := fields[2] - - status = append(status, NodeInfo{ - NodeID: int(nodeID), - Votes: votes, - IP: ip, - }) - } - } - } - } - } - - return status -} - -// combineNodeInfo объединяет данные из pvecm nodes и pvecm status -func combineNodeInfo(nodes, status []NodeInfo) []NodeInfo { - var combined []NodeInfo - - // Создаем мапы для быстрого поиска - nodesMap := make(map[int]NodeInfo) - statusMap := make(map[int]NodeInfo) - - for _, node := range nodes { - nodesMap[node.NodeID] = node - } - - for _, stat := range status { - statusMap[stat.NodeID] = stat - } - - // Объединяем данные - for i := 1; i <= 32; i++ { - if node, ok := nodesMap[i]; ok { - if stat, ok := statusMap[i]; ok { - combined = append(combined, NodeInfo{ - NodeID: i, - Votes: node.Votes, - Name: node.Name, - IP: stat.IP, - }) - } - } - } - - return combined -} - -// checkNodeOnline проверяет доступность ноды через ping -func checkNodeOnline(ctx context.Context, nodeIP string) bool { - // Создаем контекст с таймаутом для ping - pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - - // Выполняем ping с 1 пакетом - cmd := exec.CommandContext(pingCtx, "ping", "-c", "1", "-W", "1", nodeIP) - err := cmd.Run() - return err == nil -} - -// generateNodeUID создает уникальный ID ноды на основе cluster_uuid + node_id -func generateNodeUID(clusterUUID, nodeID string) string { - base := clusterUUID + ":" + nodeID - hash := sha256.Sum256([]byte(base)) - return hex.EncodeToString(hash[:])[:16] -} - -// collectCorosyncInfo собирает информацию о corosync -func collectCorosyncInfo(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // Читаем corosync.conf - corosyncPath := "/etc/corosync/corosync.conf" - if _, err := os.Stat(corosyncPath); os.IsNotExist(err) { - altPaths := []string{ - "/etc/pve/corosync.conf", - "/var/lib/pve-cluster/corosync.conf", - } - for _, path := range altPaths { - if _, err := os.Stat(path); err == nil { - corosyncPath = path - break - } - } - } - - if data, err := os.ReadFile(corosyncPath); err == nil { - // Парсим основные параметры corosync - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "#") || line == "" { - continue - } - - if strings.HasPrefix(line, "bindnetaddr:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - result["bindnetaddr"] = strings.TrimSpace(parts[1]) - } - } - if strings.HasPrefix(line, "mcastport:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - if port, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { - result["mcastport"] = port - } - } - } - if strings.HasPrefix(line, "ttl:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - if ttl, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { - result["ttl"] = ttl - } - } - } - } - } - - // Статус corosync + // Пробуем получить информацию через corosync-quorumtool if _, err := exec.LookPath("corosync-quorumtool"); err == nil { cmd := exec.CommandContext(ctx, "corosync-quorumtool", "-s") - if out, err := cmd.Output(); err == nil { + out, err := cmd.Output() + if err == nil { lines := strings.Split(string(out), "\n") for _, line := range lines { line = strings.TrimSpace(line) @@ -881,609 +610,311 @@ func collectCorosyncInfo(ctx context.Context) (map[string]any, error) { return result, nil } -// Вспомогательные функции для сбора информации о нодах -func getNodeIP(ctx context.Context, nodeName string) (string, error) { - // Пробуем получить IP через hostname - cmd := exec.CommandContext(ctx, "getent", "hosts", nodeName) + +// getClusterMembers читает список нод из /etc/pve/.members +func getClusterMembers() ([]string, error) { + data, err := os.ReadFile("/etc/pve/.members") + if err != nil { + return nil, err + } + + // Парсим JSON + var members []string + if err := json.Unmarshal(data, &members); err != nil { + // Если не JSON, пробуем построчно + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" { + members = append(members, line) + } + } + } + + return members, nil +} + +// getNodeIDsFromCorosync получает nodeid для каждой ноды из corosync.conf +func getNodeIDsFromCorosync(confPath string, members []string) (map[string]int, error) { + data, err := os.ReadFile(confPath) + if err != nil { + return nil, err + } + + nodeIDs := make(map[string]int) + content := string(data) + + // Пробуем парсить как JSON сначала + var jsonData map[string]interface{} + if err := json.Unmarshal(data, &jsonData); err == nil { + // Если это JSON, ищем nodelist + if nodelist, ok := jsonData["nodelist"].(map[string]interface{}); ok { + for nodeName, nodeData := range nodelist { + if nodeInfo, ok := nodeData.(map[string]interface{}); ok { + if id, ok := nodeInfo["id"].(float64); ok { + nodeIDs[nodeName] = int(id) + } + } + } + } + return nodeIDs, nil + } + + // Если не JSON, используем regex для текстового формата + for _, member := range members { + // Ищем блок node { ... nodeid: X ... name: member } + re := regexp.MustCompile(`node\s*{[^}]*name:\s*` + member + `[^}]*nodeid:\s*([0-9]+)`) + match := re.FindStringSubmatch(content) + if len(match) > 1 { + if id, err := strconv.Atoi(match[1]); err == nil { + nodeIDs[member] = id + } + } + } + + return nodeIDs, nil +} + +// generateClusterID создает уникальный ID кластера на основе UUID +func generateClusterID(clusterUUID string) string { + if clusterUUID == "" { + clusterUUID = "default-cluster-uuid" + } + hash := sha256.Sum256([]byte(clusterUUID)) + return hex.EncodeToString(hash[:])[:16] // первые 16 символов +} + +// generateNodeUID создает уникальный ID ноды +func generateNodeUID(clusterUUID, nodeID string) string { + base := clusterUUID + ":" + nodeID + hash := sha256.Sum256([]byte(base)) + return hex.EncodeToString(hash[:])[:16] +} + +// getCorosyncIP получает corosync IP адрес ноды +func getCorosyncIP(ctx context.Context, nodeName string) (string, error) { + // Пробуем получить через pvesh + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/status", "--output-format", "json") out, err := cmd.Output() if err == nil { - fields := strings.Fields(string(out)) - if len(fields) > 0 { - return fields[0], nil + var statusData map[string]interface{} + if err := json.Unmarshal(out, &statusData); err == nil { + if ip, ok := statusData["ip"].(string); ok { + return ip, nil + } } } - return "", errors.New("node IP not found") + return "", fmt.Errorf("corosync IP not found") } -func getNodeMachineInfo(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // Machine ID - if data, err := os.ReadFile("/etc/machine-id"); err == nil { - result["machine_id"] = strings.TrimSpace(string(data)) - } - - // Product UUID - if data, err := os.ReadFile("/sys/class/dmi/id/product_uuid"); err == nil { - result["product_uuid"] = strings.TrimSpace(string(data)) - } - - return result, nil -} - -func getNodeOSInfo(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // Kernel - if data, err := os.ReadFile("/proc/version"); err == nil { - version := strings.TrimSpace(string(data)) - if parts := strings.Fields(version); len(parts) >= 3 { - result["kernel"] = strings.Join(parts[0:3], " ") - } - } - - // PVE version - if _, err := exec.LookPath("pveversion"); err == nil { - cmd := exec.CommandContext(ctx, "pveversion", "-v") - if out, err := cmd.Output(); err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - if strings.Contains(line, "pve-manager") { - parts := strings.Fields(line) - if len(parts) >= 2 { - result["pve_version"] = parts[1] - } - } - } - } - } - - // Uptime - if data, err := os.ReadFile("/proc/uptime"); err == nil { - fields := strings.Fields(string(data)) - if len(fields) > 0 { - if uptime, err := strconv.ParseFloat(fields[0], 64); err == nil { - result["uptime_sec"] = int64(uptime) - } - } - } - - return result, nil -} - -func getNodeHardwareInfo(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // CPU информация - if data, err := os.ReadFile("/proc/cpuinfo"); err == nil { - lines := strings.Split(string(data), "\n") - var cpuModel string - var cores, sockets int - seenModels := make(map[string]bool) - seenSockets := make(map[string]bool) - - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "model name") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - model := strings.TrimSpace(parts[1]) - if !seenModels[model] { - cpuModel = model - seenModels[model] = true - } - } - } - if strings.HasPrefix(line, "processor") { - cores++ - } - if strings.HasPrefix(line, "physical id") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - socket := strings.TrimSpace(parts[1]) - if !seenSockets[socket] { - sockets++ - seenSockets[socket] = true - } - } - } - } - - result["cpu_model"] = cpuModel - result["cpu_cores"] = cores - result["sockets"] = sockets - result["threads"] = cores // В упрощенном виде - } - - // Memory - if data, err := os.ReadFile("/proc/meminfo"); err == nil { - lines := strings.Split(string(data), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "MemTotal:") { - fields := strings.Fields(line) - if len(fields) >= 2 { - if kb, err := strconv.ParseUint(fields[1], 10, 64); err == nil { - result["memory_total_mb"] = int(kb / 1024) - } - } - } - } - } - - return result, nil -} - -func getNodeResources(ctx context.Context) (map[string]any, error) { - result := map[string]any{} - - // CPU usage (упрощенная версия) - result["cpu_usage_percent"] = 0.0 - - // Memory usage - if data, err := os.ReadFile("/proc/meminfo"); err == nil { - lines := strings.Split(string(data), "\n") - var total, free, buffers, cached uint64 - for _, line := range lines { - fields := strings.Fields(line) - if len(fields) >= 2 { - if val, err := strconv.ParseUint(fields[1], 10, 64); err == nil { - switch fields[0] { - case "MemTotal:": - total = val - case "MemFree:": - free = val - case "Buffers:": - buffers = val - case "Cached:": - cached = val - } - } - } - } - used := total - free - buffers - cached - result["memory_used_mb"] = int(used / 1024) - } - - // Swap - result["swap_used_mb"] = 0 - - // Load average - if data, err := os.ReadFile("/proc/loadavg"); err == nil { - fields := strings.Fields(string(data)) - if len(fields) >= 3 { - var loadavg []float64 - for i := 0; i < 3; i++ { - if val, err := strconv.ParseFloat(fields[i], 64); err == nil { - loadavg = append(loadavg, val) - } - } - result["loadavg"] = loadavg - } - } - - return result, nil -} - -func getNodeNetworkInfo(ctx context.Context) ([]map[string]any, error) { - var networks []map[string]any - - // Упрощенная версия - только основные интерфейсы - interfaces := []string{"eth0", "ens33", "enp0s3", "vmbr0"} - for _, iface := range interfaces { - // Проверяем существование интерфейса - if _, err := os.Stat("/sys/class/net/" + iface); err == nil { - network := map[string]any{ - "iface": iface, - } - - // MAC адрес - if data, err := os.ReadFile("/sys/class/net/" + iface + "/address"); err == nil { - network["mac"] = strings.TrimSpace(string(data)) - } - - // IP адрес (упрощенно) - network["ip"] = "" - network["rx_bytes"] = 0 - network["tx_bytes"] = 0 - network["errors"] = 0 - - // Тип для bridge - if strings.HasPrefix(iface, "vmbr") { - network["type"] = "bridge" - } - - networks = append(networks, network) - } - } - - return networks, nil -} - -func getNodeDiskInfo(ctx context.Context) ([]map[string]any, error) { - var disks []map[string]any - - // Упрощенная версия - только основные диски - diskPaths := []string{"/dev/sda", "/dev/nvme0n1", "/dev/vda"} - for _, disk := range diskPaths { - if _, err := os.Stat(disk); err == nil { - diskInfo := map[string]any{ - "device": disk, - "model": "", - "size_gb": 0, - "used_gb": 0, - "health": "UNKNOWN", - } - - // Попытка получить размер - if data, err := os.ReadFile("/sys/block/" + strings.TrimPrefix(disk, "/dev/") + "/size"); err == nil { - if size, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64); err == nil { - diskInfo["size_gb"] = int(size * 512 / 1024 / 1024 / 1024) // секторы -> GB - } - } - - disks = append(disks, diskInfo) - } - } - - return disks, nil -} - -func getNodeServices(ctx context.Context) ([]map[string]any, error) { - var services []map[string]any - - serviceNames := []string{"pve-cluster", "pvedaemon", "pveproxy", "corosync"} - for _, svc := range serviceNames { - service := map[string]any{ - "name": svc, - "active": isServiceRunning(ctx, svc), - } - services = append(services, service) - } - - return services, nil -} - -func getNodeLogs(ctx context.Context) ([]map[string]any, error) { - // Упрощенная версия - возвращаем пустой массив - // В реальной реализации можно читать логи из /var/log/pve/ - return []map[string]any{}, nil -} - -func getNodeGPUInfo(ctx context.Context) ([]map[string]any, error) { - var gpus []map[string]any - - // Пробуем nvidia-smi для NVIDIA GPU - if _, err := exec.LookPath("nvidia-smi"); err == nil { - cmd := exec.CommandContext(ctx, "nvidia-smi", "--query-gpu=index,name,memory.total,memory.used,utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits") - out, err := cmd.Output() - if err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - fields := strings.Split(line, ", ") - if len(fields) >= 6 { - gpu := map[string]any{ - "index": 0, - "model": "", - "memory_total_mb": 0, - "memory_used_mb": 0, - "utilization_percent": 0.0, - "temperature_c": 0.0, - } - - // Парсим индекс - if idx, err := strconv.Atoi(fields[0]); err == nil { - gpu["index"] = idx - } - - // Модель GPU - gpu["model"] = strings.TrimSpace(fields[1]) - - // Память (в МБ) - if total, err := strconv.Atoi(fields[2]); err == nil { - gpu["memory_total_mb"] = total - } - if used, err := strconv.Atoi(fields[3]); err == nil { - gpu["memory_used_mb"] = used - } - - // Утилизация - if util, err := strconv.ParseFloat(fields[4], 64); err == nil { - gpu["utilization_percent"] = util - } - - // Температура - if temp, err := strconv.ParseFloat(fields[5], 64); err == nil { - gpu["temperature_c"] = temp - } - - gpus = append(gpus, gpu) - } - } - } - } - - // Пробуем lspci для других GPU (AMD, Intel) - if len(gpus) == 0 { - cmd := exec.CommandContext(ctx, "lspci", "-nn") - out, err := cmd.Output() - if err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.Contains(line, "VGA") || strings.Contains(line, "3D") || strings.Contains(line, "Display") { - // Простая обработка для не-NVIDIA GPU - gpu := map[string]any{ - "index": 0, - "model": line, - "memory_total_mb": 0, - "memory_used_mb": 0, - "utilization_percent": 0.0, - "temperature_c": 0.0, - } - gpus = append(gpus, gpu) - } - } - } - } - - return gpus, nil -} - -// getNodeRealIPs получает реальные IP адреса ноды (исключая corosync IP) -func getNodeRealIPs(ctx context.Context, corosyncIP string) ([]string, error) { +// getRealIPs получает реальные IP адреса ноды (исключая corosync IP) +func getRealIPs(ctx context.Context, nodeName string) ([]string, error) { var realIPs []string - - // НЕ добавляем corosync IP в real_ips - - // Пробуем получить дополнительные IP через ip addr - if _, err := exec.LookPath("ip"); err == nil { - cmd := exec.CommandContext(ctx, "ip", "addr", "show") - out, err := cmd.Output() - if err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - // Ищем строки с inet (IPv4 адреса) - if strings.HasPrefix(line, "inet ") && !strings.Contains(line, "127.0.0.1") { - parts := strings.Fields(line) - if len(parts) >= 2 { - ip := strings.Split(parts[1], "/")[0] // Убираем маску подсети - - // Исключаем corosync IP - if corosyncIP != "" && ip == corosyncIP { - continue - } - - // Проверяем, что это не уже добавленный IP - found := false - for _, existingIP := range realIPs { - if existingIP == ip { - found = true - break - } - } - if !found { - realIPs = append(realIPs, ip) - } - } - } - } - } - } - - // Fallback: пробуем через hostname -I - if len(realIPs) <= 1 { - cmd := exec.CommandContext(ctx, "hostname", "-I") - out, err := cmd.Output() - if err == nil { - ips := strings.Fields(string(out)) - for _, ip := range ips { - ip = strings.TrimSpace(ip) - if ip != "" && ip != "127.0.0.1" { - // Исключаем corosync IP - if corosyncIP != "" && ip == corosyncIP { - continue - } - - // Проверяем, что это не уже добавленный IP - found := false - for _, existingIP := range realIPs { - if existingIP == ip { - found = true - break - } - } - if !found { + + // Пробуем получить через pvesh + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/network", "--output-format", "json") + out, err := cmd.Output() + if err == nil { + var networkData []map[string]interface{} + if err := json.Unmarshal(out, &networkData); err == nil { + for _, iface := range networkData { + if address, ok := iface["address"].(string); ok && address != "" { + // Убираем маску подсети + if ip := strings.Split(address, "/")[0]; ip != "" { realIPs = append(realIPs, ip) } } } } } - + return realIPs, nil } +// getMachineID получает machine ID +func getMachineID() (string, error) { + data, err := os.ReadFile("/etc/machine-id") + if err != nil { + return "", err + } + return strings.TrimSpace(string(data)), nil +} -// calculateClusterResources вычисляет агрегированные ресурсы кластера -func calculateClusterResources(nodes []map[string]any, storages []map[string]any) (map[string]any, error) { - result := map[string]any{ - "cpu": map[string]any{ - "total_cores": 0, - "total_sockets": 0, - "total_threads": 0, - "online_cores": 0, - "online_sockets": 0, - "online_threads": 0, - }, - "memory": map[string]any{ - "total_mb": 0, - "used_mb": 0, - "online_total": 0, - "online_used": 0, - }, - "storage": map[string]any{ - "total_gb": 0.0, - "used_gb": 0.0, - "avail_gb": 0.0, - "shared_gb": 0.0, - "local_gb": 0.0, - }, - "nodes": map[string]any{ - "total": 0, - "online": 0, - }, +// getProductUUID получает product UUID +func getProductUUID() (string, error) { + data, err := os.ReadFile("/sys/class/dmi/id/product_uuid") + if err != nil { + return "", err + } + return strings.TrimSpace(string(data)), nil +} + +// checkNodeOnline проверяет онлайн статус ноды через pvesh +func checkNodeOnline(ctx context.Context, nodeName string) (bool, error) { + // Используем pvesh get /nodes --output-format json для получения списка нод + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes", "--output-format", "json") + out, err := cmd.Output() + if err != nil { + return false, err } - // Агрегируем данные по нодам - totalNodes := 0 - onlineNodes := 0 - totalCores := 0 - totalSockets := 0 - totalThreads := 0 - onlineCores := 0 - onlineSockets := 0 - onlineThreads := 0 - totalMemory := 0 - usedMemory := 0 - onlineTotalMemory := 0 - onlineUsedMemory := 0 - + var nodes []map[string]interface{} + if err := json.Unmarshal(out, &nodes); err != nil { + return false, err + } + // Ищем нашу ноду в списке for _, node := range nodes { - totalNodes++ - - // Проверяем статус ноды - if online, ok := node["online"].(bool); ok && online { - onlineNodes++ - } - - // Агрегируем CPU информацию - if hardware, ok := node["hardware"].(map[string]any); ok { - if cores, ok := hardware["cpu_cores"].(int); ok { - totalCores += cores - if online, ok := node["online"].(bool); ok && online { - onlineCores += cores - } - } - if sockets, ok := hardware["sockets"].(int); ok { - totalSockets += sockets - if online, ok := node["online"].(bool); ok && online { - onlineSockets += sockets - } - } - if threads, ok := hardware["threads"].(int); ok { - totalThreads += threads - if online, ok := node["online"].(bool); ok && online { - onlineThreads += threads - } - } - if memory, ok := hardware["memory_total_mb"].(int); ok { - totalMemory += memory - if online, ok := node["online"].(bool); ok && online { - onlineTotalMemory += memory - } + if name, ok := node["node"].(string); ok && name == nodeName { + if status, ok := node["status"].(string); ok { + return status == "online", nil } } - - // Агрегируем использование памяти - if resources, ok := node["resources"].(map[string]any); ok { - if used, ok := resources["memory_used_mb"].(int); ok { - usedMemory += used - if online, ok := node["online"].(bool); ok && online { - onlineUsedMemory += used - } - } - } - } - // Обновляем результат для нод - result["cpu"].(map[string]any)["total_cores"] = totalCores - result["cpu"].(map[string]any)["total_sockets"] = totalSockets - result["cpu"].(map[string]any)["total_threads"] = totalThreads - result["cpu"].(map[string]any)["online_cores"] = onlineCores - result["cpu"].(map[string]any)["online_sockets"] = onlineSockets - result["cpu"].(map[string]any)["online_threads"] = onlineThreads + return false, fmt.Errorf("node %s not found", nodeName) +} - result["memory"].(map[string]any)["total_mb"] = totalMemory - result["memory"].(map[string]any)["used_mb"] = usedMemory - result["memory"].(map[string]any)["online_total"] = onlineTotalMemory - result["memory"].(map[string]any)["online_used"] = onlineUsedMemory +// getAlternativeResources получает данные о ресурсах альтернативными методами +func getAlternativeResources(ctx context.Context, nodeName string) (map[string]interface{}, error) { + result := map[string]interface{}{ + "cpu_usage_percent": 0, + "memory_used_mb": 0, + "swap_used_mb": 0, + "loadavg": []float64{0, 0, 0}, + } - - result["nodes"].(map[string]any)["total"] = totalNodes - result["nodes"].(map[string]any)["online"] = onlineNodes - - // Агрегируем данные по хранилищам (если переданы) - if storages != nil { - totalStorageSize := 0.0 - totalStorageUsed := 0.0 - totalStorageAvail := 0.0 - sharedStorageSize := 0.0 - localStorageSize := 0.0 - - for _, storage := range storages { - if size, ok := storage["total_gb"].(float64); ok { - totalStorageSize += size - } - if used, ok := storage["used_gb"].(float64); ok { - totalStorageUsed += used - } - if avail, ok := storage["avail_gb"].(float64); ok { - totalStorageAvail += avail - } - - // Разделяем на shared и local - if shared, ok := storage["shared"].(bool); ok && shared { - if size, ok := storage["total_gb"].(float64); ok { - sharedStorageSize += size + // Пробуем получить данные через /nodes/{node}/rrddata + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/rrddata", "--output-format", "json") + out, err := cmd.Output() + if err == nil { + var rrdData map[string]interface{} + if err := json.Unmarshal(out, &rrdData); err == nil { + // Извлекаем последние данные о CPU и памяти + if cpu, ok := rrdData["cpu"].([]interface{}); ok && len(cpu) > 0 { + if lastCPU, ok := cpu[len(cpu)-1].(float64); ok { + result["cpu_usage_percent"] = lastCPU * 100 } - } else { - if size, ok := storage["total_gb"].(float64); ok { - localStorageSize += size + } + if mem, ok := rrdData["mem"].([]interface{}); ok && len(mem) > 0 { + if lastMem, ok := mem[len(mem)-1].(float64); ok { + result["memory_used_mb"] = int(lastMem / 1024 / 1024) + } + } + if load, ok := rrdData["loadavg"].([]interface{}); ok && len(load) > 0 { + if lastLoad, ok := load[len(load)-1].(float64); ok { + result["loadavg"] = []float64{lastLoad, lastLoad, lastLoad} } } } + } - result["storage"].(map[string]any)["total_gb"] = totalStorageSize - result["storage"].(map[string]any)["used_gb"] = totalStorageUsed - result["storage"].(map[string]any)["avail_gb"] = totalStorageAvail - result["storage"].(map[string]any)["shared_gb"] = sharedStorageSize - result["storage"].(map[string]any)["local_gb"] = localStorageSize - } else { - // Если storages не переданы, устанавливаем нулевые значения - result["storage"].(map[string]any)["total_gb"] = 0.0 - result["storage"].(map[string]any)["used_gb"] = 0.0 - result["storage"].(map[string]any)["avail_gb"] = 0.0 - result["storage"].(map[string]any)["shared_gb"] = 0.0 - result["storage"].(map[string]any)["local_gb"] = 0.0 + // Если данные все еще пустые, пробуем через /nodes/{node}/capabilities + if result["cpu_usage_percent"].(float64) == 0 { + cmd = exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/capabilities", "--output-format", "json") + out, err = cmd.Output() + if err == nil { + var capsData map[string]interface{} + if err := json.Unmarshal(out, &capsData); err == nil { + // Пробуем извлечь информацию о CPU из capabilities + if cpu, ok := capsData["cpu"].(map[string]interface{}); ok { + if cores, ok := cpu["cores"].(float64); ok { + // Оценка использования CPU на основе количества ядер + result["cpu_usage_percent"] = float64(cores) * 10 // примерная оценка + } + } + } + } } return result, nil } -// isServiceRunning проверяет, запущен ли сервис -func isServiceRunning(ctx context.Context, serviceName string) bool { - // Пробуем systemctl - if _, err := exec.LookPath("systemctl"); err == nil { - cmd := exec.CommandContext(ctx, "systemctl", "is-active", serviceName) - err := cmd.Run() - return err == nil +// getAlternativeHardware получает данные о железе альтернативными методами +func getAlternativeHardware(ctx context.Context, nodeName string) (map[string]interface{}, error) { + result := map[string]interface{}{ + "cpu_model": "unknown", + "cpu_cores": 0, + "sockets": 0, + "threads": 0, + "memory_total_mb": 0, } - // Fallback: проверяем через ps - cmd := exec.CommandContext(ctx, "ps", "aux") + // Пробуем получить данные через /nodes/{node}/capabilities + cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/capabilities", "--output-format", "json") out, err := cmd.Output() - if err != nil { - return false + if err == nil { + var capsData map[string]interface{} + if err := json.Unmarshal(out, &capsData); err == nil { + // Извлекаем информацию о CPU + if cpu, ok := capsData["cpu"].(map[string]interface{}); ok { + if cores, ok := cpu["cores"].(float64); ok { + result["cpu_cores"] = int(cores) + result["threads"] = int(cores) // упрощенно + } + if model, ok := cpu["model"].(string); ok { + result["cpu_model"] = model + } + if sockets, ok := cpu["sockets"].(float64); ok { + result["sockets"] = int(sockets) + } + } + // Извлекаем информацию о памяти + if mem, ok := capsData["memory"].(map[string]interface{}); ok { + if total, ok := mem["total"].(float64); ok { + result["memory_total_mb"] = int(total / 1024 / 1024) + } + } + } } - return strings.Contains(string(out), serviceName) + // Если данные все еще пустые, пробуем через /nodes/{node}/hardware + if result["cpu_cores"].(int) == 0 { + cmd = exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/hardware", "--output-format", "json") + out, err = cmd.Output() + if err == nil { + var hwData []map[string]interface{} + if err := json.Unmarshal(out, &hwData); err == nil { + // Ищем информацию о CPU и памяти в hardware + for _, hw := range hwData { + if hwType, ok := hw["type"].(string); ok { + if hwType == "cpu" { + if cores, ok := hw["cores"].(float64); ok { + result["cpu_cores"] = int(cores) + } + if model, ok := hw["model"].(string); ok { + result["cpu_model"] = model + } + } else if hwType == "memory" { + if total, ok := hw["total"].(float64); ok { + result["memory_total_mb"] = int(total / 1024 / 1024) + } + } + } + } + } + } + } + + return result, nil } +// parseNumber пытается извлечь число из строки +func parseNumber(s string) int { + s = strings.TrimSpace(s) + if s == "" { + return 0 + } + + // Простой парсинг числа + num := 0 + for _, c := range s { + if c >= '0' && c <= '9' { + num = num*10 + int(c-'0') + } else { + break + } + } + + return num +} diff --git a/src/collectors/proxcluster/proxcluster_unsupported.go b/src/collectors/proxcluster/proxcluster_unsupported.go index fda6a28..e365db4 100644 --- a/src/collectors/proxcluster/proxcluster_unsupported.go +++ b/src/collectors/proxcluster/proxcluster_unsupported.go @@ -3,14 +3,14 @@ package main // Автор: Сергей Антропов, сайт: https://devops.org.ru -// Заглушка для неподдерживаемых платформ. +// Заглушка для неподдерживаемых платформ import ( "context" "errors" ) -// collectProxCluster возвращает пустой результат для неподдерживаемых платформ. -func collectProxCluster(ctx context.Context) (map[string]any, error) { +// collectProxCluster возвращает пустой результат для неподдерживаемых платформ +func collectProxCluster(ctx context.Context) (map[string]interface{}, error) { return nil, errors.New("proxcluster collector is not supported on this platform") }