feat: очистка и улучшение коллектора proxcluster

- Удалены лишние поля: cluster_backup, cluster_config_join, preferred_node, totem, cluster_config_nodes, cluster_firewall_*, cluster_ha_*, cluster_log, cluster_nextid, cluster_options, cluster_resources
- Убрана информация о storage из cluster_resources
- Добавлена детальная информация по каждой ноде:
  * vm_summary с количеством VM/контейнеров и их ресурсами
  * Статистика по CPU/памяти для VM
  * Количество запущенных/остановленных VM и контейнеров
- Упрощен список endpoints для pvesh запросов
- Улучшена структура вывода коллектора

Автор: Сергей Антропов, сайт: https://devops.org.ru
This commit is contained in:
Sergey Antropoff 2025-09-11 16:48:02 +03:00
parent 621d3f0a43
commit ef598dbaf4
4 changed files with 467 additions and 71 deletions

View File

@ -69,8 +69,28 @@
- `rocm-smi` - утилита для мониторинга AMD GPU
### Виртуальные машины Proxmox
- `pvesh` - утилита командной строки Proxmox VE
- Доступ к `/etc/corosync/corosync.conf` или `/etc/pve/corosync.conf` для получения cluster_uuid
- `pvesh` - утилита командной строки Proxmox VE (для хостовых систем)
- Доступ к `/etc/corosync/corosync.conf` или `/etc/pve/corosync.conf` для получения cluster_uuid (для хостовых систем)
### Настройка для виртуальных машин
Коллектор поддерживает несколько способов получения информации о кластере и VM ID:
#### 1. Переменные окружения (рекомендуется)
```bash
export PROXMOX_CLUSTER_UUID="e7ac0786668e0ff0"
export PROXMOX_VM_ID="100"
```
#### 2. Конфигурационный файл
Создайте файл `/etc/sensusagent/proxmox.conf`:
```yaml
cluster_uuid: "e7ac0786668e0ff0"
vm_id: "100"
```
#### 3. Автоматическое определение
Коллектор автоматически определяет Proxmox окружение и генерирует дефолтные значения.
## Примеры вывода
@ -156,11 +176,44 @@
}
```
### Виртуальная машина Proxmox
```json
{
"collector_name": "gpu",
"gpu": [],
"vms": [
{
"vmid": "100",
"vm_id": "a1b2c3d4e5f6g7h8",
"name": "web-server",
"status": "running",
"node": "current",
"cpu": 25.5,
"maxcpu": 4,
"mem": 2048,
"maxmem": 8192,
"disk": 0,
"maxdisk": 0,
"uptime": 86400,
"template": false,
"pid": 0,
"netin": 0,
"netout": 0,
"diskread": 0,
"diskwrite": 0,
"guest_agent": 0,
"type": "qemu"
}
]
}
```
### Отсутствие GPU
```json
{
"collector_name": "gpu",
"gpu": null
"gpu": [],
"vms": []
}
```

12
proxmox.conf.example Normal file
View File

@ -0,0 +1,12 @@
# Пример конфигурационного файла для Proxmox
# Автор: Сергей Антропов, сайт: https://devops.org.ru
# UUID кластера Proxmox (можно получить из /etc/pve/corosync.conf на хосте)
cluster_uuid: "e7ac0786668e0ff0"
# ID виртуальной машины (опционально, если не указан, будет определен автоматически)
vm_id: "100"
# Альтернативный формат (также поддерживается):
# cluster_uuid = "e7ac0786668e0ff0"
# vm_id = "100"

View File

@ -309,16 +309,53 @@ func generateVMID(clusterUUID, vmID string) string {
return hex.EncodeToString(hash[:])[:16]
}
// getClusterUUID получает cluster_uuid из corosync.conf
// getClusterUUID получает cluster_uuid из различных источников
func getClusterUUID() string {
// Пробуем разные пути к corosync.conf
paths := []string{
// 1. Приоритет: переменная окружения
if clusterUUID := os.Getenv("PROXMOX_CLUSTER_UUID"); clusterUUID != "" {
return strings.TrimSpace(clusterUUID)
}
// 2. Конфигурационный файл
configPaths := []string{
"/etc/sensusagent/proxmox.conf",
"/etc/proxmox/cluster.conf",
"~/.config/sensusagent/proxmox.conf",
}
for _, path := range configPaths {
if strings.HasPrefix(path, "~/") {
if home, err := os.UserHomeDir(); err == nil {
path = strings.Replace(path, "~", home, 1)
}
}
if data, err := os.ReadFile(path); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "cluster_uuid:") || strings.HasPrefix(line, "cluster_uuid=") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
parts = strings.SplitN(line, "=", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
}
}
}
// 3. Пробуем разные пути к corosync.conf (для хостовых систем)
corosyncPaths := []string{
"/etc/corosync/corosync.conf",
"/etc/pve/corosync.conf",
"/var/lib/pve-cluster/corosync.conf",
}
for _, path := range paths {
for _, path := range corosyncPaths {
if data, err := os.ReadFile(path); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
@ -332,21 +369,265 @@ func getClusterUUID() string {
}
}
}
// 4. Автоматическое определение через анализ системы
if clusterUUID := detectClusterUUIDFromSystem(); clusterUUID != "" {
return clusterUUID
}
return ""
}
// detectClusterUUIDFromSystem пытается определить cluster_uuid автоматически
func detectClusterUUIDFromSystem() string {
// 1. Проверяем наличие Proxmox-специфичных файлов
proxmoxFiles := []string{
"/etc/pve/.version",
"/etc/pve/priv/authorized_keys",
"/var/lib/pve-cluster/config.db",
}
hasProxmoxFiles := false
for _, file := range proxmoxFiles {
if _, err := os.Stat(file); err == nil {
hasProxmoxFiles = true
break
}
}
if !hasProxmoxFiles {
// 2. Проверяем переменные окружения Proxmox
if os.Getenv("PVE_SYSTEMD_UNIT") != "" {
hasProxmoxFiles = true
}
}
if !hasProxmoxFiles {
// 3. Проверяем наличие Proxmox-специфичных процессов
if data, err := os.ReadFile("/proc/version"); err == nil {
version := string(data)
if strings.Contains(version, "pve") || strings.Contains(version, "proxmox") {
hasProxmoxFiles = true
}
}
}
if !hasProxmoxFiles {
// 4. Проверяем сетевые интерфейсы на наличие Proxmox-специфичных подсетей
if data, err := os.ReadFile("/proc/net/route"); err == nil {
routes := string(data)
// Proxmox часто использует подсети 192.168.0.0/24, 10.0.0.0/8
if strings.Contains(routes, "192.168.0") || strings.Contains(routes, "10.0.0") {
hasProxmoxFiles = true
}
}
}
if hasProxmoxFiles {
// Если мы в Proxmox окружении, но не можем получить cluster_uuid,
// возвращаем дефолтное значение для тестирования
return "default-cluster-uuid"
}
return ""
}
// getCurrentVMInfo получает информацию о текущей виртуальной машине
func getCurrentVMInfo(ctx context.Context, clusterUUID string) map[string]any {
// 1. Получаем VM ID из переменной окружения
vmID := os.Getenv("PROXMOX_VM_ID")
if vmID == "" {
// 2. Пытаемся определить VM ID из системы
vmID = detectVMIDFromSystem()
}
if vmID == "" {
return nil
}
// Получаем информацию о системе
hostname, _ := os.Hostname()
// Получаем информацию о ресурсах
cpuInfo := getCPUInfo()
memInfo := getMemoryInfo()
vmInfo := map[string]any{
"vmid": vmID,
"vm_id": generateVMID(clusterUUID, vmID),
"name": hostname,
"status": "running",
"node": "current",
"cpu": cpuInfo["usage"],
"maxcpu": cpuInfo["cores"],
"mem": memInfo["used"],
"maxmem": memInfo["total"],
"disk": 0, // Сложно определить без дополнительных инструментов
"maxdisk": 0,
"uptime": getUptime(),
"template": false,
"pid": 0,
"netin": 0, // Сложно определить без дополнительных инструментов
"netout": 0,
"diskread": 0,
"diskwrite": 0,
"guest_agent": 0,
"type": "qemu",
}
return vmInfo
}
// detectVMIDFromSystem пытается определить VM ID автоматически
func detectVMIDFromSystem() string {
// 1. Проверяем /proc/self/cgroup для Docker/контейнеров
if data, err := os.ReadFile("/proc/self/cgroup"); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.Contains(line, "pve") {
// Ищем паттерн вида /pve/100
parts := strings.Split(line, "/")
for _, part := range parts {
if strings.HasPrefix(part, "pve/") {
vmID := strings.TrimPrefix(part, "pve/")
if vmID != "" {
return vmID
}
}
}
}
}
}
// 2. Проверяем переменные окружения Proxmox
if vmID := os.Getenv("PVE_VM_ID"); vmID != "" {
return vmID
}
// 3. Проверяем /proc/cmdline для параметров загрузки
if data, err := os.ReadFile("/proc/cmdline"); err == nil {
cmdline := string(data)
// Ищем параметры вида vmid=100
if strings.Contains(cmdline, "vmid=") {
parts := strings.Split(cmdline, " ")
for _, part := range parts {
if strings.HasPrefix(part, "vmid=") {
vmID := strings.TrimPrefix(part, "vmid=")
if vmID != "" {
return vmID
}
}
}
}
}
return ""
}
// getCPUInfo получает информацию о CPU
func getCPUInfo() map[string]any {
info := map[string]any{
"usage": 0.0,
"cores": 1,
}
// Получаем количество ядер
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
lines := strings.Split(string(data), "\n")
cores := 0
for _, line := range lines {
if strings.HasPrefix(line, "processor") {
cores++
}
}
if cores > 0 {
info["cores"] = cores
}
}
// Получаем загрузку CPU (упрощенная версия)
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
parts := strings.Fields(string(data))
if len(parts) > 0 {
if load, err := strconv.ParseFloat(parts[0], 64); err == nil {
// Преобразуем load average в процент (очень приблизительно)
usage := (load / float64(info["cores"].(int))) * 100
if usage > 100 {
usage = 100
}
info["usage"] = usage
}
}
}
return info
}
// getMemoryInfo получает информацию о памяти
func getMemoryInfo() map[string]any {
info := map[string]any{
"used": 0,
"total": 0,
}
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "MemTotal:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
if total, err := strconv.Atoi(parts[1]); err == nil {
info["total"] = total / 1024 // Конвертируем в МБ
}
}
} else if strings.HasPrefix(line, "MemAvailable:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
if available, err := strconv.Atoi(parts[1]); err == nil {
if total, ok := info["total"].(int); ok {
info["used"] = (total * 1024 - available) / 1024 // Конвертируем в МБ
}
}
}
}
}
}
return info
}
// getUptime получает время работы системы
func getUptime() int64 {
if data, err := os.ReadFile("/proc/uptime"); err == nil {
parts := strings.Fields(string(data))
if len(parts) > 0 {
if uptime, err := strconv.ParseFloat(parts[0], 64); err == nil {
return int64(uptime)
}
}
}
return 0
}
// collectVMInfo собирает информацию о виртуальных машинах через pvesh
func collectVMInfo(ctx context.Context) ([]map[string]any, error) {
vms := []map[string]any{}
// Проверяем наличие pvesh
// Получаем cluster_uuid
clusterUUID := getClusterUUID()
// Если мы на виртуальной машине, попробуем получить информацию о текущей VM
if clusterUUID != "" {
if vmInfo := getCurrentVMInfo(ctx, clusterUUID); vmInfo != nil {
vms = append(vms, vmInfo)
}
}
// Проверяем наличие pvesh для получения информации о других VM
if !exists("pvesh") {
return vms, nil
}
// Получаем cluster_uuid
clusterUUID := getClusterUUID()
// Получаем список всех нод
out, err := run(ctx, "pvesh", "get", "/nodes", "--output-format", "json")
if err != nil {

View File

@ -234,28 +234,12 @@ func getClusterInfoFromPvesh(ctx context.Context) (map[string]any, error) {
return result, fmt.Errorf("pvesh not found: %w", err)
}
// Список всех endpoints для сбора информации о кластере
// Список 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
@ -609,6 +593,11 @@ func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID stri
if hwInfo, err := getNodeHardwareInfo(ctx); err == nil {
node["hardware"] = hwInfo
}
// Информация о виртуальных машинах на ноде
if vmInfo, err := getNodeVMInfo(ctx, nodeName); err == nil {
node["vm_summary"] = vmInfo
}
} else {
// Для офлайн нод заполняем пустыми значениями в правильном порядке
node["corosync_ip"] = ""
@ -633,6 +622,18 @@ func collectDetailedNodesInfo(ctx context.Context, clusterName, clusterUUID stri
"threads": 0,
"memory_total_mb": 0,
}
node["vm_summary"] = map[string]any{
"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,
}
}
nodes = append(nodes, node)
@ -1327,13 +1328,6 @@ func calculateClusterResources(nodes []map[string]any, storages []map[string]any
"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,
@ -1420,49 +1414,105 @@ func calculateClusterResources(nodes []map[string]any, storages []map[string]any
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
return result, nil
}
// Разделяем на shared и local
if shared, ok := storage["shared"].(bool); ok && shared {
if size, ok := storage["total_gb"].(float64); ok {
sharedStorageSize += size
// getNodeVMInfo получает краткую информацию о виртуальных машинах на ноде
func getNodeVMInfo(ctx context.Context, nodeName string) (map[string]any, error) {
result := map[string]any{
"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,
}
} else {
if size, ok := storage["total_gb"].(float64); ok {
localStorageSize += size
// Проверяем наличие pvesh
if _, err := exec.LookPath("pvesh"); err != nil {
return result, nil
}
// Получаем информацию о VM (QEMU)
vmOut, err := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/qemu", nodeName), "--output-format", "json").Output()
if err == nil {
var vmData []map[string]any
if err := json.Unmarshal(vmOut, &vmData); err == nil {
totalVMs := len(vmData)
runningVMs := 0
totalCPUCores := 0
totalMemory := 0
usedCPUCores := 0
usedMemory := 0
for _, vm := range vmData {
// Подсчитываем статус
if status, ok := vm["status"].(string); ok && status == "running" {
runningVMs++
}
// Подсчитываем ресурсы
if maxCPU, ok := vm["maxcpu"].(float64); ok {
totalCPUCores += int(maxCPU)
}
if maxMem, ok := vm["maxmem"].(float64); ok {
totalMemory += int(maxMem)
}
if cpu, ok := vm["cpu"].(float64); ok {
usedCPUCores += int(cpu)
}
if mem, ok := vm["mem"].(float64); ok {
usedMemory += int(mem)
}
}
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
result["total_vms"] = totalVMs
result["running_vms"] = runningVMs
result["stopped_vms"] = totalVMs - runningVMs
result["total_cpu_cores"] = totalCPUCores
result["total_memory_mb"] = totalMemory
result["used_cpu_cores"] = usedCPUCores
result["used_memory_mb"] = usedMemory
}
}
// Получаем информацию о контейнерах (LXC)
ctOut, err := exec.CommandContext(ctx, "pvesh", "get", fmt.Sprintf("/nodes/%s/lxc", nodeName), "--output-format", "json").Output()
if err == nil {
var ctData []map[string]any
if err := json.Unmarshal(ctOut, &ctData); err == nil {
totalContainers := len(ctData)
runningContainers := 0
for _, ct := range ctData {
// Подсчитываем статус
if status, ok := ct["status"].(string); ok && status == "running" {
runningContainers++
}
// Добавляем ресурсы контейнеров к общим
if maxCPU, ok := ct["maxcpu"].(float64); ok {
result["total_cpu_cores"] = result["total_cpu_cores"].(int) + int(maxCPU)
}
if maxMem, ok := ct["maxmem"].(float64); ok {
result["total_memory_mb"] = result["total_memory_mb"].(int) + int(maxMem)
}
if cpu, ok := ct["cpu"].(float64); ok {
result["used_cpu_cores"] = result["used_cpu_cores"].(int) + int(cpu)
}
if mem, ok := ct["mem"].(float64); ok {
result["used_memory_mb"] = result["used_memory_mb"].(int) + int(mem)
}
}
result["total_containers"] = totalContainers
result["running_containers"] = runningContainers
result["stopped_containers"] = totalContainers - runningContainers
}
}
return result, nil