Проект

Общее

Профиль

Бэкап Hyper-V через PowerShell » История » Версия 1

Redmine Admin, 25.03.2026 10:44

1 1 Redmine Admin
<div class="hyperv-backup-guide">
2
  <h2>💾 Автоматический бэкап виртуальных машин Hyper-V</h2>
3
  <p><strong>Описание:</strong> Скрипт PowerShell для еженедельного экспорта ВМ Hyper-V с ротацией архивов и автоматической настройкой через Планировщик заданий.</p>
4
5
  <div class="overview-box">
6
    <h3>📋 Компоненты решения</h3>
7
    <table class="info-table">
8
      <thead>
9
        <tr>
10
          <th>Компонент</th>
11
          <th>Функция</th>
12
          <th>Путь</th>
13
        </tr>
14
      </thead>
15
      <tbody>
16
        <tr>
17
          <td><code>Backup-HyperV.ps1</code></td>
18
          <td>Экспорт ВМ, ротация бэкапов, логирование</td>
19
          <td><code>C:\Scripts\</code></td>
20
        </tr>
21
        <tr>
22
          <td><code>Setup-BackupTask.ps1</code></td>
23
          <td>Автоматическая регистрация задачи в Планировщике</td>
24
          <td><code>C:\Scripts\</code></td>
25
        </tr>
26
        <tr>
27
          <td>Планировщик заданий</td>
28
          <td>Запуск каждую неделю в воскресенье в 03:00</td>
29
          <td><code>taskschd.msc</code></td>
30
        </tr>
31
      </tbody>
32
    </table>
33
  </div>
34
35
  <hr>
36
37
  <h2>🔧 Часть 1: Основной скрипт бэкапа</h2>
38
39
  <h3>1.1 Создайте файл</h3>
40
  <pre class="powershell"><code># Путь для сохранения
41
C:\Scripts\Backup-HyperV.ps1</code></pre>
42
43
  <h3>1.2 Содержимое <code>Backup-HyperV.ps1</code></h3>
44
  <pre class="powershell"><code># ================= НАСТРОЙКИ =================
45
$BackupRootPath = "D:\HyperVBackups"       # Путь, где будут храниться бэкапы
46
$LogFilePath    = "C:\Scripts\BackupLog.txt" # Путь к файлу лога
47
$RetentionDays  = 180                      # Сколько дней хранить бэкапы
48
# =============================================
49
50
# Функция для записи в лог
51
function Write-Log {
52
    param ([string]$Message)
53
    $TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
54
    $LogEntry = "[$TimeStamp] $Message"
55
    Add-Content -Path $LogFilePath -Value $LogEntry
56
    Write-Host $LogEntry
57
}
58
59
# Проверка прав администратора
60
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
61
    Write-Log "ОШИБКА: Скрипт должен быть запущен от имени администратора!"
62
    exit 1
