-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGet-HVReplicaReport.ps1
399 lines (328 loc) · 15.2 KB
/
Get-HVReplicaReport.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
#Requires -Version 5.1
#Requires -Modules Hyper-V
param (
[string]$ReportFilePath = 'c:\temp\ReplicaReport.html',
[int]$MaxReportAgeInMinutes = 60,
[switch]$SkipSettingsCheck
)
###########################
#region Function Definitions
#############################
function Test-VMReplicaSettingsMatch {
param (
[Parameter(Mandatory = $true)]
[string]$VMName,
[Parameter(Mandatory = $true)]
[string]$PrimaryHost,
[Parameter(Mandatory = $true)]
[string]$ReplicaHost
)
# Get settings of primary VM (on PrimaryHost)
$VM1 = Get-VM -ComputerName $PrimaryHost -Name $VMName
# Get detailed information about the VM's memory, cpu.
$VM1Memory = Get-VMMemory -VM $VM1
$VM1CPU = Get-VMProcessor -VM $VM1
# Use Get-VHD to gather information about all VHDs or VHDXs attached to the VM
$VM1HardDrives = Get-VHD -VMId $VM1.VMId -ComputerName $PrimaryHost
# Get a count of SCSI controllers attached to the VM
$VM1SCSIControllers = Get-VMScsiController -VM $VM1
# Combine the returned objects into a single object
$VM1Settings = New-Object -TypeName PSObject -Property @{
VMName = $VM1.Name
MemoryStartup = $VM1Memory.Startup
MemoryMinimum = $VM1Memory.Minimum
MemoryMaximum = $VM1Memory.Maximum
CPUCount = $VM1CPU.Count
HardDriveCount = $VM1HardDrives.Count
HardDriveSize = $VM1HardDrives.Size
SCSIControllers = $VM1SCSIControllers | Select-Object -Property Name, Drives
}
# Get settings of replica VM (on ReplicaHost)
$VM2 = Get-VM -ComputerName $ReplicaHost -Name $VMName
# Get detailed information about the VM's memory, cpu.
$VM2Memory = Get-VMMemory -VM $VM2
$VM2CPU = Get-VMProcessor -VM $VM2
# Use Get-VHD to gather information about all VHDs or VHDXs attached to the VM
$VM2HardDrives = Get-VHD -VMId $VM2.VMId -ComputerName $ReplicaHost
# Get a count of SCSI controllers attached to the VM
$VM2SCSIControllers = Get-VMScsiController -VM $VM2
# Combine the returned objects into a single object
$VM2Settings = New-Object -TypeName PSObject -Property @{
VMName = $VM2.Name
MemoryStartup = $VM2Memory.Startup
MemoryMinimum = $VM2Memory.Minimum
MemoryMaximum = $VM2Memory.Maximum
CPUCount = $VM2CPU.Count
HardDriveCount = $VM2HardDrives.Count
HardDriveSize = $VM2HardDrives.Size
SCSIControllers = $VM2SCSIControllers | Select-Object -Property Name, Drives
}
# Convert the VM settings objects to JSON and compare them
$VM1Json = $VM1Settings | ConvertTo-Json
$VM2Json = $VM2Settings | ConvertTo-Json
# Compare the settings of VM1 and VM2 returning True if the match and False if they don't.
return !(Compare-Object $VM1Json $VM2Json)
}
##############################
#endregion Function Definitions
################################
#################
# Main Entry Point
###################
# Set default error action
$ErrorActionPreference = 'Stop'
# Load list of Hyper-V Host Servers from settings file
Write-Host ('Loading list of Hyper-V Host Servers from settings file...')
$settingsPath = Join-Path -Path $PSScriptRoot -ChildPath 'settings.json'
# If the settings file doesn't exist, abort script with error
if (!(Test-Path -Path $settingsPath)) {
Write-Error ('Settings.json file not found at: {0}. See ExampleSettings.json.' -f $settingsPath)
exit
}
$jsonContent = Get-Content -Path $settingsPath -Raw
$settings = ConvertFrom-Json -InputObject $jsonContent
$hvHosts = $settings.hvHosts
Write-Host 'List of host servers loaded from settings file:'
$hvHosts | ForEach-Object { Write-Host ('- {0}' -f $_) }
# Retrieve virtual machine replication information from Hyper-V hosts
# Filter the output to only include virtual machine replications that have a RelationshipType other than 'Extended'
Write-Host ('Retrieving virtual machine replication information from {0} Hyper-V hosts...' -f $hvHosts.Count)
$repInfo = Get-VMReplication -ComputerName $hvHosts | Where-Object { $_.RelationshipType -ne 'Extended' }
Write-Host 'Generating report...'
# Sort the Replication info output by Name and Mode
# Convert the output to HTML and specify the properties to include in the table
$repInfoHTML = $repInfo | Sort-Object Name, Mode | ConvertTo-Html -Fragment -Property Name, Mode, PrimaryServer, ReplicaServer, State, Health, FrequencySec, RelationshipType -PreContent '<div id="ReplicationTable">' -PostContent '</div>'
if (!$SkipSettingsCheck) {
Write-Host 'Performing VM settings check...'
$namesOfVMsWithReplicas = ($repInfo | Where-Object { $_.ReplicationMode -like '*Replica' } | Select-Object -ExpandProperty Name -Unique | Sort-Object)
$primaryVMs = $repInfo | Where-Object { $_.ReplicationMode -eq 'Primary' }
$replicaVMs = $repInfo | Where-Object { $_.ReplicationMode -eq 'Replica' }
$extendedReplicaVMs = $repInfo | Where-Object { $_.ReplicationMode -eq 'ExtendedReplica' }
# Create an empty array to store the results of the comparison
$replicaSettingsMatch = @()
# Loop through each VM that has a replica
foreach ($vmName in $namesOfVMsWithReplicas) {
Write-Host ('Checking replica settings for VM: {0}' -f $vmName)
# Get the primary and replica VM objects
$primaryVM = $primaryVMs | Where-Object { $_.Name -eq $vmName }
$replicaVM = $replicaVMs | Where-Object { $_.Name -eq $vmName }
# More than one ReplicaVM returned?
if ($replicaVM.Count -gt 1) {
Write-Host ('* More than one replica VM returned for VM: {0}. Is there an Extended Replica initialization in progress?' -f $vmName)
# Select only the first ReplicaVM returned
$replicaVM = $replicaVM | Select-Object -First 1
Write-Host ('* Comparing only the first replica VM returned (Host: {0}) for VM: {1}' -f $replicaVM.ReplicaServer, $vmName)
}
# Test the settings of the primary and replica VMs to see if they match
$settingsMatch = Test-VMReplicaSettingsMatch -VMName $vmName -PrimaryHost $primaryVM.PrimaryServer -ReplicaHost $replicaVM.ReplicaServer
# Create a custom object to store the results of the comparison
$replicaSettingsMatch += New-Object -TypeName PSObject -Property @{
VMName = $vmName
SettingsMatch = $settingsMatch
}
}
# Create an empty array to store the results of the comparison
$extendedReplicaSettingsMatch = @()
# Loop through each VM that has an extended replica
foreach ($vmName in $namesOfVMsWithReplicas) {
# Get the primary and replica VM objects
$primaryVM = $primaryVMs | Where-Object { $_.Name -eq $vmName }
$extendedReplicaVM = $extendedReplicaVMs | Where-Object { $_.Name -eq $vmName }
# If the extended replica is not found, skip to the next VM
if ($null -eq $extendedReplicaVM) {
continue
}
Write-Host ('Checking extended replica settings for VM: {0}' -f $vmName)
# Test the settings of the primary and replica VMs to see if they match
$settingsMatch = Test-VMReplicaSettingsMatch -VMName $vmName -PrimaryHost $primaryVM.PrimaryServer -ReplicaHost $extendedReplicaVM.ReplicaServer
# Create a custom object to store the results of the comparison
$extendedReplicaSettingsMatch += New-Object -TypeName PSObject -Property @{
VMName = $vmName
SettingsMatch = $settingsMatch
}
}
# Use namesOfVMsWithReplicas, replicaSettingsMatch and extendedReplicaSettingsMatch to build an array of objects containing entries for each named VM with a replica.
# The object will contain the VM name, the replica mode, and the result of the settings comparisons.
$replicaReport = @()
foreach ($vmName in $namesOfVMsWithReplicas) {
$replicaReport += New-Object -TypeName PSObject -Property @{
Name = $vmName
ReplicaSettingsMatch = $replicaSettingsMatch | Where-Object { $_.VMName -eq $vmName } | Select-Object -ExpandProperty SettingsMatch
ExtendedReplicaSettingsMatch = $extendedReplicaSettingsMatch | Where-Object { $_.VMName -eq $vmName } | Select-Object -ExpandProperty SettingsMatch
}
}
# Convert the replicaReport array to HTML and specify the properties to include in the table
$replicaReportHTML = $replicaReport | Sort-Object Name | ConvertTo-Html -Fragment -Property Name, ReplicaSettingsMatch, ExtendedReplicaSettingsMatch -PreContent '<div id="SettingsMatchTable">' -PostContent '</div>'
# Combine $replicaReportHTML and $repInfoHTML into a single HTML
$repInfoHTML = $repInfoHTML + '<br />' + $replicaReportHTML
}
# Append time and date stamp to report.
Write-Host 'Appending time and date stamp to report...'
$repInfoHTML = $repInfoHTML + ('<div id="dateStamp">Report created on {0}, at {1}</div>' -f (Get-Date).ToString('MMM dd, yyyy'), (Get-Date).ToString('h:mm:ss tt'))
# Define the HTML header.
$htmlHeader = @"
<style>
body
{
font-family: 'Hack', monospace;
}
table
{
border-width: 1px;
border-style: solid;
border-color: black;
border-collapse: collapse;
}
th
{
border-width: 1px;
padding: 3px;
border-style: solid;
border-color: black;
background-color: #6495ED;
}
td
{
border-width: 1px;
padding: 3px;
border-style: solid;
border-color: black;
}
tbody tr:nth-child(odd)
{
background-color: lightgray;
color: black;
}
div#dateStamp
{
font-size: 12px;
font-style: italic;
font-weight: normal;
margin-top: 6px;
}
</style>
"@
# Define the HTML footer that contains JS scripts.
$htmlFooter = @"
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
// Get all <td> elements in the document
const tds = document.getElementsByTagName('td');
const tdsLength = tds.length;
// Loop through each <td> element
for (let i = 0; i < tdsLength; i++) {
const textContent = tds[i].textContent;
if (textContent === 'False') {
tds[i].style.color = 'red';
} else if (textContent === 'True') {
tds[i].style.color = 'green';
}
}
// Select the table within the element with id 'ReplicationTable'
const table = document.querySelector('#ReplicationTable table');
if (table) {
// Get all <th> elements (table headers) in the table
const headers = table.getElementsByTagName('th');
let healthColumnIndex = -1;
// Loop through the headers to find the 'Health' column
Array.from(headers).forEach((header, index) => {
if (header.textContent.trim() === 'Health') {
healthColumnIndex = index;
}
});
// If the 'Health' column is found
if (healthColumnIndex !== -1) {
// Get all rows in the table
const rows = table.getElementsByTagName('tr');
const rowsLength = rows.length;
// Loop through each row, starting from 1 to skip the header row
for (let i = 1; i < rowsLength; i++) {
const cells = rows[i].getElementsByTagName('td');
const healthCell = cells[healthColumnIndex];
if (healthCell) {
const healthText = healthCell.textContent.trim();
healthCell.style.color = healthText === 'Normal' ? 'green' : 'red';
}
}
}
}
// Get all table rows
const tableRows = document.querySelectorAll('table tr');
// Add mouseover and mouseout event listeners to each row
tableRows.forEach(row => {
row.addEventListener('mouseover', () => {
const firstCell = row.cells[0];
const valueToMatch = firstCell.textContent;
tableRows.forEach(otherRow => {
if (otherRow === row) {
// Highlight the row being hovered over
row.style.backgroundColor = '#FBF719';
} else if (otherRow.cells[0].textContent === valueToMatch) {
// Highlight rows with matching first cell content
otherRow.style.backgroundColor = '#E1DE16';
}
});
});
row.addEventListener('mouseout', () => {
// Remove background color when mouse leaves the row
tableRows.forEach(otherRow => {
otherRow.style.backgroundColor = '';
});
});
});
// Get the text content of the dateStamp div
const dateStampText = document.getElementById('dateStamp').textContent;
// Extract the date and time part from the text
const dateTimeString = dateStampText.match(/on (.+), at (.+)/);
const dateString = dateTimeString[1];
const timeString = dateTimeString[2];
// Combine date and time into a single string
const fullDateTimeString = ```${dateString} `${timeString}``;
// Parse the extracted date and time into a Date object
const reportDate = new Date(fullDateTimeString);
// Get the current date and time
const currentDate = new Date();
// Calculate the difference in milliseconds
const timeDifference = currentDate - reportDate;
// Convert the difference to minutes
const timeDifferenceInMinutes = timeDifference / (1000 * 60);
// Check if the report date is more than the specified number of minutes ago
if (timeDifferenceInMinutes > $MaxReportAgeInMinutes) {
// Move the div element to the top of the body
const dateStampDiv = document.getElementById('dateStamp');
document.body.insertBefore(dateStampDiv, document.body.firstChild);
// Style the div element
dateStampDiv.style.fontSize = '2em';
dateStampDiv.style.fontWeight = 'bold';
dateStampDiv.style.color = 'red';
// Create a new div element for the additional message
const warningDiv = document.createElement('div');
warningDiv.style.color = 'black';
warningDiv.style.fontSize = '1.5em';
warningDiv.style.fontStyle = 'italic';
warningDiv.textContent = 'Report may be out of date, please confirm!';
// Insert the new div element after the dateStamp div
dateStampDiv.insertAdjacentElement('afterend', warningDiv);
console.log('The report date is more than $MaxReportAgeInMinutes minutes ago.');
} else {
console.log('The report date is within the last $MaxReportAgeInMinutes minutes.');
}
});
</script>
"@
# Combine the HTML header, footer and the report HTML into a single HTML document.
$htmlTemplate = @"
<html>
<head>
$htmlHeader
</head>
<body>
$repInfoHTML
</body>
$htmlFooter
</html>
"@
# Write the HTML to a file.
Write-Host ('Writing the HTML report file to: {0}' -f $ReportFilePath)
$htmlTemplate | Out-File $ReportFilePath
Write-Host 'Report generation completed.'