feat: переход на pvesh API для получения данных о нодах и ресурсах

- Заменен ping на pvesh API для определения статуса нод
- Добавлена функция getNodesFromPvesh для получения списка нод через pvesh get /nodes
- Добавлена функция getNodeStatusFromPvesh для получения статуса ноды через pvesh get /nodes/{node}/status
- Улучшена функция getNodeResources - теперь использует pvesh API с fallback на /proc
- Улучшена функция getNodeHardwareInfo - теперь использует pvesh API с fallback на /proc
- Добавлен сбор реальных данных о CPU usage, memory usage, load average через pvesh
- Улучшена точность данных о ресурсах нод

Автор: Сергей Антропов, сайт: https://devops.org.ru
This commit is contained in:
Sergey Antropoff 2025-09-11 17:14:13 +03:00
parent 7f2b25e94d
commit b229c8fcdd

View File

@ -521,14 +521,8 @@ func parseSizeToGB(sizeStr string) (float64, error) {
func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID string) ([]map[string]any, error) {
var nodes []map[string]any
// Получаем данные из pvecm nodes (имена нод)
nodesData := parsePvecmNodes(ctx)
// Получаем данные из pvecm status (IP адреса)
statusData := parsePvecmStatus(ctx)
// Объединяем данные
combinedNodes := combineNodeInfo(nodesData, statusData)
// Получаем данные о нодах через pvesh API
combinedNodes := getNodesFromPvesh(ctx)
// Если не удалось получить данные через pvecm, создаем информацию о текущей ноде
if len(combinedNodes) == 0 {
@ -605,13 +599,8 @@ func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID stri
}
}
// Определяем статус ноды
isOnline := true // По умолчанию считаем ноду онлайн
// Если это не локальная нода, проверяем доступность через ping
if !isLocal {
isOnline = checkNodeOnline(ctx, nodeIP)
}
// Определяем статус ноды из pvesh данных
isOnline := getNodeStatusFromPvesh(ctx, nodeName)
// Создаем структуру ноды с правильным порядком полей
node := map[string]any{
@ -704,6 +693,106 @@ func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID stri
return nodes, nil
}
// getNodesFromPvesh получает информацию о нодах через pvesh API
func getNodesFromPvesh(ctx context.Context) []NodeInfo {
var nodes []NodeInfo
// Проверяем наличие pvesh
if _, err := exec.LookPath("pvesh"); err != nil {
return nodes
}
// Получаем список нод
cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes", "--output-format", "json")
out, err := cmd.Output()
if err != nil {
return nodes
}
var nodesData []map[string]any
if err := json.Unmarshal(out, &nodesData); err != nil {
return nodes
}
// Преобразуем данные в структуру NodeInfo
for i, nodeData := range nodesData {
nodeID := i + 1
name := ""
status := "offline"
if n, ok := nodeData["node"].(string); ok {
name = n
}
if s, ok := nodeData["status"].(string); ok {
status = s
}
// Получаем IP адрес ноды
var nodeIP string
if name != "" {
// Получаем детальную информацию о ноде
detailCmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", name), "--output-format", "json")
detailOut, err := detailCmd.Output()
if err == nil {
var detailData map[string]any
if err := json.Unmarshal(detailOut, &detailData); err == nil {
if ip, ok := detailData["ip"].(string); ok {
nodeIP = ip
}
}
}
}
// Определяем votes (обычно 1 для каждой ноды)
votes := 1
if status == "online" {
votes = 1
} else {
votes = 0
}
nodes = append(nodes, NodeInfo{
NodeID: nodeID,
Votes: votes,
Name: name,
IP: nodeIP,
})
}
return nodes
}
// getNodeStatusFromPvesh получает статус ноды через pvesh API
func getNodeStatusFromPvesh(ctx context.Context, nodeName string) bool {
if nodeName == "" {
return false
}
// Проверяем наличие pvesh
if _, err := exec.LookPath("pvesh"); err != nil {
return true // Если pvesh недоступен, считаем ноду онлайн
}
// Получаем статус ноды
cmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", nodeName), "--output-format", "json")
out, err := cmd.Output()
if err != nil {
return false
}
var statusData map[string]any
if err := json.Unmarshal(out, &statusData); err != nil {
return false
}
// Проверяем статус
if status, ok := statusData["status"].(string); ok {
return status == "online"
}
return false
}
// NodeInfo структура для хранения информации о ноде
type NodeInfo struct {
NodeID int
@ -1030,9 +1119,41 @@ func getNodeOSInfo(ctx context.Context) (map[string]any, error) {
}
func getNodeHardwareInfo(ctx context.Context) (map[string]any, error) {
result := map[string]any{}
result := map[string]any{
"cpu_model": "",
"cpu_cores": 0,
"sockets": 0,
"threads": 0,
"memory_total_mb": 0,
}
// CPU информация
// Пробуем получить данные через pvesh API
if hostname, err := os.Hostname(); err == nil {
if _, err := exec.LookPath("pvesh"); err == nil {
// Получаем информацию о ноде через pvesh
cmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", hostname), "--output-format", "json")
out, err := cmd.Output()
if err == nil {
var statusData map[string]any
if err := json.Unmarshal(out, &statusData); err == nil {
// CPU cores
if maxcpu, ok := statusData["maxcpu"].(float64); ok {
result["cpu_cores"] = int(maxcpu)
result["threads"] = int(maxcpu)
}
// Memory total
if maxmem, ok := statusData["maxmem"].(float64); ok {
result["memory_total_mb"] = int(maxmem)
}
}
}
}
}
// Fallback: получаем данные из /proc если pvesh недоступен
if result["cpu_cores"].(int) == 0 {
// CPU информация из /proc/cpuinfo
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
lines := strings.Split(string(data), "\n")
var cpuModel string
@ -1070,10 +1191,12 @@ func getNodeHardwareInfo(ctx context.Context) (map[string]any, error) {
result["cpu_model"] = cpuModel
result["cpu_cores"] = cores
result["sockets"] = sockets
result["threads"] = cores // В упрощенном виде
result["threads"] = cores
}
}
// Memory
if result["memory_total_mb"].(int) == 0 {
// Memory информация из /proc/meminfo
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
@ -1087,17 +1210,84 @@ func getNodeHardwareInfo(ctx context.Context) (map[string]any, error) {
}
}
}
}
return result, nil
}
func getNodeResources(ctx context.Context) (map[string]any, error) {
result := map[string]any{}
result := map[string]any{
"cpu_usage_percent": 0.0,
"memory_used_mb": 0,
"swap_used_mb": 0,
"loadavg": []float64{0, 0, 0},
}
// CPU usage (упрощенная версия)
result["cpu_usage_percent"] = 0.0
// Пробуем получить данные через pvesh API
if hostname, err := os.Hostname(); err == nil {
if _, err := exec.LookPath("pvesh"); err == nil {
// Получаем ресурсы через pvesh
cmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", hostname), "--output-format", "json")
out, err := cmd.Output()
if err == nil {
var statusData map[string]any
if err := json.Unmarshal(out, &statusData); err == nil {
// CPU usage
if cpu, ok := statusData["cpu"].(float64); ok {
result["cpu_usage_percent"] = cpu * 100
}
// Memory usage
if mem, ok := statusData["memory"].(float64); ok {
result["memory_used_mb"] = int(mem)
}
// Load average
if loadavg, ok := statusData["loadavg"].([]interface{}); ok && len(loadavg) >= 3 {
var load []float64
for i := 0; i < 3; i++ {
if val, ok := loadavg[i].(float64); ok {
load = append(load, val)
} else {
load = append(load, 0)
}
}
result["loadavg"] = load
}
}
}
}
}
// Fallback: получаем данные из /proc если pvesh недоступен
if result["cpu_usage_percent"].(float64) == 0.0 {
// CPU usage из /proc/stat
if data, err := os.ReadFile("/proc/stat"); err == nil {
lines := strings.Split(string(data), "\n")
if len(lines) > 0 {
fields := strings.Fields(lines[0])
if len(fields) >= 8 {
// Простой расчет CPU usage
var total, idle uint64
for i := 1; i < len(fields); i++ {
if val, err := strconv.ParseUint(fields[i], 10, 64); err == nil {
total += val
if i == 4 { // idle time
idle = val
}
}
}
if total > 0 {
usage := float64(total-idle) / float64(total) * 100
result["cpu_usage_percent"] = usage
}
}
}
}
}
if result["memory_used_mb"].(int) == 0 {
// Memory usage из /proc/meminfo
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
lines := strings.Split(string(data), "\n")
var total, free, buffers, cached uint64
@ -1121,11 +1311,10 @@ func getNodeResources(ctx context.Context) (map[string]any, error) {
used := total - free - buffers - cached
result["memory_used_mb"] = int(used / 1024)
}
}
// Swap
result["swap_used_mb"] = 0
// Load average
if len(result["loadavg"].([]float64)) == 0 || result["loadavg"].([]float64)[0] == 0 {
// Load average из /proc/loadavg
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
fields := strings.Fields(string(data))
if len(fields) >= 3 {
@ -1133,11 +1322,14 @@ func getNodeResources(ctx context.Context) (map[string]any, error) {
for i := 0; i < 3; i++ {
if val, err := strconv.ParseFloat(fields[i], 64); err == nil {
loadavg = append(loadavg, val)
} else {
loadavg = append(loadavg, 0)
}
}
result["loadavg"] = loadavg
}
}
}
return result, nil
}