Menu

Мониторинг парка виртуальных машин

Знакомые попросили внедрить систему мониторинга для парка виртуальных машин, которые остаются в офисе работать по ночам. На коленке, за один вечер получилось заколхозить рабочее решение.

Дано: виртуальные машины под Windows 10 работают на физических машинах под RED OS. Необходимо снимать базовые метрики типа загрузки CPU, свободного места ни диске и т.п. и отправлять на сервер, чтобы хранить и визуализировать.

Решение: поднимаем отдельный сервер с белым IP с Debian 13, устанавливаем nginx на порту 8080 (как точку входа), СУБД MariaDB, Grafana.

Ниже привожу конфигурационные файлы, и сразу оговорюсь, что в СУБД было создано две БД, которые наполняются параллельно - это объяснимо, т.к. первую БД изобрёл заказчик, а вторую я сделал как положено.

Скрипт для сбора и отпрвки статистики на ВМ выглядит так:

# send-metrics.ps1

$vm_id = "VM-303-11" # Уникальный ID ВМ
$vm_profile = "Coffee" # Уникальный profile ВМ
$vm_threads = "2" # How many threads are allowed for ВМ

$server_ip = "47.82.X.X"
$api_endpoint = "http://$server_ip`:8080/metrics"

Write-Host "[+] Начинаю сбор метрик для $vm_id..." -ForegroundColor Cyan

# 1. Сбор метрик
try {
$cpu = (Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
if ($null -eq $cpu) { $cpu = 0 }

$diskObj = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'"
$disk_free = if ($diskObj) { $diskObj.FreeSpace } else { 0 }

# Check if BAS process is running (regardless of window title)
$basProc = Get-Process BAS -ErrorAction SilentlyContinue
$bas_running = if ($basProc) { $true } else { $false }

# Still capture title if available (for debugging)
$bas_title = if ($basProc -and $basProc.MainWindowTitle) {
$basProc.MainWindowTitle
} else {
"BAS running (no window)" # More accurate than "not running"
}

Write-Host "[+] CPU: $cpu%, Disk free: $disk_free bytes, BAS running: $bas_running, Title: '$bas_title'" -ForegroundColor Green
} catch {
Write-Host "[-] Ошибка при сборе метрик: $($_.Exception.Message)" -ForegroundColor Red
Start-Sleep -Seconds 15
exit 1
}

# 2. Анализ логов BAS
$log_path = "C:\tmp\success.log"
$success_events = @()

if (Test-Path $log_path) {
try {
$lines = Get-Content $log_path -ErrorAction Stop | Select-Object -Last 100
$matched = $lines | Where-Object { $_ -match "ПРОГОН ЗАКОНЧЕН!!!" }
$success_events = foreach ($line in $matched) {
@{
timestamp = Get-Date -Format "o"
message = $line
}
}
Write-Host "[+] Найдено $($success_events.Count) успешных событий в логе." -ForegroundColor Green
} catch {
Write-Host "[-] Ошибка при чтении лога: $($_.Exception.Message)" -ForegroundColor Yellow
}
} else {
Write-Host "[ ] Лог-файл $log_path не найден." -ForegroundColor Gray
}

# 3. Формирование JSON — NOW INCLUDES vm_profile and vm_threads
try {
$bodyObj = @{
vm_id = $vm_id
vm_profile = $vm_profile # ← Added
vm_threads = $vm_threads # ← Added (sent as string; server can parse as int if needed)
timestamp = Get-Date -Format "o"
cpu = $cpu
disk_free = $disk_free
bas_running = $bas_running # More reliable status
bas_title = $bas_title # For debugging
success_events = $success_events
}

$body = $bodyObj | ConvertTo-Json -Depth 3 -Compress
Write-Host "[+] Сформирован JSON (первые 100 символов): $($body.Substring(0, [Math]::Min(100, $body.Length)))..." -ForegroundColor Cyan
} catch {
Write-Host "[-] Ошибка при создании JSON: $($_.Exception.Message)" -ForegroundColor Red
Start-Sleep -Seconds 15
exit 1
}

# 4. Отправка на сервер
try {
Write-Host "[+] Отправляю данные на $api_endpoint..." -ForegroundColor Cyan
$response = Invoke-RestMethod -Uri $api_endpoint -Method Post -Body $body -ContentType "application/json" -TimeoutSec 15
Write-Host "[+] Успешный ответ от сервера: $($response | ConvertTo-Json -Depth 2)" -ForegroundColor Green
} catch {
$errorMessage = $_.Exception.Message
if ($_.Exception.Response) {
$statusCode = $_.Exception.Response.StatusCode.value__
Write-Host "[-] HTTP ошибка $statusCode при отправке: $errorMessage" -ForegroundColor Red
} else {
Write-Host "[-] Ошибка подключения: $errorMessage" -ForegroundColor Red
}
}

# Пауза для ручного запуска
Write-Host "`n[=] Скрипт завершён. Пауза на 3-5 секунд..." -ForegroundColor Magenta
Start-Sleep -Seconds 4

 

Удобно из консоли добавить его в планировщик:

 


schtasks /create /tn "SendMetrics" /tr "powershell.exe -ExecutionPolicy Bypass -File \"C:\tmp\send-metrics.ps1\"" /sc minute /mo 5 /f

 

 


Далее, на сервере запускаем nginx:

 



/etc/nginx/conf.d # cat monitor.conf
server {
listen 8080;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
}
}

 