63
}
64
65
# Проверка существования модуля Hyper-V
66
if (-not (Get-Module -ListAvailable -Name Hyper-V)) {
67
    Write-Log "ОШИБКА: Модуль Hyper-V не найден. Убедитесь, что роль установлена."
68
    exit 1
69
}
70
71
# Проверка пути бэкапа
72
if (-not (Test-Path -Path $BackupRootPath)) {
73
    try {
74
        New-Item -ItemType Directory -Path $BackupRootPath -Force | Out-Null
75
        Write-Log "Папка бэкапов создана: $BackupRootPath"
76
    } catch {
77
        Write-Log "ОШИБКА: Не удалось создать папку бэкапов. $_"
78
        exit 1
79
    }
80
}
81
82
$DateStamp = Get-Date -Format "yyyy-MM-dd"
83
Write-Log "=== Начало задачи бэкапа ==="
84
Write-Log "Дата бэкапа: $DateStamp"
85
86
# Получение списка всех ВМ
87
$VMs = Get-VM
88
89
if ($VMs.Count -eq 0) {
90
    Write-Log "Виртуальные машины не найдены."
91
} else {
92
    foreach ($VM in $VMs) {
93
        $VMName = $VM.Name
94
        $ExportPath = "$BackupRootPath\$($VMName)_$DateStamp" 
95
        
96
        try {
97
            Write-Log "Начало экспорта ВМ: $VMName"
98
            Export-VM -Name $VMName -Path $ExportPath -ErrorAction Stop
99
            Write-Log "Успешно: ВМ $VMName экспортирована в $ExportPath"
100
        }
101
        catch {
102
            Write-Log "ОШИБКА экспорта ВМ $VMName : $_"
103
        }
104
    }
105
}
106
107
Write-Log "=== Начало очистки старых бэкапов (старше $RetentionDays дней) ==="
108
109
$BackupFolders = Get-ChildItem -Path $BackupRootPath -Directory
110
$DeletedCount = 0
111
112
foreach ($Folder in $BackupFolders) {
113
    if ($Folder.CreationTime -lt (Get-Date).AddDays(-$RetentionDays)) {
114
        try {
115
            Write-Log "Удаление старой папки: $($Folder.Name) (Дата: $($Folder.CreationTime))"
116
            Remove-Item -Path $Folder.FullName -Recurse -Force
117
            $DeletedCount++
118
        }
119
        catch {
120
            Write-Log "ОШИБКА удаления папки $($Folder.Name): $_"
121
        }
122
    }
123
}
124
125
Write-Log "Удалено папок: $DeletedCount"
126
Write-Log "=== Задача бэкапа завершена ==="
127
Write-Log "-------------------------------------------"</code></pre>
128
129
  <h3>1.3 Разрешите выполнение скриптов</h3>
130
  <pre class="powershell"><code># Разрешите выполнение скриптов (однократно)
131
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
132
133
# Тестовый запуск (от имени Администратора!)
134
C:\Scripts\Backup-HyperV.ps1</code></pre>
135
136
  <div class="success-box">
137
    ✅ <strong>Проверка:</strong> Убедитесь, что папки появились в <code>D:\HyperVBackups\</code> и записи добавлены в <code>C:\Scripts\BackupLog.txt</code>
138
  </div>
139
140
  <hr>
141
142
  <h2>⏰ Часть 2: Настройка Планировщика заданий</h2>
143
  <p>Есть два способа: <strong>ручной</strong> (через GUI) или <strong>автоматический</strong> (через скрипт).</p>
144
145
  <h3>📌 Вариант А: Ручная настройка (через GUI)</h3>
146
  
147
  <ol>
148
    <li>Откройте Планировщик заданий: <code>taskschd.msc</code></li>
149
    <li>В меню справа выберите <strong>Создать задачу...</strong> (не "Создать простую задачу")</li>
150
  </ol>
151
152
  <table class="config-table">
153
    <thead>
154
      <tr>
155
        <th>Вкладка</th>
156
        <th>Параметр</th>
157
        <th>Значение</th>
158
      </tr>
159
    </thead>
160
    <tbody>
161
      <tr>
162
        <td rowspan="4"><strong>Общие</strong></td>
163
        <td>Имя задачи</td>
164
        <td><code>Hyper-V Weekly Backup</code></td>
165
      </tr>
166
      <tr>
167
        <td>Выполнять для</td>
168
        <td>✅ Всех пользователей</td>
169
      </tr>
170
      <tr>
171
        <td>Права</td>
172
        <td>✅ Выполнять с наивысшими правами</td>
173
      </tr>
174
      <tr>
175
        <td>ОС</td>
176
        <td>Windows 10/Server 2016 и выше</td>
177
      </tr>
178
      <tr>
179
        <td rowspan="4"><strong>Триггеры</strong></td>
180
        <td>Начало задачи</td>
181
        <td>По расписанию</td>
182
      </tr>
183
      <tr>
184
        <td>Частота</td>
185
        <td>Еженедельно</td>
186
      </tr>
187
      <tr>
188
        <td>Дни недели</td>
189
        <td>Воскресенье</td>
190
      </tr>
191
      <tr>
192
        <td>Время</td>
193
        <td>03:00:00</td>
194
      </tr>
195
      <tr>
196
        <td rowspan="2"><strong>Действия</strong></td>
197
        <td>Программа</td>
198
        <td><code>powershell.exe</code></td>
199
      </tr>
200
      <tr>
201
        <td>Аргументы</td>
