diff --git a/Makefile b/Makefile index 7f53be8..faa543d 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,8 @@ collectors: CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/docker ./src/collectors/docker && \ CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/gpu ./src/collectors/gpu && \ CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \ - CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster"; \ + CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \ + CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode"; \ fi @# Убедимся, что скрипты исполняемые @chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true @@ -85,7 +86,8 @@ collectors-linux: CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/docker ./src/collectors/docker && \ CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/gpu ./src/collectors/gpu && \ CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \ - CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster" + CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \ + CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode" collectors-windows: # Кросс-сборка коллекторов для Windows diff --git a/bin/agent/config.yaml b/bin/agent/config.yaml index eadcb10..5352a7d 100644 --- a/bin/agent/config.yaml +++ b/bin/agent/config.yaml @@ -5,7 +5,7 @@ mode: auto # stdout | kafka | auto log_level: info kafka: - enabled: false + enabled: true brokers: ["10.29.91.4:9092"] topic: "sensus.metrics" client_id: "sensusagent" @@ -23,7 +23,7 @@ kafka: collectors: system: - enabled: true + enabled: false type: exec key: system interval: "3600s" @@ -55,7 +55,7 @@ collectors: exec: "./collectors/sample.sh" platforms: [darwin, linux] hba: - enabled: true + enabled: false type: exec key: hba interval: "3600s" @@ -63,7 +63,7 @@ collectors: exec: "./collectors/hba" platforms: [linux] sensors: - enabled: true + enabled: false type: exec key: sensors interval: "3600s" @@ -79,7 +79,7 @@ collectors: exec: "./collectors/docker" platforms: [darwin, linux] gpu: - enabled: true + enabled: false type: exec key: gpu interval: "3600s" @@ -94,14 +94,21 @@ collectors: timeout: "60s" exec: "./collectors/kubernetes" platforms: [linux] + proxnode: + enabled: true + type: exec + key: proxnode + interval: "1800s" + timeout: "30s" + exec: "./collectors/proxnode" + platforms: [linux] proxcluster: enabled: true type: exec key: proxcluster interval: "1800s" - timeout: "30s" + timeout: "600s" exec: "./collectors/proxcluster" platforms: [linux] - diff --git a/prox.json b/prox.json deleted file mode 100644 index abc3b73..0000000 --- a/prox.json +++ /dev/null @@ -1,1826 +0,0 @@ -{ - "collector_name": "proxcluster", - "nodes": [ - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "{", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"nodename\": \"pnode02\",", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"version\": 283,", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"cluster\": { \"name\": \"CISM\", \"version\": 43, \"nodes\": 32, \"quorate\": 1 },", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"nodelist\": {", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode28\": { \"id\": 28, \"online\": 1, \"ip\": \"10.14.253.38\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode20\": { \"id\": 17, \"online\": 1, \"ip\": \"10.14.253.30\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode37\": { \"id\": 6, \"online\": 1, \"ip\": \"10.14.253.47\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode23\": { \"id\": 23, \"online\": 1, \"ip\": \"10.14.253.33\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode14\": { \"id\": 12, \"online\": 1, \"ip\": \"10.14.253.24\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode29\": { \"id\": 29, \"online\": 1, \"ip\": \"10.14.253.39\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode18\": { \"id\": 18, \"online\": 1, \"ip\": \"10.14.253.28\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode08\": { \"id\": 7, \"online\": 1, \"ip\": \"10.14.253.18\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode24\": { \"id\": 24, \"online\": 1, \"ip\": \"10.14.253.34\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode15\": { \"id\": 13, \"online\": 1, \"ip\": \"10.14.253.25\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode03\": { \"id\": 2, \"online\": 1, \"ip\": \"10.14.253.13\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode12\": { \"id\": 1, \"online\": 1, \"ip\": \"10.14.253.22\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode30\": { \"id\": 30, \"online\": 1, \"ip\": \"10.14.253.40\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode17\": { \"id\": 19, \"online\": 1, \"ip\": \"10.14.253.27\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode10\": { \"id\": 8, \"online\": 1, \"ip\": \"10.14.253.20\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode25\": { \"id\": 25, \"online\": 1, \"ip\": \"10.14.253.35\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode02\": { \"id\": 14, \"online\": 1, \"ip\": \"10.14.253.12\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode04\": { \"id\": 3, \"online\": 1, \"ip\": \"10.14.253.14\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode31\": { \"id\": 31, \"online\": 1, \"ip\": \"10.14.253.41\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode39\": { \"id\": 20, \"online\": 1, \"ip\": \"10.14.253.49\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode09\": { \"id\": 9, \"online\": 1, \"ip\": \"10.14.253.19\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode26\": { \"id\": 26, \"online\": 1, \"ip\": \"10.14.253.36\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode40\": { \"id\": 15, \"online\": 1, \"ip\": \"10.14.253.50\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode05\": { \"id\": 4, \"online\": 1, \"ip\": \"10.14.253.15\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode32\": { \"id\": 32, \"online\": 1, \"ip\": \"10.14.253.42\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode21\": { \"id\": 21, \"online\": 1, \"ip\": \"10.14.253.31\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode11\": { \"id\": 10, \"online\": 1, \"ip\": \"10.14.253.21\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode27\": { \"id\": 27, \"online\": 1, \"ip\": \"10.14.253.37\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode19\": { \"id\": 16, \"online\": 1, \"ip\": \"10.14.253.29\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode06\": { \"id\": 5, \"online\": 1, \"ip\": \"10.14.253.16\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode22\": { \"id\": 22, \"online\": 1, \"ip\": \"10.14.253.32\"},", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "\"pnode13\": { \"id\": 11, \"online\": 1, \"ip\": \"10.14.253.23\"}", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "}", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - }, - { - "cluster_id": "03edb3b277800f1e", - "cluster_uuid": "", - "corosync_ip": "", - "hardware": { - "cpu_cores": 0, - "cpu_model": "unknown", - "memory_total_mb": 0, - "sockets": 0, - "threads": 0 - }, - "machine_id": "", - "name": "}", - "node_id": 0, - "node_uid": "19e89348f2a9d5f3", - "online": false, - "os": { - "kernel": "unknown", - "pve_version": "unknown", - "uptime_sec": 0 - }, - "product_uuid": "", - "real_ips": [], - "resources": { - "cpu_usage_percent": 0, - "loadavg": [ - 0, - 0, - 0 - ], - "memory_used_mb": 0, - "swap_used_mb": 0 - }, - "vm_summary": { - "running_containers": 0, - "running_vms": 0, - "stopped_containers": 0, - "stopped_vms": 0, - "total_containers": 0, - "total_cpu_cores": 0, - "total_memory_mb": 0, - "total_vms": 0, - "used_cpu_cores": 0, - "used_memory_mb": 0 - } - } - ], - "summary": { - "cluster_id": "03edb3b277800f1e", - "cluster_resources": { - "cpu": { - "online_cores": 0, - "total_cores": 0 - }, - "memory": { - "total_mb": 0, - "used_mb": 0 - }, - "nodes": { - "online": 0, - "total": 39 - } - }, - "cluster_uuid": "", - "corosync": {}, - "name": "CISM", - "quorum": { - "expected_votes": 0, - "members": 0, - "quorate": false, - "total_votes": 0 - }, - "version": "7.4-16" - } -} diff --git a/runner/inventory.ini b/runner/inventory.ini index f3935a6..b0e303b 100644 --- a/runner/inventory.ini +++ b/runner/inventory.ini @@ -5,4 +5,4 @@ #videotest7 ansible_host=10.13.37.186 ansible_user=devops #videotest8 ansible_host=10.13.37.187 ansible_user=devops pnode02 ansible_host=10.14.253.12 ansible_user=devops -dbrain01 ansible_host=10.14.246.75 ansible_user=devops +#dbrain01 ansible_host=10.14.246.75 ansible_user=devops diff --git a/src/collectors/docker/main.go b/src/collectors/docker/main.go index ba13fe1..70f5d62 100644 --- a/src/collectors/docker/main.go +++ b/src/collectors/docker/main.go @@ -15,6 +15,9 @@ import ( // collectDocker реализуется платформенно. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -25,6 +28,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/gpu/main.go b/src/collectors/gpu/main.go index fb317dc..688d4d8 100644 --- a/src/collectors/gpu/main.go +++ b/src/collectors/gpu/main.go @@ -16,6 +16,9 @@ import ( // collectGPU реализуется платформенно. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -27,6 +30,14 @@ func main() { fmt.Println("{\"gpu\":[]}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + // Если ключ gpu отсутствует, нормализуем к пустому массиву if _, ok := data["gpu"]; !ok { data["gpu"] = []any{} diff --git a/src/collectors/hba/main.go b/src/collectors/hba/main.go index 48e8c79..099f0fc 100644 --- a/src/collectors/hba/main.go +++ b/src/collectors/hba/main.go @@ -16,6 +16,9 @@ import ( // collectHBA реализуется в файлах с билд-тегами под конкретные ОС. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -26,6 +29,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/kubernetes/main.go b/src/collectors/kubernetes/main.go index 05e4ec6..5da2d4d 100644 --- a/src/collectors/kubernetes/main.go +++ b/src/collectors/kubernetes/main.go @@ -16,6 +16,9 @@ import ( // collectKubernetes реализуется платформенно. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 12*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -26,6 +29,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/macos/main.go b/src/collectors/macos/main.go index 86e2f61..f5f92dd 100644 --- a/src/collectors/macos/main.go +++ b/src/collectors/macos/main.go @@ -15,6 +15,9 @@ import ( // collectInfo реализуется в файлах с билд-тегами. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -24,6 +27,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/proxcluster/main.go b/src/collectors/proxcluster/main.go index aeea340..7c1475b 100644 --- a/src/collectors/proxcluster/main.go +++ b/src/collectors/proxcluster/main.go @@ -13,6 +13,9 @@ import ( ) func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -23,6 +26,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/proxcluster/proxcluster_linux.go b/src/collectors/proxcluster/proxcluster_linux.go index 7ae9589..b00e85e 100644 --- a/src/collectors/proxcluster/proxcluster_linux.go +++ b/src/collectors/proxcluster/proxcluster_linux.go @@ -13,769 +13,278 @@ import ( "fmt" "os" "os/exec" - "regexp" "strconv" "strings" + "sync" ) +// Структуры для работы с API Proxmox +type ClusterInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Nodes int `json:"nodes"` + Quorate int `json:"quorate"` + Type string `json:"type"` + Version int `json:"version"` +} + +type ClusterStatus struct { + ID string `json:"id"` + IP string `json:"ip"` + Level string `json:"level"` + Local int `json:"local"` + Name string `json:"name"` + NodeID int `json:"nodeid"` + Online int `json:"online"` + Type string `json:"type"` +} + +// Итоговая структура +type ClusterData struct { + CollectorName string `json:"collector_name"` + Summary map[string]any `json:"summary"` + Nodes []map[string]any `json:"nodes"` +} + // 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", - } - - // Получаем информацию о кластере через pvesh - clusterInfo, err := getClusterInfo(ctx) + // Получаем статус кластера для получения информации о кластере + clusterStatRaw, err := runPvesh("/cluster/status") if err != nil { - clusterInfo = map[string]interface{}{ - "cluster_id": "unknown", - "cluster_uuid": "unknown", - "name": "unknown", - "version": "unknown", - } - } - - // Получаем информацию о нодах - nodesInfo, err := getNodesInfo(ctx) - if err != nil { - nodesInfo = []map[string]interface{}{} - } - - // Создаем summary - summary := map[string]interface{}{ - "cluster_id": clusterInfo["cluster_id"], - "cluster_uuid": clusterInfo["cluster_uuid"], - "name": clusterInfo["name"], - "version": clusterInfo["version"], - } - - // Агрегируем ресурсы кластера - 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, - } - } - summary["quorum"] = quorumInfo - - // Получаем информацию о corosync - corosyncInfo, err := getCorosyncInfo(ctx) - if err != nil { - corosyncInfo = map[string]interface{}{} - } - summary["corosync"] = corosyncInfo - - result["summary"] = summary - result["nodes"] = nodesInfo - - return result, nil -} - -// getClusterInfo получает основную информацию о кластере -func getClusterInfo(ctx context.Context) (map[string]interface{}, error) { - result := map[string]interface{}{ - "cluster_id": "unknown", + debugLog("Failed to get cluster status: %v", err) + // Возвращаем базовую структуру при ошибке + return map[string]interface{}{ + "collector_name": "proxcluster", + "cluster": map[string]interface{}{ + "cluster_name": "unknown", + "cluster_uid": "unknown", "cluster_uuid": "unknown", - "name": "unknown", - "version": "unknown", + "nodes": map[string]int{ + "total": 0, + "online": 0, + }, + "quorum": map[string]any{}, + }, + "nodes": []map[string]any{}, + }, nil } - // Читаем информацию из corosync.conf - clusterName, clusterUUID, err := parseCorosyncConf("/etc/pve/corosync.conf") - if err == nil { - result["name"] = clusterName - result["cluster_uuid"] = clusterUUID - result["cluster_id"] = generateClusterID(clusterUUID) + var clusterStatusData []map[string]interface{} + if err := json.Unmarshal(clusterStatRaw, &clusterStatusData); err != nil { + debugLog("Failed to parse cluster status: %v", err) + return nil, err } - // Получаем версию через 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 + // Извлекаем информацию о кластере (первый элемент) + var clusterInfo ClusterInfo + if len(clusterStatusData) > 0 && clusterStatusData[0]["type"] == "cluster" { + clusterInfo.ID = getString(clusterStatusData[0], "id") + clusterInfo.Name = getString(clusterStatusData[0], "name") + clusterInfo.Nodes = getInt(clusterStatusData[0], "nodes") + clusterInfo.Quorate = getInt(clusterStatusData[0], "quorate") + clusterInfo.Type = getString(clusterStatusData[0], "type") + clusterInfo.Version = getInt(clusterStatusData[0], "version") } + + // Извлекаем информацию о всех нодах + var clusterStat []ClusterStatus + for i := 1; i < len(clusterStatusData); i++ { + if clusterStatusData[i]["type"] == "node" { + node := ClusterStatus{ + ID: getString(clusterStatusData[i], "id"), + IP: getString(clusterStatusData[i], "ip"), + Level: getString(clusterStatusData[i], "level"), + Local: getInt(clusterStatusData[i], "local"), + Name: getString(clusterStatusData[i], "name"), + NodeID: getInt(clusterStatusData[i], "nodeid"), + Online: getInt(clusterStatusData[i], "online"), + Type: getString(clusterStatusData[i], "type"), } + clusterStat = append(clusterStat, node) } } - return result, nil -} + debugLog("Cluster info: %+v", clusterInfo) + debugLog("Cluster nodes: %+v", clusterStat) -// parseCorosyncConf парсит corosync.conf и извлекает cluster_name и cluster_uuid -func parseCorosyncConf(path string) (string, string, error) { - data, err := os.ReadFile(path) - if err != nil { - return "", "", err - } - - var clusterName, clusterUUID string - lines := strings.Split(string(data), "\n") - - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "#") || line == "" { - continue - } - - if strings.HasPrefix(line, "cluster_name:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - clusterName = strings.TrimSpace(parts[1]) - } - } - - if strings.HasPrefix(line, "cluster_uuid:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - clusterUUID = strings.TrimSpace(parts[1]) - } - } - } - - if clusterName == "" { - // Если cluster_name не найден, пробуем получить из /etc/pve/.members - if members, err := getClusterMembers(); err == nil && len(members) > 0 { - // Используем имя первой ноды как основу для имени кластера - clusterName = "cluster-" + members[0] + // Получаем cluster_uuid из corosync + clusterUUID := runSimple("grep", "cluster_name", "/etc/pve/corosync.conf") + if clusterUUID == "unknown" || clusterUUID == "" { + clusterUUID = clusterInfo.Name // fallback на имя из API } else { - clusterName = "unknown-cluster" - } - } - - return clusterName, clusterUUID, nil -} - -// 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{}{} - - // Сначала проверяем онлайн статус ноды - 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 - } - - // Если данные о ресурсах пустые, пробуем альтернативные методы - cpuUsage := 0.0 - if val, ok := resources["cpu_usage_percent"].(float64); ok { - cpuUsage = val - } else if val, ok := resources["cpu_usage_percent"].(int); ok { - cpuUsage = float64(val) - } - - memoryUsed := 0 - if val, ok := resources["memory_used_mb"].(int); ok { - memoryUsed = val - } else if val, ok := resources["memory_used_mb"].(float64); ok { - memoryUsed = int(val) - } - - if cpuUsage == 0 && memoryUsed == 0 { - if altResources, err := getAlternativeResources(ctx, nodeName); err == nil { - for k, v := range altResources { - resources[k] = v - } + // Извлекаем только значение после двоеточия + parts := strings.SplitN(clusterUUID, ":", 2) + if len(parts) > 1 { + clusterUUID = strings.TrimSpace(parts[1]) } } - 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) // конвертируем в МБ - } + // Генерируем cluster_id через SHA + clusterID := generateClusterID(clusterUUID) - // Если данные о железе пустые, пробуем альтернативные методы - cpuCores := 0 - if val, ok := hardware["cpu_cores"].(int); ok { - cpuCores = val - } else if val, ok := hardware["cpu_cores"].(float64); ok { - cpuCores = int(val) - } - - memoryTotal := 0 - if val, ok := hardware["memory_total_mb"].(int); ok { - memoryTotal = val - } else if val, ok := hardware["memory_total_mb"].(float64); ok { - memoryTotal = int(val) - } - - if cpuCores == 0 && memoryTotal == 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 - } + // Обрабатываем все ноды кластера асинхронно + nodes := make([]map[string]any, len(clusterStat)) + var wg sync.WaitGroup + var mu sync.Mutex + var totalStats = struct { + totalCpuCores int + totalSockets int + totalMemoryMb int64 + totalVms int + totalContainers int + totalTemplateVms int + totalStoppedVms int + totalStoppedContainers int + totalRunningVms int + totalRunningContainers int + }{} - // Получаем corosync IP (пробуем через pvesh) - if corosyncIP, err := getCorosyncIP(ctx, nodeName); err == nil { - result["corosync_ip"] = corosyncIP - } + for i, clusterNode := range clusterStat { + wg.Add(1) + go func(index int, node ClusterStatus) { + defer wg.Done() + + debugLog("Processing node: %s", node.Name) - // Получаем реальные IP адреса - if realIPs, err := getRealIPs(ctx, nodeName); err == nil { - result["real_ips"] = realIPs - } + // Генерируем node_uid через SHA + nodeUID := generateNodeUID(clusterInfo.ID, strconv.Itoa(node.NodeID)) - // Получаем 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 { - totalCores := 0 - if val, ok := result["total_cpu_cores"].(int); ok { - totalCores = val - } - result["total_cpu_cores"] = totalCores + int(cpus) - if status, ok := vm["status"].(string); ok && status == "running" { - usedCores := 0 - if val, ok := result["used_cpu_cores"].(int); ok { - usedCores = val - } - result["used_cpu_cores"] = usedCores + int(cpus) - } - } - if mem, ok := vm["maxmem"].(float64); ok { - totalMem := 0 - if val, ok := result["total_memory_mb"].(int); ok { - totalMem = val - } - result["total_memory_mb"] = totalMem + int(mem/1024/1024) - if status, ok := vm["status"].(string); ok && status == "running" { - usedMem := 0 - if val, ok := result["used_memory_mb"].(int); ok { - usedMem = val - } - result["used_memory_mb"] = usedMem + int(mem/1024/1024) - } - } - } - } - } - - // Получаем контейнеры с детальной информацией о ресурсах - cmd = exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/lxc", "--output-format", "json") - out, err = cmd.Output() - if err == nil { - 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 { - totalCores := 0 - if val, ok := result["total_cpu_cores"].(int); ok { - totalCores = val - } - result["total_cpu_cores"] = totalCores + int(cpus) - if status, ok := container["status"].(string); ok && status == "running" { - usedCores := 0 - if val, ok := result["used_cpu_cores"].(int); ok { - usedCores = val - } - result["used_cpu_cores"] = usedCores + int(cpus) - } - } - if mem, ok := container["maxmem"].(float64); ok { - totalMem := 0 - if val, ok := result["total_memory_mb"].(int); ok { - totalMem = val - } - result["total_memory_mb"] = totalMem + int(mem/1024/1024) - if status, ok := container["status"].(string); ok && status == "running" { - usedMem := 0 - if val, ok := result["used_memory_mb"].(int); ok { - usedMem = val - } - result["used_memory_mb"] = usedMem + int(mem/1024/1024) - } - } - } - } - } - - return result, nil -} - -// 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, - }, - } - - totalCores := 0 - onlineCores := 0 - totalMemory := 0 - usedMemory := 0 - onlineNodes := 0 - - for _, node := range nodes { - if online, ok := node["online"].(bool); ok && online { - onlineNodes++ - } - - // Агрегируем CPU и память (упрощенно) - if hardware, ok := node["hardware"].(map[string]interface{}); ok { - cores := 0 - if val, ok := hardware["cpu_cores"].(int); ok { - cores = val - } else if val, ok := hardware["cpu_cores"].(float64); ok { - cores = int(val) - } - if cores > 0 { - totalCores += cores - if online, ok := node["online"].(bool); ok && online { - onlineCores += cores - } + // Получаем corosync IP из конфигурации + corosyncIPs := getCorosyncIP(node.Name) + + // Получаем информацию о ноде (CPU, память, VM) + nodeInfo := getNodeInfo(node.Name) + + // Создаем данные ноды, объединяя информацию из cluster и corosync + nodeData := map[string]any{ + "node_name": node.Name, + "node_id": node.NodeID, + "node_uid": nodeUID, + "online": node.Online == 1, + "type": node.Type, + "version": runSimple("pveversion"), + "corosync_ip": corosyncIPs, + "real_ip": getRealIPs(), } - memory := 0 - if val, ok := hardware["memory_total_mb"].(int); ok { - memory = val - } else if val, ok := hardware["memory_total_mb"].(float64); ok { - memory = int(val) - } - if memory > 0 { - totalMemory += memory - } - } - - if resources, ok := node["resources"].(map[string]interface{}); ok { - used := 0 - if val, ok := resources["memory_used_mb"].(int); ok { - used = val - } else if val, ok := resources["memory_used_mb"].(float64); ok { - used = int(val) - } - if used > 0 { - usedMemory += used - } - } - } - - 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 -} - -// getQuorumInfo получает информацию о кворуме -func getQuorumInfo(ctx context.Context) (map[string]interface{}, error) { - result := map[string]interface{}{ - "quorate": false, - "members": 0, - "total_votes": 0, - "expected_votes": 0, - } - - // Пробуем corosync-quorumtool - if _, err := exec.LookPath("corosync-quorumtool"); err == nil { - cmd := exec.CommandContext(ctx, "corosync-quorumtool", "-s") - 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, "Quorate:") { - result["quorate"] = strings.Contains(line, "Yes") - } - if strings.HasPrefix(line, "Nodes:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - // Простой парсинг числа - if count := parseNumber(parts[1]); count > 0 { - result["members"] = count - } + // Добавляем информацию о CPU, памяти и VM + if nodeInfo != nil { + nodeData["cpu_model"] = nodeInfo["cpu_model"] + nodeData["cpu_cores"] = nodeInfo["cpu_cores"] + nodeData["sockets"] = nodeInfo["sockets"] + nodeData["threads"] = nodeInfo["threads"] + nodeData["total_memory_mb"] = nodeInfo["total_memory_mb"] + nodeData["vm_summary"] = nodeInfo["vm_summary"] + nodeData["loadavg"] = nodeInfo["loadavg"] + + // Суммируем статистику для кластера + mu.Lock() + if cpuCores, ok := nodeInfo["cpu_cores"].(int); ok { + totalStats.totalCpuCores += cpuCores + } + if sockets, ok := nodeInfo["sockets"].(int); ok { + totalStats.totalSockets += sockets + } + if memoryMb, ok := nodeInfo["total_memory_mb"].(int64); ok { + totalStats.totalMemoryMb += memoryMb + } + if vmSummary, ok := nodeInfo["vm_summary"].(map[string]any); ok { + if totalVms, ok := vmSummary["total_vms"].(int); ok { + totalStats.totalVms += totalVms + } + if totalContainers, ok := vmSummary["total_containers"].(int); ok { + totalStats.totalContainers += totalContainers + } + if templateVms, ok := vmSummary["template_vms"].(int); ok { + totalStats.totalTemplateVms += templateVms + } + if stoppedVms, ok := vmSummary["stopped_vms"].(int); ok { + totalStats.totalStoppedVms += stoppedVms + } + if stoppedContainers, ok := vmSummary["stopped_containers"].(int); ok { + totalStats.totalStoppedContainers += stoppedContainers + } + if runningVms, ok := vmSummary["running_vms"].(int); ok { + totalStats.totalRunningVms += runningVms + } + if runningContainers, ok := vmSummary["running_containers"].(int); ok { + totalStats.totalRunningContainers += runningContainers } } + mu.Unlock() } - } + + mu.Lock() + nodes[index] = nodeData + mu.Unlock() + }(i, clusterNode) } - - return result, nil -} - -// getCorosyncInfo получает информацию о corosync -func getCorosyncInfo(ctx context.Context) (map[string]interface{}, error) { - result := map[string]interface{}{} - // Пробуем получить информацию через corosync-quorumtool - if _, err := exec.LookPath("corosync-quorumtool"); err == nil { - cmd := exec.CommandContext(ctx, "corosync-quorumtool", "-s") - out, err := cmd.Output() - if err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "Quorum provider:") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - result["quorum_provider"] = strings.TrimSpace(parts[1]) - } - } - } - } + wg.Wait() + + // Создаем cluster с информацией о кластере + cluster := map[string]any{ + "cluster_name": clusterInfo.Name, + "cluster_uid": clusterID, + "cluster_uuid": clusterUUID, + "nodes": map[string]int{ + "online": countOnline(clusterStat), + "total": len(clusterStat), + }, + "quorum": getQuorumInfo(), + "total_cpu_cores": totalStats.totalCpuCores, + "total_sockets": totalStats.totalSockets, + "total_threads": totalStats.totalSockets * totalStats.totalCpuCores, + "total_memory_mb": totalStats.totalMemoryMb, + "total_vms": totalStats.totalVms, + "total_containers": totalStats.totalContainers, + "total_template_vms": totalStats.totalTemplateVms, + "total_stopped_vms": totalStats.totalStoppedVms, + "total_stopped_containers": totalStats.totalStoppedContainers, + "total_running_vms": totalStats.totalRunningVms, + "total_running_containers": totalStats.totalRunningContainers, } - return result, nil + // Возвращаем структуру в новом формате + resultMap := map[string]interface{}{ + "collector_name": "proxcluster", + "cluster": cluster, + "nodes": nodes, + } + + debugLog("Final result: %+v", resultMap) + return resultMap, nil } +// runPvesh запускает pvesh get и возвращает JSON +func runPvesh(path string) ([]byte, error) { + cmd := exec.Command("pvesh", "get", path, "--output-format", "json") + return cmd.Output() +} -// getClusterMembers читает список нод из /etc/pve/.members -func getClusterMembers() ([]string, error) { - data, err := os.ReadFile("/etc/pve/.members") +// countOnline считает количество онлайн нод +func countOnline(stat []ClusterStatus) int { + c := 0 + for _, n := range stat { + if n.Online == 1 { + c++ + } + } + return c +} + +// runSimple выполняет простую команду и возвращает результат +func runSimple(cmd string, args ...string) string { + out, err := exec.Command(cmd, args...).Output() if err != nil { - debugLog("Failed to read /etc/pve/.members: %v", err) - return nil, err + debugLog("Failed to run command %s %v: %v", cmd, args, err) + return "unknown" } - - debugLog("Raw .members data: %s", string(data)) - - // Пробуем парсить как JSON объект - var membersData map[string]interface{} - if err := json.Unmarshal(data, &membersData); err == nil { - debugLog("Parsed .members as JSON object") - // Если это JSON объект, извлекаем имена нод из nodelist - if nodelist, ok := membersData["nodelist"].(map[string]interface{}); ok { - var members []string - for nodeName := range nodelist { - members = append(members, nodeName) - debugLog("Found node in nodelist: %s", nodeName) - } - return members, nil - } - } - - // Пробуем парсить как JSON массив - var members []string - if err := json.Unmarshal(data, &members); err == nil { - debugLog("Parsed .members as JSON array with %d members", len(members)) - return members, nil - } - - debugLog("Failed to parse as JSON, trying line by line") - // Если не JSON, пробуем построчно - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line != "" { - members = append(members, line) - } - } - - debugLog("Parsed %d members from lines", len(members)) - return members, nil + return strings.TrimSpace(string(out)) } -// 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) - - // Пробуем парсить как 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 для текстового формата - content := string(data) - 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 { @@ -793,272 +302,502 @@ func generateNodeUID(clusterUUID, nodeID string) string { 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 { - var statusData map[string]interface{} - if err := json.Unmarshal(out, &statusData); err == nil { - if ip, ok := statusData["ip"].(string); ok { - return ip, nil +// getString извлекает строку из map[string]interface{} +func getString(data map[string]interface{}, key string) string { + if val, ok := data[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +// getInt извлекает int из map[string]interface{} +func getInt(data map[string]interface{}, key string) int { + if val, ok := data[key]; ok { + switch v := val.(type) { + case int: + return v + case float64: + return int(v) + case string: + if i, err := strconv.Atoi(v); err == nil { + return i } } } - return "", fmt.Errorf("corosync IP not found") + return 0 } -// getRealIPs получает реальные IP адреса ноды (исключая corosync IP) -func getRealIPs(ctx context.Context, nodeName string) ([]string, error) { - var realIPs []string +// getInt64 извлекает int64 из map[string]interface{} +func getInt64(data map[string]interface{}, key string) int64 { + if val, ok := data[key]; ok { + switch v := val.(type) { + case int: + return int64(v) + case int64: + return v + case float64: + return int64(v) + case string: + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + } + } + return 0 +} + +// getFloat64 извлекает float64 из map[string]interface{} +func getFloat64(data map[string]interface{}, key string) float64 { + if val, ok := data[key]; ok { + switch v := val.(type) { + case float64: + return v + case int: + return float64(v) + case int64: + return float64(v) + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + } + } + return 0.0 +} + +// getRealIPs получает реальные IP адреса интерфейсов (исключая corosync IP) +func getRealIPs() []string { + raw := runSimple("ip", "addr", "show") + lines := strings.Split(raw, "\n") + var ips []string - // Пробуем получить через 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) + // Получаем corosync IP для исключения + corosyncIPs := getCorosyncIPs() + + for _, line := range lines { + line = strings.TrimSpace(line) + 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] // убираем /24, /25 и т.д. + + // Проверяем, не является ли это corosync IP + isCorosyncIP := false + for _, corosyncIP := range corosyncIPs { + if ip == corosyncIP { + isCorosyncIP = true + break } } + + if !isCorosyncIP { + ips = append(ips, ip) + } + } + } + } + return ips +} + +// getCorosyncIPs получает все corosync IP адреса +func getCorosyncIPs() []string { + var ips []string + + // Читаем corosync.conf + raw := runSimple("cat", "/etc/pve/corosync.conf") + if raw == "unknown" { + return ips + } + + lines := strings.Split(raw, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + if strings.HasPrefix(line, "ring0_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ip := strings.TrimSpace(parts[1]) + ips = append(ips, ip) + } + } else if strings.HasPrefix(line, "ring1_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ip := strings.TrimSpace(parts[1]) + ips = append(ips, ip) } } } - return realIPs, nil + return ips } -// getMachineID получает machine ID -func getMachineID() (string, error) { - data, err := os.ReadFile("/etc/machine-id") +// getNodeInfo получает информацию о ноде (CPU, память, VM) +func getNodeInfo(nodeName string) map[string]any { + // Получаем статус ноды + nodeStatRaw, err := runPvesh("/nodes/" + nodeName + "/status") if err != nil { - return "", err - } - return strings.TrimSpace(string(data)), nil -} - -// 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 { - debugLog("Failed to get nodes list: %v", err) - return false, err + debugLog("Failed to get node status for %s: %v", nodeName, err) + return nil } - debugLog("Raw pvesh nodes output: %s", string(out)) - - var nodes []map[string]interface{} - if err := json.Unmarshal(out, &nodes); err != nil { - debugLog("Failed to parse nodes JSON: %v", err) - return false, err + var nodeStatusData map[string]interface{} + if err := json.Unmarshal(nodeStatRaw, &nodeStatusData); err != nil { + debugLog("Failed to parse node status for %s: %v", nodeName, err) + return nil } - debugLog("Found %d nodes in pvesh output", len(nodes)) - - // Ищем нашу ноду в списке - for _, node := range nodes { - if name, ok := node["node"].(string); ok && name == nodeName { - debugLog("Found node %s in pvesh output", nodeName) - // Проверяем разные варианты статуса - if status, ok := node["status"].(string); ok { - debugLog("Node %s status (string): %s", nodeName, status) - return status == "online", nil - } - // Проверяем числовой статус (1 = online, 0 = offline) - if status, ok := node["status"].(float64); ok { - debugLog("Node %s status (float64): %f", nodeName, status) - return status == 1, nil - } - // Проверяем булевый статус - if status, ok := node["status"].(bool); ok { - debugLog("Node %s status (bool): %t", nodeName, status) - return status, nil - } - debugLog("Node %s found but status field is unknown type", nodeName) - } - } - - debugLog("Node %s not found in pvesh output", nodeName) - return false, fmt.Errorf("node %s not found", nodeName) -} - -// 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}, - } - - // Пробуем получить данные через /nodes/{node}/rrddata - cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/rrddata", "--output-format", "json") - out, err := cmd.Output() + // Получаем VM + vmRaw, err := runPvesh("/nodes/" + nodeName + "/qemu") + var vms []VM 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 - } - } - 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} - } - } + if err := json.Unmarshal(vmRaw, &vms); err != nil { + debugLog("Failed to parse VMs for %s: %v", nodeName, err) } + } else { + debugLog("Failed to get VMs for %s: %v", nodeName, err) } - // Если данные все еще пустые, пробуем через /nodes/{node}/capabilities - cpuUsage := 0.0 - if val, ok := result["cpu_usage_percent"].(float64); ok { - cpuUsage = val - } else if val, ok := result["cpu_usage_percent"].(int); ok { - cpuUsage = float64(val) - } - - if cpuUsage == 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 -} - -// 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, - } - - // Пробуем получить данные через /nodes/{node}/capabilities - cmd := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+nodeName+"/capabilities", "--output-format", "json") - out, err := cmd.Output() + // Получаем контейнеры + ctRaw, err := runPvesh("/nodes/" + nodeName + "/lxc") + var cts []VM 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) - } - } + if err := json.Unmarshal(ctRaw, &cts); err != nil { + debugLog("Failed to parse containers for %s: %v", nodeName, err) } + } else { + debugLog("Failed to get containers for %s: %v", nodeName, err) } - // Если данные все еще пустые, пробуем через /nodes/{node}/hardware + // Извлекаем данные о CPU из cpuinfo cpuCores := 0 - if val, ok := result["cpu_cores"].(int); ok { - cpuCores = val - } else if val, ok := result["cpu_cores"].(float64); ok { - cpuCores = int(val) + cpuSockets := 0 + cpuModel := "unknown" + if cpuinfo, ok := nodeStatusData["cpuinfo"].(map[string]interface{}); ok { + cpuCores = getInt(cpuinfo, "cores") + cpuSockets = getInt(cpuinfo, "sockets") + cpuModel = getString(cpuinfo, "model") } - - if cpuCores == 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) - } - } - } + + // Извлекаем данные о памяти + memoryTotal := int64(0) + if memory, ok := nodeStatusData["memory"].(map[string]interface{}); ok { + memoryTotal = getInt64(memory, "total") + } + + // Извлекаем loadavg + loadavg := []float64{0, 0, 0} + if loadavgData, ok := nodeStatusData["loadavg"].([]interface{}); ok && len(loadavgData) >= 3 { + for i := 0; i < 3; i++ { + if val, ok := loadavgData[i].(string); ok { + if f, err := strconv.ParseFloat(val, 64); err == nil { + loadavg[i] = f } } } } - return result, nil + // Создаем сводку по VM + vmSummary := createVMSummary(vms, cts, nodeName) + + return map[string]any{ + "cpu_model": cpuModel, + "cpu_cores": cpuCores, + "sockets": cpuSockets, + "threads": cpuCores * cpuSockets, + "total_memory_mb": memoryTotal / 1024 / 1024, + "vm_summary": vmSummary, + "loadavg": loadavg, + } } -// 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') +// VM структура для VM и контейнеров +type VM struct { + Vmid interface{} `json:"vmid"` // Может быть int или string + Name string `json:"name"` + Status string `json:"status"` + Mem int64 `json:"mem"` + MaxMem int64 `json:"maxmem"` + Cores int `json:"cpus"` + MaxDisk int64 `json:"maxdisk"` + Type string `json:"type"` +} + +// createVMSummary создает сводку по VM и контейнерам +func createVMSummary(vms []VM, cts []VM, nodeName string) map[string]any { + totalVMs := len(vms) + runningVMs := 0 + stoppedVMs := 0 + templateVMs := 0 + + for _, v := range vms { + // Проверяем, является ли VM template + if isTemplateVM(v.Vmid, nodeName) { + templateVMs++ + continue // Пропускаем template VM в подсчете + } + + if v.Status == "running" { + runningVMs++ } else { - break + // Считаем stopped VM только если это не template + stoppedVMs++ + } + } + + totalCTs := len(cts) + runningCTs := 0 + stoppedCTs := 0 + for _, c := range cts { + if c.Status == "running" { + runningCTs++ + } else { + stoppedCTs++ } } - return num + return map[string]any{ + "total_vms": totalVMs, + "running_vms": runningVMs, + "stopped_vms": stoppedVMs, + "template_vms": templateVMs, + "total_containers": totalCTs, + "running_containers": runningCTs, + "stopped_containers": stoppedCTs, + } } +// isTemplateVM проверяет, является ли VM template +func isTemplateVM(vmid interface{}, nodeName string) bool { + // Преобразуем vmid в строку + var vmidStr string + switch v := vmid.(type) { + case int: + vmidStr = strconv.Itoa(v) + case string: + vmidStr = v + case float64: + vmidStr = strconv.Itoa(int(v)) + default: + return false + } + + // Получаем конфигурацию VM + raw := runSimple("pvesh", "get", "/nodes/"+nodeName+"/qemu/"+vmidStr+"/config", "--output-format", "json") + if raw == "unknown" { + return false + } + + // Парсим JSON для получения template флага + var config map[string]interface{} + if err := json.Unmarshal([]byte(raw), &config); err != nil { + return false + } + + // Проверяем template флаг (может быть "template" или "templates") + if template, ok := config["template"]; ok { + if templateInt, ok := template.(float64); ok { + return templateInt == 1 + } + if templateInt, ok := template.(int); ok { + return templateInt == 1 + } + } + + // Проверяем templates флаг + if templates, ok := config["templates"]; ok { + if templatesInt, ok := templates.(float64); ok { + return templatesInt == 1 + } + if templatesInt, ok := templates.(int); ok { + return templatesInt == 1 + } + } + + return false +} + +// getCorosyncInfo получает информацию из corosync.conf +func getCorosyncInfo() map[string]any { + corosyncInfo := map[string]any{ + "cluster_name": "unknown", + "transport": "unknown", + "nodes": []map[string]any{}, + } + + // Читаем corosync.conf + raw := runSimple("cat", "/etc/pve/corosync.conf") + if raw == "unknown" { + return corosyncInfo + } + + lines := strings.Split(raw, "\n") + var currentNode map[string]any + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Парсим cluster_name + if strings.HasPrefix(line, "cluster_name:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + corosyncInfo["cluster_name"] = strings.TrimSpace(parts[1]) + } + } + + // Парсим transport + if strings.HasPrefix(line, "transport:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + corosyncInfo["transport"] = strings.TrimSpace(parts[1]) + } + } + + // Парсим ноды + if line == "node {" { + currentNode = map[string]any{} + } else if line == "}" && currentNode != nil { + corosyncInfo["nodes"] = append(corosyncInfo["nodes"].([]map[string]any), currentNode) + currentNode = nil + } else if currentNode != nil { + if strings.HasPrefix(line, "name:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode["name"] = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "nodeid:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + if id, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + currentNode["nodeid"] = id + } + } + } else if strings.HasPrefix(line, "ring0_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode["ring0_addr"] = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "ring1_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode["ring1_addr"] = strings.TrimSpace(parts[1]) + } + } + } + } + + return corosyncInfo +} + +// getQuorumInfo получает информацию о quorum из pvecm status +func getQuorumInfo() map[string]any { + quorumInfo := map[string]any{ + "quorate": false, + "members": 0, + "total_votes": 0, + "expected_votes": 0, + } + + // Получаем информацию из pvecm status + raw := runSimple("pvecm", "status") + if raw == "unknown" { + return quorumInfo + } + + lines := strings.Split(raw, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + if strings.Contains(line, "Quorate:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + quorumInfo["quorate"] = strings.TrimSpace(parts[1]) == "Yes" + } + } else if strings.Contains(line, "Expected votes:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + quorumInfo["expected_votes"] = votes + } + } + } else if strings.Contains(line, "Total votes:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + quorumInfo["total_votes"] = votes + } + } + } else if strings.Contains(line, "Nodes:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + if nodes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + quorumInfo["members"] = nodes + } + } + } + } + + return quorumInfo +} + +// getCorosyncIP получает corosync IP для конкретной ноды (возвращает оба IP) +func getCorosyncIP(nodeName string) []string { + // Читаем corosync.conf + raw := runSimple("cat", "/etc/pve/corosync.conf") + if raw == "unknown" { + return []string{"unknown"} + } + + lines := strings.Split(raw, "\n") + var currentNode string + var ring0Addr string + var ring1Addr string + + for _, line := range lines { + line = strings.TrimSpace(line) + + if line == "node {" { + currentNode = "" + ring0Addr = "" + ring1Addr = "" + } else if line == "}" && currentNode != "" { + if currentNode == nodeName { + ips := []string{ring0Addr} + if ring1Addr != "" { + ips = append(ips, ring1Addr) + } + return ips + } + currentNode = "" + ring0Addr = "" + ring1Addr = "" + } else if strings.HasPrefix(line, "name:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "ring0_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ring0Addr = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "ring1_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ring1Addr = strings.TrimSpace(parts[1]) + } + } + } + + return []string{"unknown"} +} + + // debugLog выводит отладочную информацию (только если установлена переменная окружения DEBUG=1) func debugLog(format string, args ...interface{}) { if os.Getenv("DEBUG") == "1" { fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", args...) } -} +} \ No newline at end of file diff --git a/src/collectors/proxnode/main.go b/src/collectors/proxnode/main.go new file mode 100644 index 0000000..1506e64 --- /dev/null +++ b/src/collectors/proxnode/main.go @@ -0,0 +1,54 @@ +// +build linux + +package main + +// Автор: Сергей Антропов, сайт: https://devops.org.ru +// Коллектор proxnode - собирает информацию о Proxmox ноде + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" +) + +func main() { + // Засекаем время начала выполнения + startTime := time.Now() + + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT + timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + data, err := collectProxCluster(ctx) + if err != nil || data == nil { + fmt.Println("{}") + return + } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + + enc := json.NewEncoder(os.Stdout) + enc.SetEscapeHTML(false) + _ = enc.Encode(data) +} + +func parseDurationOr(env string, def time.Duration) time.Duration { + v := strings.TrimSpace(os.Getenv(env)) + if v == "" { + return def + } + d, err := time.ParseDuration(v) + if err != nil { + return def + } + return d +} diff --git a/src/collectors/proxnode/proxnode_linux.go b/src/collectors/proxnode/proxnode_linux.go new file mode 100644 index 0000000..17679af --- /dev/null +++ b/src/collectors/proxnode/proxnode_linux.go @@ -0,0 +1,965 @@ +//go:build linux + +package main + +// Автор: Сергей Антропов, сайт: https://devops.org.ru +// Коллектор proxcluster для Linux - собирает информацию о Proxmox кластере + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "os/exec" + "strconv" + "strings" +) + +// Структуры для работы с API Proxmox +type ClusterInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Nodes int `json:"nodes"` + Quorate int `json:"quorate"` + Type string `json:"type"` + Version int `json:"version"` +} + +type ClusterStatus struct { + ID string `json:"id"` + IP string `json:"ip"` + Level string `json:"level"` + Local int `json:"local"` + Name string `json:"name"` + NodeID int `json:"nodeid"` + Online int `json:"online"` + Type string `json:"type"` +} + +type NodeStatus struct { + Node string `json:"node"` + Status string `json:"status"` + CPU float64 `json:"cpu"` + MaxCPU int `json:"maxcpu"` + Mem int64 `json:"mem"` + MaxMem int64 `json:"maxmem"` + Uptime int64 `json:"uptime"` + ID string `json:"id"` +} + +type VM struct { + Vmid interface{} `json:"vmid"` // Может быть int или string + Name string `json:"name"` + Status string `json:"status"` + Mem int64 `json:"mem"` + MaxMem int64 `json:"maxmem"` + Cores int `json:"cpus"` + MaxDisk int64 `json:"maxdisk"` + Type string `json:"type"` +} + +// Итоговая структура +type ClusterData struct { + CollectorName string `json:"collector_name"` + Summary map[string]any `json:"summary"` + Nodes []map[string]any `json:"nodes"` +} + +// collectProxCluster собирает информацию о локальном Proxmox сервере +func collectProxCluster(ctx context.Context) (map[string]interface{}, error) { + // Получаем имя локального сервера + hostname := runSimple("hostname") + debugLog("Local hostname: %s", hostname) + + // Получаем статус кластера для получения информации о кластере + clusterStatRaw, err := runPvesh("/cluster/status") + if err != nil { + debugLog("Failed to get cluster status: %v", err) + // Возвращаем базовую структуру при ошибке + return map[string]interface{}{ + "collector_name": "proxcluster", + "summary": map[string]interface{}{ + "cluster_id": "unknown", + "cluster_uuid": "unknown", + "name": "unknown", + "version": "unknown", + "cluster_resources": map[string]interface{}{ + "cpu": map[string]int{ + "total_cores": 0, + "online_cores": 0, + }, + "memory": map[string]int64{ + "total_mb": 0, + "used_mb": 0, + }, + "nodes": map[string]int{ + "total": 0, + "online": 0, + }, + }, + "quorum": map[string]any{}, + "corosync": map[string]any{}, + }, + "nodes": []map[string]any{}, + }, nil + } + + var clusterStatusData []map[string]interface{} + if err := json.Unmarshal(clusterStatRaw, &clusterStatusData); err != nil { + debugLog("Failed to parse cluster status: %v", err) + return nil, err + } + + // Извлекаем информацию о кластере (первый элемент) + var clusterInfo ClusterInfo + if len(clusterStatusData) > 0 && clusterStatusData[0]["type"] == "cluster" { + clusterInfo.ID = getString(clusterStatusData[0], "id") + clusterInfo.Name = getString(clusterStatusData[0], "name") + clusterInfo.Nodes = getInt(clusterStatusData[0], "nodes") + clusterInfo.Quorate = getInt(clusterStatusData[0], "quorate") + clusterInfo.Type = getString(clusterStatusData[0], "type") + clusterInfo.Version = getInt(clusterStatusData[0], "version") + } + + // Извлекаем информацию о всех нодах и находим локальную + var clusterStat []ClusterStatus + var localNode ClusterStatus + for i := 1; i < len(clusterStatusData); i++ { + if clusterStatusData[i]["type"] == "node" { + node := ClusterStatus{ + ID: getString(clusterStatusData[i], "id"), + IP: getString(clusterStatusData[i], "ip"), + Level: getString(clusterStatusData[i], "level"), + Local: getInt(clusterStatusData[i], "local"), + Name: getString(clusterStatusData[i], "name"), + NodeID: getInt(clusterStatusData[i], "nodeid"), + Online: getInt(clusterStatusData[i], "online"), + Type: getString(clusterStatusData[i], "type"), + } + clusterStat = append(clusterStat, node) + + // Находим локальную ноду + if node.Local == 1 { + localNode = node + } + } + } + + debugLog("Cluster info: %+v", clusterInfo) + debugLog("Cluster nodes: %+v", clusterStat) + debugLog("Local node: %+v", localNode) + + // Получаем cluster_uuid из corosync + clusterUUID := runSimple("grep", "cluster_name", "/etc/pve/corosync.conf") + if clusterUUID == "unknown" || clusterUUID == "" { + clusterUUID = clusterInfo.Name // fallback на имя из API + } else { + // Извлекаем только значение после двоеточия + parts := strings.SplitN(clusterUUID, ":", 2) + if len(parts) > 1 { + clusterUUID = strings.TrimSpace(parts[1]) + } + } + + // Генерируем cluster_id через SHA + clusterID := generateClusterID(clusterUUID) + + + // Обрабатываем только локальную ноду + nodes := []map[string]any{} + + if localNode.Name != "" { + debugLog("Processing local node: %s", localNode.Name) + + // Получаем статус локальной ноды + nodeStatRaw, err := runPvesh("/nodes/" + localNode.Name + "/status") + var nodeStatusData map[string]interface{} + if err == nil { + if err := json.Unmarshal(nodeStatRaw, &nodeStatusData); err != nil { + debugLog("Failed to parse node status for %s: %v", localNode.Name, err) + } + } else { + debugLog("Failed to get node status for %s: %v", localNode.Name, err) + } + + debugLog("Node status data for %s: %+v", localNode.Name, nodeStatusData) + + // Получаем VM + vmRaw, err := runPvesh("/nodes/" + localNode.Name + "/qemu") + var vms []VM + if err == nil { + if err := json.Unmarshal(vmRaw, &vms); err != nil { + debugLog("Failed to parse VMs for %s: %v", localNode.Name, err) + } + } else { + debugLog("Failed to get VMs for %s: %v", localNode.Name, err) + } + + // Получаем контейнеры + ctRaw, err := runPvesh("/nodes/" + localNode.Name + "/lxc") + var cts []VM + if err == nil { + if err := json.Unmarshal(ctRaw, &cts); err != nil { + debugLog("Failed to parse containers for %s: %v", localNode.Name, err) + } + } else { + debugLog("Failed to get containers for %s: %v", localNode.Name, err) + } + + // Генерируем node_uid через SHA + nodeUID := generateNodeUID(clusterInfo.ID, strconv.Itoa(localNode.NodeID)) + + // Извлекаем данные из статуса ноды + cpuUsage := getFloat64(nodeStatusData, "cpu") * 100 + // Если CPU usage из API равен 0, получаем реальные данные из top + if cpuUsage == 0 { + cpuUsage = getRealCPUUsage() + } + uptime := getInt64(nodeStatusData, "uptime") + + // Извлекаем данные о памяти + memoryUsed := int64(0) + memoryTotal := int64(0) + if memory, ok := nodeStatusData["memory"].(map[string]interface{}); ok { + memoryUsed = getInt64(memory, "used") + memoryTotal = getInt64(memory, "total") + } + + // Извлекаем данные о swap + swapUsed := int64(0) + if swap, ok := nodeStatusData["swap"].(map[string]interface{}); ok { + swapUsed = getInt64(swap, "used") + } + + // Извлекаем данные о CPU из cpuinfo + cpuCores := 0 + cpuSockets := 0 + cpuModel := "unknown" + if cpuinfo, ok := nodeStatusData["cpuinfo"].(map[string]interface{}); ok { + cpuCores = getInt(cpuinfo, "cores") + cpuSockets = getInt(cpuinfo, "sockets") + cpuModel = getString(cpuinfo, "model") + } + + // Извлекаем loadavg + loadavg := []float64{0, 0, 0} + if loadavgData, ok := nodeStatusData["loadavg"].([]interface{}); ok && len(loadavgData) >= 3 { + for i := 0; i < 3; i++ { + if val, ok := loadavgData[i].(string); ok { + if f, err := strconv.ParseFloat(val, 64); err == nil { + loadavg[i] = f + } + } + } + } + + // Получаем corosync IP из конфигурации + corosyncIPs := getCorosyncIP(localNode.Name) + + // Создаем данные локальной ноды + node := map[string]any{ + "cluster_name": clusterInfo.Name, + "cluster_uuid": clusterUUID, + "name": localNode.Name, + "node_id": localNode.NodeID, + "node_uid": nodeUID, + "cluster_uid": clusterID, + "online": localNode.Online == 1, + "product_uuid": runSimple("dmidecode", "-s", "system-uuid"), + "machine_id": runSimple("cat", "/etc/machine-id"), + "corosync_ip": corosyncIPs, + "real_ips": getRealIPs(), + "nodes": map[string]int{ + "online": countOnline(clusterStat), + "total": len(clusterStat), + }, + "hardware": map[string]any{ + "cpu_cores": cpuCores, + "cpu_model": cpuModel, + "total_memory_mb": memoryTotal / 1024 / 1024, + "sockets": cpuSockets, + "threads": cpuCores * cpuSockets, + "total_cpu_cores": cpuCores * cpuSockets, + }, + "os": map[string]any{ + "kernel": getString(nodeStatusData, "kversion"), + "pve_version": getString(nodeStatusData, "pveversion"), + "uptime_human": formatUptime(uptime), + "uptime_sec": uptime, + }, + "resources_used": map[string]any{ + "cpu_usage_percent": cpuUsage, + "memory_used_mb": memoryUsed / 1024 / 1024, + "swap_used_mb": swapUsed / 1024 / 1024, + "loadavg": loadavg, + }, + "vm_summary": map[string]any{ + "total_vms": len(vms), + "total_containers": len(cts), + "running_vms": 0, // Будет заполнено ниже + "running_containers": 0, // Будет заполнено ниже + "stopped_containers": 0, // Будет заполнено ниже + "stopped_vms": 0, // Будет заполнено ниже + "template_vms": 0, // Будет заполнено ниже + }, + "vm_resources_summary": map[string]any{ + "total_host_cores": cpuCores * cpuSockets, + "host_vm_total_cores": 0, // Будет заполнено ниже + "host_vm_used_cores": 0, // Будет заполнено ниже + "kvm64_vm_total_cores": 0, // Будет заполнено ниже + "kvm64_vm_used_cores": 0, // Будет заполнено ниже + "total_vm_cores": 0, // Будет заполнено ниже + "used_vm_cores": 0, // Будет заполнено ниже + "total_vm_memory_mb": 0, // Будет заполнено ниже + "used_vm_memory_mb": 0, // Будет заполнено ниже + }, + "quorum": getQuorumInfo(), + "version": runSimple("pveversion"), + } + + // Получаем данные из summarizeVMs + vmSummaryData := summarizeVMs(vms, cts, cpuCores, cpuSockets, localNode.Name) + + // Заполняем vm_summary + node["vm_summary"].(map[string]any)["running_vms"] = vmSummaryData["running_vms"] + node["vm_summary"].(map[string]any)["running_containers"] = vmSummaryData["running_containers"] + node["vm_summary"].(map[string]any)["stopped_containers"] = vmSummaryData["stopped_containers"] + node["vm_summary"].(map[string]any)["stopped_vms"] = vmSummaryData["stopped_vms"] + node["vm_summary"].(map[string]any)["template_vms"] = vmSummaryData["template_vms"] + + // Заполняем vm_resources_summary + node["vm_resources_summary"].(map[string]any)["host_vm_total_cores"] = vmSummaryData["host_total_cores"] + node["vm_resources_summary"].(map[string]any)["host_vm_used_cores"] = vmSummaryData["host_used_cores"] + node["vm_resources_summary"].(map[string]any)["kvm64_vm_total_cores"] = vmSummaryData["kvm64_total_cores"] + node["vm_resources_summary"].(map[string]any)["kvm64_vm_used_cores"] = vmSummaryData["kvm64_used_cores"] + node["vm_resources_summary"].(map[string]any)["total_vm_cores"] = vmSummaryData["total_cpu_cores"] + node["vm_resources_summary"].(map[string]any)["used_vm_cores"] = vmSummaryData["used_cpu_cores"] + node["vm_resources_summary"].(map[string]any)["total_vm_memory_mb"] = vmSummaryData["total_memory_mb"] + node["vm_resources_summary"].(map[string]any)["used_vm_memory_mb"] = vmSummaryData["used_memory_mb"] + + nodes = append(nodes, node) + } + + // Возвращаем структуру в новом формате + resultMap := map[string]interface{}{ + "collector_name": "proxnode", + "node": nodes, + } + + debugLog("Final result: %+v", resultMap) + return resultMap, nil +} + +// runPvesh запускает pvesh get и возвращает JSON +func runPvesh(path string) ([]byte, error) { + cmd := exec.Command("pvesh", "get", path, "--output-format", "json") + return cmd.Output() +} + +// countOnline считает количество онлайн нод +func countOnline(stat []ClusterStatus) int { + c := 0 + for _, n := range stat { + if n.Online == 1 { + c++ + } + } + return c +} + +// runSimple выполняет простую команду и возвращает результат +func runSimple(cmd string, args ...string) string { + out, err := exec.Command(cmd, args...).Output() + if err != nil { + debugLog("Failed to run command %s %v: %v", cmd, args, err) + return "unknown" + } + return strings.TrimSpace(string(out)) +} + + +// readLoadavg читает load average из /proc/loadavg +func readLoadavg() []float64 { + raw := runSimple("cat", "/proc/loadavg") + parts := strings.Fields(raw) + var res []float64 + for i := 0; i < 3 && i < len(parts); i++ { + var f float64 + if _, err := fmt.Sscanf(parts[i], "%f", &f); err == nil { + res = append(res, f) + } else { + res = append(res, 0.0) + } + } + // Дополняем до 3 элементов если не хватает + for len(res) < 3 { + res = append(res, 0.0) + } + return res +} + +// runCpuModel получает модель CPU +func runCpuModel() string { + raw := runSimple("grep", "model name", "/proc/cpuinfo") + if raw == "" { + return "unknown" + } + // Берем только первую строку + lines := strings.Split(raw, "\n") + if len(lines) > 0 { + parts := strings.SplitN(lines[0], ":", 2) + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + return "unknown" +} + +// summarizeVMs создает сводку по VM и контейнерам +func summarizeVMs(vms []VM, cts []VM, hostCores int, hostSockets int, nodeName string) map[string]any { + totalVMs := len(vms) + runningVMs := 0 + stoppedVMs := 0 + templateVMs := 0 + cpuCores := 0 + usedCpuCores := 0 + hostUsedCores := 0 + hostTotalCores := 0 + kvm64TotalCores := 0 + kvm64UsedCores := 0 + memUsed := int64(0) + memTotal := int64(0) + + for _, v := range vms { + // Проверяем, является ли VM template + if isTemplateVM(v.Vmid, nodeName) { + templateVMs++ + continue // Пропускаем template VM в подсчете ресурсов + } + + if v.Status == "running" { + runningVMs++ + + // Проверяем тип CPU для запущенных VM + if isHostCPU(v.Vmid, nodeName) { + hostUsedCores += v.Cores // Ядра типа "host" + } else { + usedCpuCores += v.Cores // Ядра типа "kvm64", "kvm32" и т.д. + kvm64UsedCores += v.Cores // Все не-host типы считаем kvm64 + } + } else { + // Считаем stopped VM только если это не template + stoppedVMs++ + } + + // Подсчитываем общие ядра для всех VM (включенных и выключенных, но не template) + cpuCores += v.Cores + if isHostCPU(v.Vmid, nodeName) { + hostTotalCores += v.Cores // Общие ядра типа "host" + } else { + // Все остальные типы CPU (kvm64, kvm32 и т.д.) считаем kvm64 + kvm64TotalCores += v.Cores + } + + memUsed += v.Mem + memTotal += v.MaxMem + } + + totalCTs := len(cts) + runningCTs := 0 + stoppedCTs := 0 + for _, c := range cts { + if c.Status == "running" { + runningCTs++ + hostUsedCores += c.Cores // Контейнеры всегда используют host ядра + } else { + stoppedCTs++ + } + cpuCores += c.Cores + hostTotalCores += c.Cores // Контейнеры всегда используют host ядра + memUsed += c.Mem + memTotal += c.MaxMem + } + + // Вычисляем физические host ядра + physicalHostCores := hostCores * hostSockets + + return map[string]any{ + "total_vms": totalVMs, + "running_vms": runningVMs, + "stopped_vms": stoppedVMs, + "template_vms": templateVMs, // Количество template VM + "total_containers": totalCTs, + "running_containers": runningCTs, + "stopped_containers": stoppedCTs, + "total_cpu_cores": cpuCores, + "total_memory_mb": memTotal / 1024 / 1024, + "used_cpu_cores": usedCpuCores, + "used_memory_mb": memUsed / 1024 / 1024, + "host_total_cores": hostTotalCores, // Общие ядра VM с типом "host" + контейнеры (без template) + "host_used_cores": hostUsedCores, // Используемые ядра VM с типом "host" + контейнеры + "kvm64_total_cores": kvm64TotalCores, // Общие ядра VM с типом "kvm64" (без template) + "kvm64_used_cores": kvm64UsedCores, // Используемые ядра VM с типом "kvm64" + "physical_host_cores": physicalHostCores, // Физические ядра хоста + } +} + +// 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] +} + +// getString извлекает строку из map[string]interface{} +func getString(data map[string]interface{}, key string) string { + if val, ok := data[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +// getInt извлекает int из map[string]interface{} +func getInt(data map[string]interface{}, key string) int { + if val, ok := data[key]; ok { + switch v := val.(type) { + case int: + return v + case float64: + return int(v) + case string: + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + } + return 0 +} + +// getInt64 извлекает int64 из map[string]interface{} +func getInt64(data map[string]interface{}, key string) int64 { + if val, ok := data[key]; ok { + switch v := val.(type) { + case int: + return int64(v) + case int64: + return v + case float64: + return int64(v) + case string: + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + } + } + return 0 +} + +// getFloat64 извлекает float64 из map[string]interface{} +func getFloat64(data map[string]interface{}, key string) float64 { + if val, ok := data[key]; ok { + switch v := val.(type) { + case float64: + return v + case int: + return float64(v) + case int64: + return float64(v) + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + } + } + return 0.0 +} + +// getRealIPs получает реальные IP адреса интерфейсов (исключая corosync IP) +func getRealIPs() []string { + raw := runSimple("ip", "addr", "show") + lines := strings.Split(raw, "\n") + var ips []string + + // Получаем corosync IP для исключения + corosyncIPs := getCorosyncIPs() + + for _, line := range lines { + line = strings.TrimSpace(line) + 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] // убираем /24, /25 и т.д. + + // Проверяем, не является ли это corosync IP + isCorosyncIP := false + for _, corosyncIP := range corosyncIPs { + if ip == corosyncIP { + isCorosyncIP = true + break + } + } + + if !isCorosyncIP { + ips = append(ips, ip) + } + } + } + } + return ips +} + +// isHostCPU проверяет, использует ли VM host CPU +func isHostCPU(vmid interface{}, nodeName string) bool { + // Преобразуем vmid в строку + var vmidStr string + switch v := vmid.(type) { + case int: + vmidStr = strconv.Itoa(v) + case string: + vmidStr = v + default: + return false + } + + // Получаем конфигурацию VM + raw := runSimple("pvesh", "get", "/nodes/"+nodeName+"/qemu/"+vmidStr+"/config", "--output-format", "json") + if raw == "unknown" { + return false + } + + // Парсим JSON для получения типа CPU + var config map[string]interface{} + if err := json.Unmarshal([]byte(raw), &config); err != nil { + return false + } + + // Проверяем тип CPU + if cpuType, ok := config["cpu"].(string); ok { + return cpuType == "host" + } + + // По умолчанию считаем kvm64 (не host) + return false +} + +// isTemplateVM проверяет, является ли VM template +func isTemplateVM(vmid interface{}, nodeName string) bool { + // Преобразуем vmid в строку + var vmidStr string + switch v := vmid.(type) { + case int: + vmidStr = strconv.Itoa(v) + case string: + vmidStr = v + case float64: + vmidStr = strconv.Itoa(int(v)) + default: + return false + } + + // Получаем конфигурацию VM + raw := runSimple("pvesh", "get", "/nodes/"+nodeName+"/qemu/"+vmidStr+"/config", "--output-format", "json") + if raw == "unknown" { + return false + } + + // Парсим JSON для получения template флага + var config map[string]interface{} + if err := json.Unmarshal([]byte(raw), &config); err != nil { + return false + } + + // Проверяем template флаг (может быть "template" или "templates") + if template, ok := config["template"]; ok { + if templateInt, ok := template.(float64); ok { + return templateInt == 1 + } + if templateInt, ok := template.(int); ok { + return templateInt == 1 + } + } + + // Проверяем templates флаг + if templates, ok := config["templates"]; ok { + if templatesInt, ok := templates.(float64); ok { + return templatesInt == 1 + } + if templatesInt, ok := templates.(int); ok { + return templatesInt == 1 + } + } + + return false +} + + +// formatUptime форматирует uptime в человеческий вид +func formatUptime(seconds int64) string { + days := seconds / 86400 + hours := (seconds % 86400) / 3600 + minutes := (seconds % 3600) / 60 + + if days > 0 { + return fmt.Sprintf("%d days, %d hours, %d minutes", days, hours, minutes) + } else if hours > 0 { + return fmt.Sprintf("%d hours, %d minutes", hours, minutes) + } else { + return fmt.Sprintf("%d minutes", minutes) + } +} + +// getRealCPUUsage получает реальное использование CPU из top +func getRealCPUUsage() float64 { + // Получаем данные из top + raw := runSimple("top", "-bn1") + lines := strings.Split(raw, "\n") + + for _, line := range lines { + if strings.Contains(line, "%Cpu(s):") { + // Парсим строку типа: %Cpu(s): 6.5 us, 7.5 sy, 0.0 ni, 59.3 id, 26.5 wa, 0.0 hi, 0.2 si, 0.0 st + parts := strings.Split(line, ",") + us := 0.0 + sy := 0.0 + + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.Contains(part, "us") { + fmt.Sscanf(part, "%f us", &us) + } else if strings.Contains(part, "sy") { + fmt.Sscanf(part, "%f sy", &sy) + } + } + + return us + sy // user + system = общее использование CPU + } + } + return 0.0 +} + +// getCorosyncInfo получает информацию из corosync.conf +func getCorosyncInfo() map[string]any { + corosyncInfo := map[string]any{ + "cluster_name": "unknown", + "transport": "unknown", + "nodes": []map[string]any{}, + } + + // Читаем corosync.conf + raw := runSimple("cat", "/etc/pve/corosync.conf") + if raw == "unknown" { + return corosyncInfo + } + + lines := strings.Split(raw, "\n") + var currentNode map[string]any + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Парсим cluster_name + if strings.HasPrefix(line, "cluster_name:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + corosyncInfo["cluster_name"] = strings.TrimSpace(parts[1]) + } + } + + // Парсим transport + if strings.HasPrefix(line, "transport:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + corosyncInfo["transport"] = strings.TrimSpace(parts[1]) + } + } + + // Парсим ноды + if line == "node {" { + currentNode = map[string]any{} + } else if line == "}" && currentNode != nil { + corosyncInfo["nodes"] = append(corosyncInfo["nodes"].([]map[string]any), currentNode) + currentNode = nil + } else if currentNode != nil { + if strings.HasPrefix(line, "name:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode["name"] = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "nodeid:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + if id, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + currentNode["nodeid"] = id + } + } + } else if strings.HasPrefix(line, "ring0_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode["ring0_addr"] = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "ring1_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode["ring1_addr"] = strings.TrimSpace(parts[1]) + } + } + } + } + + return corosyncInfo +} + +// getQuorumInfo получает информацию о quorum из pvecm status +func getQuorumInfo() map[string]any { + quorumInfo := map[string]any{ + "quorate": false, + "members": 0, + "total_votes": 0, + "expected_votes": 0, + } + + // Получаем информацию из pvecm status + raw := runSimple("pvecm", "status") + if raw == "unknown" { + return quorumInfo + } + + lines := strings.Split(raw, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + if strings.Contains(line, "Quorate:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + quorumInfo["quorate"] = strings.TrimSpace(parts[1]) == "Yes" + } + } else if strings.Contains(line, "Expected votes:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + quorumInfo["expected_votes"] = votes + } + } + } else if strings.Contains(line, "Total votes:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + quorumInfo["total_votes"] = votes + } + } + } else if strings.Contains(line, "Nodes:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + if nodes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { + quorumInfo["members"] = nodes + } + } + } + } + + return quorumInfo +} + +// getCorosyncIP получает corosync IP для конкретной ноды (возвращает оба IP) +func getCorosyncIP(nodeName string) []string { + // Читаем corosync.conf + raw := runSimple("cat", "/etc/pve/corosync.conf") + if raw == "unknown" { + return []string{"unknown"} + } + + lines := strings.Split(raw, "\n") + var currentNode string + var ring0Addr string + var ring1Addr string + + for _, line := range lines { + line = strings.TrimSpace(line) + + if line == "node {" { + currentNode = "" + ring0Addr = "" + ring1Addr = "" + } else if line == "}" && currentNode != "" { + if currentNode == nodeName { + ips := []string{ring0Addr} + if ring1Addr != "" { + ips = append(ips, ring1Addr) + } + return ips + } + currentNode = "" + ring0Addr = "" + ring1Addr = "" + } else if strings.HasPrefix(line, "name:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + currentNode = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "ring0_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ring0Addr = strings.TrimSpace(parts[1]) + } + } else if strings.HasPrefix(line, "ring1_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ring1Addr = strings.TrimSpace(parts[1]) + } + } + } + + return []string{"unknown"} +} + +// getCorosyncIPs получает все corosync IP адреса +func getCorosyncIPs() []string { + var ips []string + + // Читаем corosync.conf + raw := runSimple("cat", "/etc/pve/corosync.conf") + if raw == "unknown" { + return ips + } + + lines := strings.Split(raw, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + if strings.HasPrefix(line, "ring0_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ip := strings.TrimSpace(parts[1]) + ips = append(ips, ip) + } + } else if strings.HasPrefix(line, "ring1_addr:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + ip := strings.TrimSpace(parts[1]) + ips = append(ips, ip) + } + } + } + + return ips +} + +// debugLog выводит отладочную информацию (только если установлена переменная окружения DEBUG=1) +func debugLog(format string, args ...interface{}) { + if os.Getenv("DEBUG") == "1" { + fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", args...) + } +} \ No newline at end of file diff --git a/src/collectors/proxnode/proxnode_unsupported.go b/src/collectors/proxnode/proxnode_unsupported.go new file mode 100644 index 0000000..31b88a2 --- /dev/null +++ b/src/collectors/proxnode/proxnode_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux + +package main + +import ( + "os" +) + +func main() { + // Платформа не поддерживается + os.Exit(1) +} diff --git a/src/collectors/sensors/main.go b/src/collectors/sensors/main.go index 80ec613..fe71857 100644 --- a/src/collectors/sensors/main.go +++ b/src/collectors/sensors/main.go @@ -15,6 +15,9 @@ import ( // collectSensors реализуется платформенно. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -25,6 +28,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/system/main.go b/src/collectors/system/main.go index bde43a0..020a258 100644 --- a/src/collectors/system/main.go +++ b/src/collectors/system/main.go @@ -16,6 +16,9 @@ import ( // collectSystem реализуется в файлах с билд-тегами под конкретные ОС. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -26,6 +29,14 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(data) diff --git a/src/collectors/uptime/main.go b/src/collectors/uptime/main.go index 8399d7c..cef362d 100644 --- a/src/collectors/uptime/main.go +++ b/src/collectors/uptime/main.go @@ -15,6 +15,9 @@ import ( // collectUptime реализуется в файлах с билд-тегами под конкретные ОС. func main() { + // Засекаем время начала выполнения + startTime := time.Now() + timeout := parseDurationOr("COLLECTOR_TIMEOUT", 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -24,10 +27,16 @@ func main() { fmt.Println("{}") return } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + out := map[string]any{ "collector_name": "uptime", "seconds": secs, "human": humanize(time.Duration(secs) * time.Second), + "execution_time_ms": executionTime.Milliseconds(), + "execution_time_seconds": executionTime.Seconds(), } enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) diff --git a/src/core/output/output.go b/src/core/output/output.go index ccf3c66..7a64388 100644 --- a/src/core/output/output.go +++ b/src/core/output/output.go @@ -31,6 +31,15 @@ type Output interface { type StdoutOutput struct{} func (s *StdoutOutput) Write(_ context.Context, data Payload) error { + // Извлекаем имена коллекторов из payload для логирования + collectorNames := make([]string, 0, len(data)) + for key := range data { + collectorNames = append(collectorNames, key) + } + + // Логируем вывод в stdout с информацией о коллекторах + slog.Info("stdout output", "collectors", collectorNames) + enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) return enc.Encode(data) @@ -137,8 +146,15 @@ func (k *KafkaOutput) Write(ctx context.Context, data Payload) error { if err := k.writer.WriteMessages(ctx, msg); err != nil { return err } - // Логируем успешную отправку - slog.Info("kafka message sent", "topic", k.topic, "bytes", len(b)) + + // Извлекаем имена коллекторов из payload для логирования + collectorNames := make([]string, 0, len(data)) + for key := range data { + collectorNames = append(collectorNames, key) + } + + // Логируем успешную отправку с информацией о коллекторах + slog.Info("kafka message sent", "topic", k.topic, "bytes", len(b), "collectors", collectorNames) return nil } diff --git a/src/core/runner/runner.go b/src/core/runner/runner.go index 9b34347..d782ab7 100644 --- a/src/core/runner/runner.go +++ b/src/core/runner/runner.go @@ -40,11 +40,13 @@ func (r *Runner) RunOnce(ctx context.Context) { wg.Add(1) go func(c collector.Collector) { defer wg.Done() + slog.Info("collector started", "name", c.Name(), "key", c.Key()) res, err := c.Collect(ctx) if err != nil { slog.Warn("collector error", "name", c.Name(), "err", err) res = map[string]any{} } + slog.Info("collector completed", "name", c.Name(), "key", c.Key()) mu.Lock() payload[c.Key()] = res mu.Unlock() @@ -80,12 +82,14 @@ func (r *Runner) RunContinuous(ctx context.Context) { defer wg.Done() // Немедленный первый запуск runOnce := func() { + slog.Info("collector started", "name", c.Name(), "key", c.Key()) res, err := c.Collect(ctx) if err != nil { slog.Warn("collector error", "name", c.Name(), "err", err) res = map[string]any{} } if res == nil { res = map[string]any{} } + slog.Info("collector completed", "name", c.Name(), "key", c.Key()) select { case updates <- update{key: c.Key(), data: res, intervalSec: int(c.Interval().Seconds())}: case <-ctx.Done():