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:
parent
7f2b25e94d
commit
b229c8fcdd
@ -521,14 +521,8 @@ func parseSizeToGB(sizeStr string) (float64, error) {
|
|||||||
func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID string) ([]map[string]any, error) {
|
func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID string) ([]map[string]any, error) {
|
||||||
var nodes []map[string]any
|
var nodes []map[string]any
|
||||||
|
|
||||||
// Получаем данные из pvecm nodes (имена нод)
|
// Получаем данные о нодах через pvesh API
|
||||||
nodesData := parsePvecmNodes(ctx)
|
combinedNodes := getNodesFromPvesh(ctx)
|
||||||
|
|
||||||
// Получаем данные из pvecm status (IP адреса)
|
|
||||||
statusData := parsePvecmStatus(ctx)
|
|
||||||
|
|
||||||
// Объединяем данные
|
|
||||||
combinedNodes := combineNodeInfo(nodesData, statusData)
|
|
||||||
|
|
||||||
// Если не удалось получить данные через pvecm, создаем информацию о текущей ноде
|
// Если не удалось получить данные через pvecm, создаем информацию о текущей ноде
|
||||||
if len(combinedNodes) == 0 {
|
if len(combinedNodes) == 0 {
|
||||||
@ -605,13 +599,8 @@ func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяем статус ноды
|
// Определяем статус ноды из pvesh данных
|
||||||
isOnline := true // По умолчанию считаем ноду онлайн
|
isOnline := getNodeStatusFromPvesh(ctx, nodeName)
|
||||||
|
|
||||||
// Если это не локальная нода, проверяем доступность через ping
|
|
||||||
if !isLocal {
|
|
||||||
isOnline = checkNodeOnline(ctx, nodeIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем структуру ноды с правильным порядком полей
|
// Создаем структуру ноды с правильным порядком полей
|
||||||
node := map[string]any{
|
node := map[string]any{
|
||||||
@ -704,6 +693,106 @@ func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID stri
|
|||||||
return nodes, nil
|
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 структура для хранения информации о ноде
|
// NodeInfo структура для хранения информации о ноде
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
NodeID int
|
NodeID int
|
||||||
@ -1030,58 +1119,93 @@ func getNodeOSInfo(ctx context.Context) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getNodeHardwareInfo(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 data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
if hostname, err := os.Hostname(); err == nil {
|
||||||
lines := strings.Split(string(data), "\n")
|
if _, err := exec.LookPath("pvesh"); err == nil {
|
||||||
var cpuModel string
|
// Получаем информацию о ноде через pvesh
|
||||||
var cores, sockets int
|
cmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", hostname), "--output-format", "json")
|
||||||
seenModels := make(map[string]bool)
|
out, err := cmd.Output()
|
||||||
seenSockets := make(map[string]bool)
|
if err == nil {
|
||||||
|
var statusData map[string]any
|
||||||
for _, line := range lines {
|
if err := json.Unmarshal(out, &statusData); err == nil {
|
||||||
line = strings.TrimSpace(line)
|
// CPU cores
|
||||||
if strings.HasPrefix(line, "model name") {
|
if maxcpu, ok := statusData["maxcpu"].(float64); ok {
|
||||||
parts := strings.SplitN(line, ":", 2)
|
result["cpu_cores"] = int(maxcpu)
|
||||||
if len(parts) == 2 {
|
result["threads"] = int(maxcpu)
|
||||||
model := strings.TrimSpace(parts[1])
|
|
||||||
if !seenModels[model] {
|
|
||||||
cpuModel = model
|
|
||||||
seenModels[model] = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
// Memory total
|
||||||
if strings.HasPrefix(line, "processor") {
|
if maxmem, ok := statusData["maxmem"].(float64); ok {
|
||||||
cores++
|
result["memory_total_mb"] = int(maxmem)
|
||||||
}
|
|
||||||
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
|
// Fallback: получаем данные из /proc если pvesh недоступен
|
||||||
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
if result["cpu_cores"].(int) == 0 {
|
||||||
lines := strings.Split(string(data), "\n")
|
// CPU информация из /proc/cpuinfo
|
||||||
for _, line := range lines {
|
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
||||||
if strings.HasPrefix(line, "MemTotal:") {
|
lines := strings.Split(string(data), "\n")
|
||||||
fields := strings.Fields(line)
|
var cpuModel string
|
||||||
if len(fields) >= 2 {
|
var cores, sockets int
|
||||||
if kb, err := strconv.ParseUint(fields[1], 10, 64); err == nil {
|
seenModels := make(map[string]bool)
|
||||||
result["memory_total_mb"] = int(kb / 1024)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1092,50 +1216,118 @@ func getNodeHardwareInfo(ctx context.Context) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getNodeResources(ctx context.Context) (map[string]any, error) {
|
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 (упрощенная версия)
|
// Пробуем получить данные через pvesh API
|
||||||
result["cpu_usage_percent"] = 0.0
|
if hostname, err := os.Hostname(); err == nil {
|
||||||
|
if _, err := exec.LookPath("pvesh"); err == nil {
|
||||||
// Memory usage
|
// Получаем ресурсы через pvesh
|
||||||
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
cmd := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/status", hostname), "--output-format", "json")
|
||||||
lines := strings.Split(string(data), "\n")
|
out, err := cmd.Output()
|
||||||
var total, free, buffers, cached uint64
|
if err == nil {
|
||||||
for _, line := range lines {
|
var statusData map[string]any
|
||||||
fields := strings.Fields(line)
|
if err := json.Unmarshal(out, &statusData); err == nil {
|
||||||
if len(fields) >= 2 {
|
// CPU usage
|
||||||
if val, err := strconv.ParseUint(fields[1], 10, 64); err == nil {
|
if cpu, ok := statusData["cpu"].(float64); ok {
|
||||||
switch fields[0] {
|
result["cpu_usage_percent"] = cpu * 100
|
||||||
case "MemTotal:":
|
}
|
||||||
total = val
|
|
||||||
case "MemFree:":
|
// Memory usage
|
||||||
free = val
|
if mem, ok := statusData["memory"].(float64); ok {
|
||||||
case "Buffers:":
|
result["memory_used_mb"] = int(mem)
|
||||||
buffers = val
|
}
|
||||||
case "Cached:":
|
|
||||||
cached = val
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
used := total - free - buffers - cached
|
|
||||||
result["memory_used_mb"] = int(used / 1024)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap
|
// Fallback: получаем данные из /proc если pvesh недоступен
|
||||||
result["swap_used_mb"] = 0
|
if result["cpu_usage_percent"].(float64) == 0.0 {
|
||||||
|
// CPU usage из /proc/stat
|
||||||
// Load average
|
if data, err := os.ReadFile("/proc/stat"); err == nil {
|
||||||
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
|
lines := strings.Split(string(data), "\n")
|
||||||
fields := strings.Fields(string(data))
|
if len(lines) > 0 {
|
||||||
if len(fields) >= 3 {
|
fields := strings.Fields(lines[0])
|
||||||
var loadavg []float64
|
if len(fields) >= 8 {
|
||||||
for i := 0; i < 3; i++ {
|
// Простой расчет CPU usage
|
||||||
if val, err := strconv.ParseFloat(fields[i], 64); err == nil {
|
var total, idle uint64
|
||||||
loadavg = append(loadavg, val)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result["loadavg"] = loadavg
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
var loadavg []float64
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user