Дополнительно нам потребуется скрипт на Питоне, который мы будем запускать как службу:


root@SammiCheng ~/bas_monitoring # cat server.py
from flask import Flask, request, jsonify
import mysql.connector
from datetime import datetime
from zoneinfo import ZoneInfo # Python 3.9+

app = Flask(__name__)

# Подключение к двум БД
db1 = mysql.connector.connect(
host="localhost",
user="root",
password="drach.pro",
database="bas_monitor"
)

db2 = mysql.connector.connect(
host="localhost",
user="root",
password="drach.pro",
database="bas_monitor_2"
)

# Московский часовой пояс
MOSCOW_TZ = ZoneInfo("Europe/Moscow")

@app.route('/metrics', methods=['POST'])
def receive_metrics():
try:
data = request.get_json()
if not data:
return jsonify({"error": "No JSON data received"}), 400

moscow_time = datetime.now(MOSCOW_TZ).strftime('%Y-%m-%d %H:%M:%S')

# === Надёжная обработка входных данных ===
vm_id = str(data.get('vm_id', 'UNKNOWN'))[:100]
vm_profile = str(data.get('vm_profile', 'Cyber'))[:50]
if vm_profile.strip() == '' or vm_profile.lower() == 'null':
vm_profile = 'Cyber'

vm_threads_raw = data.get('vm_threads', '0')
try:
threads = int(vm_threads_raw)
except (ValueError, TypeError):
threads = 0

cpu = float(data.get('cpu', 0.0)) if data.get('cpu') is not None else 0.0
disk_free = int(data.get('disk_free', 0)) if data.get('disk_free') is not None else 0

# Защищаем bas_title от dict/list
bas_title_raw = data.get('bas_title')
if bas_title_raw is None:
bas_title = ""
elif isinstance(bas_title_raw, (dict, list)):
bas_title = str(bas_title_raw)[:255]
else:
bas_title = str(bas_title_raw)[:255]

# Обрабатываем success_events
success_events = data.get('success_events', [])
if not isinstance(success_events, list):
success_events = []
success_count = len(success_events)

# === Первая БД: bas_monitor ===
cursor1 = db1.cursor()
cursor1.execute("""
INSERT INTO metrics (vm_id, timestamp, cpu, disk_free, bas_title)
VALUES (%s, %s, %s, %s, %s)
""", (vm_id, moscow_time, cpu, disk_free, bas_title))

for event in success_events:
msg = str(event.get('message', ''))[:512] if isinstance(event, dict) else str(event)[:512]
cursor1.execute("""
INSERT INTO success_events (vm_id, event_time, message)
VALUES (%s, %s, %s)
""", (vm_id, moscow_time, msg))

db1.commit()
cursor1.close()

# === Вторая БД: bas_monitor_2 ===
cursor2 = db2.cursor()
cursor2.execute("""
INSERT INTO metrics (vm_id, vm_group, timestamp, cpu, disk_free, threads, bas_title, success)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""", (vm_id, vm_profile, moscow_time, cpu, disk_free, threads, bas_title, success_count))

db2.commit()
cursor2.close()

return jsonify({"status": "ok"}), 200

except Exception as e:
print(f"[ERROR] {type(e).__name__}: {e}")
return jsonify({"error": "Internal server error"}), 500

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000) # порт не совпадает с API_ENDPOINT в PowerShell, а совпадает с портом nginx!

 

Теперь запускаем Grafana и идём настраивать графики...

Оставить комментарий

Ваше мнение очень важно для нас! Обязательно выскажите Ваши мысли, пожелания и критику! Не стесняйтесь задавать вопросы. Скорее всего, ответ появится уже через 2-3 дня. Спасибо заранее.

Удалите из списка все объекты, которые не являются животными:

🐶
🐱
🐭
🐹
🗑️ Перетащите сюда
Go to top