202
        <td><code>-ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\Scripts\Backup-HyperV.ps1"</code></td>
203
      </tr>
204
      <tr>
205
        <td rowspan="2"><strong>Условия</strong></td>
206
        <td>Питание</td>
207
        <td>❌ Снять "Запускать только от электросети"</td>
208
      </tr>
209
      <tr>
210
        <td>Сеть</td>
211
        <td>❌ Снять "Запускать только при входе пользователя"</td>
212
      </tr>
213
    </tbody>
214
  </table>
215
216
  <h3>🤖 Вариант Б: Автоматическая настройка (через скрипт)</h3>
217
218
  <h4>Шаг 1: Создайте файл настройки</h4>
219
  <pre class="powershell"><code># Путь для сохранения
220
C:\Scripts\Setup-BackupTask.ps1</code></pre>
221
222
  <h4>Шаг 2: Вставьте код</h4>
223
  <pre class="powershell"><code># ==============================================================================
224
# Скрипт создания задачи Планировщика для бэкапа Hyper-V
225
# Запускать СТРОГО от имени Администратора
226
# ==============================================================================
227
228
# --- НАСТРОЙКИ ---
229
$TaskName       = "Hyper-V Weekly Backup"
230
$TaskPath       = "\" 
231
$ScriptPath     = "C:\Scripts\Backup-HyperV.ps1"
232
$LogPath        = "C:\Scripts\TaskCreation.log"
233
$RunAsUser      = "SYSTEM"
234
# -----------------
235
236
function Write-Log {
237
    param ([string]$Message)
238
    $TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
239
    $LogEntry = "[$TimeStamp] $Message"
240
    Add-Content -Path $LogPath -Value $LogEntry
241
    Write-Host $LogEntry -ForegroundColor Cyan
242
}
243
244
Write-Log "=== Начало настройки задачи планировщика ==="
245
246
# 1. Проверка прав администратора
247
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
248
    Write-Log "ОШИБКА: Скрипт должен быть запущен от имени администратора!"
249
    exit 1
250
}
251
252
# 2. Проверка существования скрипта бэкапа
253
if (-not (Test-Path -Path $ScriptPath)) {
254
    Write-Log "ОШИБКА: Скрипт бэкапа не найден по пути: $ScriptPath"
255
    exit 1
256
}
257
258
# 3. Проверка модуля Планировщика
259
try {
260
    $ScheduleService = New-Object -ComObject "Schedule.Service"
261
    $ScheduleService.Connect()
262
    Write-Log "Подключение к службе Планировщика заданий успешно."
263
} catch {
264
    Write-Log "ОШИБКА: Не удалось подключиться к службе Планировщика. $_"
265
    exit 1
266
}
267
268
# 4. Удаление старой задачи (если существует)
269
$RootFolder = $ScheduleService.GetFolder($TaskPath)
270
try {
271
    $OldTask = $RootFolder.GetTask($TaskName)
272
    if ($OldTask) {
273
        Write-Log "Обнаружена существующая задача '$TaskName'. Удаление..."
274
        $RootFolder.DeleteTask($TaskName, 0)
275
        Start-Sleep -Seconds 1
276
    }
277
} catch { }
278
279
# 5. Создание новой задачи
280
Write-Log "Создание новой задачи '$TaskName'..."
281
$TaskDefinition = $ScheduleService.NewTask(0)
282
283
# --- Principals ---
284
$TaskDefinition.Principal.UserId = $RunAsUser
285
$TaskDefinition.Principal.RunLevel = 1
286
$TaskDefinition.Principal.LogonType = 3
287
Write-Log "Настроено: Запуск от $RunAsUser с наивысшими правами."
288
289
# --- Triggers ---
290
$Trigger = $TaskDefinition.Triggers.Create(1)
291
$Trigger.StartBoundary = (Get-Date).Date.AddHours(3).ToString("yyyy-MM-ddTHH:mm:ss")
292
$Trigger.DaysOfWeek = 64
293
$Trigger.WeeksInterval = 1
294
$Trigger.Enabled = $true
295
Write-Log "Настроено: Запуск каждое Воскресенье в 03:00."
296
297
# --- Actions ---
298
$Action = $TaskDefinition.Actions.Create(0)
299
$Action.Path = "powershell.exe"
300
$Action.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
301
$Action.WorkingDirectory = "C:\Scripts"
302
Write-Log "Настроено: Запуск PowerShell скрипта $ScriptPath"
303
304
# --- Settings ---
305
$TaskDefinition.Settings.AllowHardTerminate = $true
306
$TaskDefinition.Settings.StartWhenAvailable = $true
307
$TaskDefinition.Settings.StopIfGoingOnBatteries = $false
308
$TaskDefinition.Settings.IdleSettings.StopOnIdleEnd = $false
309
$TaskDefinition.Settings.ExecutionTimeLimit = "PT12H"
310
Write-Log "Настроено: Лимит времени 12ч, работа от ИБП разрешена."
311
312
# --- Регистрация ---
313
try {
314
    $RootFolder.RegisterTaskDefinition($TaskName, $TaskDefinition, 6, $RunAsUser, $null, 3)
315
    Write-Log "УСПЕШНО: Задача '$TaskName' создана и зарегистрирована."
316
} catch {
317
    Write-Log "ОШИБКА при регистрации задачи: $_"
318
    exit 1
319
}
320
321
# 6. Тестовый запуск
322
Write-Log "Проверка статуса задачи..."
323
try {
324
    $CreatedTask = $RootFolder.GetTask($TaskName)
325
    Write-Log "Статус задачи: $($CreatedTask.State)"
326
    
327
    Write-Host "`nХотите запустить тестовое выполнение задачи? (Y/N)" -ForegroundColor Yellow
328
    $response = Read-Host
329
    
330
    if ($response -eq 'Y' -or $response -eq 'y') {
331
        Write-Log "Запуск задачи на выполнение..."
332
        $CreatedTask.Run("")
333
        Write-Log "Задача запущена. Проверьте логи бэкапа через несколько минут."
334
    }
335
} catch {
336
    Write-Log "Не удалось получить доступ к созданной задаче: $_"
337
}
338
339
Write-Log "=== Настройка завершена ==="</code></pre>
340
341
  <h4>Шаг 3: Запустите скрипт настройки</h4>
342
  <pre class="powershell"><code># Откройте PowerShell от имени администратора
343
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
344
C:\Scripts\Setup-BackupTask.ps1</code></pre>
345
346
  <hr>
347
348
  <h2>⚠️ Часть 3: Важные замечания и рекомендации</h2>
349
350
  <div class="warning-box">
351
    <h3>💿 Дисковое пространство</h3>
352
    <p>Бэкап через <code>Export-VM</code> создает полные копии дисков (VHDX).</p>
353
    <p><strong>Формула расчёта:</strong> <code>Размер_ВМ × Количество_ВМ × (180 дней / 7 дней)</code></p>
354
    <p><strong>Пример:</strong> 5 ВМ по 100 ГБ × 26 недель = <strong>13 ТБ</strong> за 180 дней</p>
355
    <p>✅ Убедитесь, что на диске <code>D:</code> достаточно места. При ограничении рассмотрите:</p>
356
    <ul>
357
      <li>Сокращение срока хранения (<code>$RetentionDays</code>)</li>
358
      <li>Использование дедупликации</li>
359
      <li>Выгрузку старых бэкапов в облако</li>
360
    </ul>
361
  </div>
362
363
  <div class="info-box">
364
    <h3>⚡ Производительность</h3>
365
    <ul>
366
      <li><code>Export-VM</code> создает временный чекпоинт на работающей ВМ для целостности данных</li>
367
      <li>Это может немного замедлить ВМ во время бэкапа</li>
368
      <li>Время бэкапа зависит от скорости диска и объема данных</li>
369
      <li>Задания не накладываются (запуск раз в неделю)</li>
370
    </ul>
371
  </div>
372
373
  <div class="tip-box">
374
    <h3>🧪 Тестирование</h3>
375
    <ul>
376
      <li>Не ждите воскресенья — запустите скрипт вручную один раз</li>
377
      <li>Проверьте, что ВМ экспортируются и лог пишется</li>
378
      <li>Для проверки удаления: временно установите <code>$RetentionDays = 0</code>, затем верните <code>180</code></li>
379
    </ul>
380
  </div>
381
382
  <div class="success-box">
383
    <h3>🔄 Восстановление</h3>
384
    <pre class="powershell"><code># Через PowerShell
385
Import-VM -Path "D:\HyperVBackups\VMName_2023-10-29\VMName\Virtual Machines\GUID.xml"
386
387
# Или через диспетчер Hyper-V → Импорт виртуальной машины</code></pre>
388
  </div>
389
390
  <div class="info-box">
391
    <h3>📧 Уведомления</h3>
392
    <p>В данном скрипте уведомления только в лог-файл. Для email-уведомлений добавьте в скрипт:</p>
393
    <pre class="powershell"><code>Send-MailMessage -From "backup@domain.com" -To "admin@domain.com" `
394
    -Subject "Hyper-V Backup Status" -Body $LogContent -SmtpServer "smtp.domain.com"</code></pre>
395
  </div>
396
397
  <hr>
398
399
  <h2>✅ Часть 4: Проверка результата</h2>
400
401
  <ol>
402
    <li>Откройте Планировщик заданий: <code>taskschd.msc</code></li>
403
    <li>Найдите задачу <strong>Hyper-V Weekly Backup</strong></li>
404
    <li>ПКМ → <strong>Свойства</strong></li>
405
    <li>Проверьте вкладку <strong>Общие</strong>: ✅ "Выполнять с наивысшими правами"</li>
406
    <li>Включите вкладку <strong>Журнал</strong> (если отключена) для истории запусков</li>
407
  </ol>
408
409
  <h3>📊 Полезные команды для проверки</h3>
410
  <pre class="powershell"><code># Просмотр логов бэкапа
411
Get-Content C:\Scripts\BackupLog.txt -Tail 50
412
413
# Просмотр логов создания задачи
414
Get-Content C:\Scripts\TaskCreation.log -Tail 50
415
416
# Проверка статуса задачи
417
Get-ScheduledTask -TaskName "Hyper-V Weekly Backup"
418
419
# Запуск задачи вручную
420
Start-ScheduledTask -TaskName "Hyper-V Weekly Backup"
421
422
# Просмотр истории выполнения
423
Get-ScheduledTaskInfo -TaskName "Hyper-V Weekly Backup"
424
425
# Проверка папок бэкапа
426
Get-ChildItem D:\HyperVBackups -Directory | Sort-Object CreationTime -Descending | Select-Object -First 10</code></pre>
427
428
  <h3>📁 Структура после настройки</h3>
429
  <pre><code>C:\Scripts\
430
├── Backup-HyperV.ps1          # Основной скрипт бэкапа
431
├── Setup-BackupTask.ps1       # Скрипт настройки задачи
432
├── BackupLog.txt              # Лог выполнения бэкапов
433
└── TaskCreation.log           # Лог создания задачи
434
435
D:\HyperVBackups\
436
├── VM1_2026-06-15\
437
│   └── VM1\
438
│       ├── Virtual Machines\
439
│       └── ...
440
├── VM2_2026-06-15\
441
└── ...</code></pre>
442
443
  <div class="warning-box">
444
    <h3>🔐 Требования безопасности</h3>
445
    <ul>
446
      <li>Оба скрипта требуют запуска <strong>от имени Администратора</strong></li>
447
      <li>Задача выполняется от <strong>SYSTEM</strong> (наиболее надежно для серверов)</li>
448
      <li>Тип входа <strong>S4U</strong> — задача работает без входа пользователя</li>
449
      <li>Храните скрипты в защищенной папке с ограниченным доступом</li>
450
    </ul>
451
  </div>
452
</div>
453
454
<style>
455
.hyperv-backup-guide {
456
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
457
  line-height: 1.6;
458
  color: #2c3e50;
459
  max-width: 100%;
460
  font-size: 14px;
461
}
462
.hyperv-backup-guide h2 {
463
  color: #1a5276;
464
  border-bottom: 3px solid #3498db;
465
  padding-bottom: 0.6em;
466
  margin: 1.5em 0 1em;
467
  font-size: 1.4em;
468
}
469
.hyperv-backup-guide h3 {
470
  color: #2874a6;
471
  margin: 1.3em 0 0.7em;
472
  background: #ebf5fb;
473
  padding: 0.6em 0.9em;
474
  border-left: 4px solid #3498db;
475
  border-radius: 0 4px 4px 0;
476
  font-size: 1.15em;
477
}
478
.hyperv-backup-guide h4 {
479
  color: #34495e;
480
  margin: 1em 0 0.5em;
481
  font-size: 1.05em;
482
}
483
.hyperv-backup-guide p { margin: 0.6em 0; }
484
.hyperv-backup-guide hr {
485
  border: none;
486
  border-top: 2px solid #ecf0f1;
487
  margin: 1.5em 0;
488
}
489
.hyperv-backup-guide pre {
490
  background: #1e1e1e;
491
  color: #d4d4d4;
492
  padding: 1em;
493
  border-radius: 5px;
494
  overflow-x: auto;
495
  font-size: 0.85em;
496
  margin: 0.7em 0;
497
  border: 1px solid #3a3a3a;
498
}
499
.hyperv-backup-guide code {
500
  font-family: "Consolas", "Monaco", "Courier New", monospace;
501
  background: #f4f6f7;
502
  padding: 0.2em 0.4em;
503
  border-radius: 3px;
504
  font-size: 0.9em;
505
  color: #e74c3c;
506
}
507
.hyperv-backup-guide pre code {
508
  background: none;
509
  padding: 0;
510
  color: #d4d4d4;
511
}
512
.hyperv-backup-guide pre.powershell::before {
513
  content: "🪟 PowerShell";
514
  display: block;
515
  background: #5c2d91;
516
  color: #fff;
517
  padding: 0.25em 0.6em;
518
  margin: -1em -1em 0.8em -1em;
519
  font-size: 0.75em;
520
  border-radius: 4px 4px 0 0;
521
  font-weight: 600;
522
}
523
.hyperv-backup-guide table {
524
  width: 100%;
525
  border-collapse: collapse;
526
  margin: 0.8em 0;
527
  font-size: 0.9em;
528
}
529
.hyperv-backup-guide th,
530
.hyperv-backup-guide td {
531
  border: 1px solid #bdc3c7;
532
  padding: 0.6em 0.8em;
533
  text-align: left;
534
}
535
.hyperv-backup-guide th {
536
  background: #3498db;
537
  color: #fff;
538
  font-weight: 600;
539
}
540
.hyperv-backup-guide tr:nth-child(even) {
541
  background: #f8f9fa;
542
}
543
.hyperv-backup-guide ul, .hyperv-backup-guide ol {
544
  padding-left: 1.6em;
545
  margin: 0.5em 0;
546
}
547
.hyperv-backup-guide li { margin: 0.35em 0; }
548
.hyperv-backup-guide .overview-box,
549
.hyperv-backup-guide .info-box,
550
.hyperv-backup-guide .warning-box,
551
.hyperv-backup-guide .tip-box,
552
.hyperv-backup-guide .success-box {
553
  padding: 1em 1.2em;
554
  margin: 1em 0;
555
  border-radius: 0 5px 5px 0;
556
  border-left: 4px solid;
557
}
558
.hyperv-backup-guide .overview-box {
559
  background: #e8f4fd;
560
  border-color: #3498db;
561
}
562
.hyperv-backup-guide .info-box {
563
  background: #f0f7ff;
564
  border-color: #17a2b8;
565
}
566
.hyperv-backup-guide .warning-box {
567
  background: #fff3cd;
568
  border-color: #f39c12;
569
}
570
.hyperv-backup-guide .tip-box {
571
  background: #e8f8f5;
572
  border-color: #27ae60;
573
}
574
.hyperv-backup-guide .success-box {
575
  background: #e6f7ee;
576
  border-color: #27ae60;
577
}
578
.hyperv-backup-guide .config-table td:nth-child(1) {
579
  font-weight: 600;
580
  background: #f8f9fa;
581
}
582
@media (max-width: 768px) {
583
  .hyperv-backup-guide { font-size: 13px; }
584
  .hyperv-backup-guide pre { font-size: 0.8em; }
585
  .hyperv-backup-guide table { font-size: 0.85em; }
586
}
587
</style>