-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathxVMSwitch.psm1
565 lines (467 loc) · 22.8 KB
/
xVMSwitch.psm1
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
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
enum Ensure
{
Absent
Present
}
enum SwitchType
{
External
Internal
Private
}
enum MinimumBandwidthMode
{
None
Default
Weight
Absolute
}
enum AllowDisruptiveAction
{
Never
IfNeeeded
OnlyIfUnused
}
enum ConfigurationMode
{
ApplyOnly
ApplyAndMonitor
ApplyAndAutoCorrect
}
enum TrackingMethod
{
Guid
Name
}
# TODO
# Localized Data
# Probably changing remaining boolean properties to [Nullable[Boolean]]
# TIDY UP CODE!!
[DscResource()]
class xVMSwitch
{
[DscProperty(Key)]
[ValidateNotNullOrEmpty()]
[System.String]$Name
[DscProperty()]
[Ensure]$Ensure = [Ensure]::Present
[DscProperty()]
[SwitchType]$SwitchType
[DscProperty()]
[ValidateNotNullOrEmpty()]
[System.String]$NetAdapterName
[DscProperty()]
[System.Nullable[System.Boolean]]$AllowManagementOS
[DscProperty()]
[System.Boolean]$IovEnabled
[DscProperty()]
[MinimumBandwidthMode]$MinimumBandwidthMode
[DscProperty()]
[ValidateNotNullOrEmpty()]
[System.String]$Notes
[DscProperty()]
[System.Int64]$DefaultFlowMinimumBandwidthAbsolute
[DscProperty()]
[System.Int64]$DefaultFlowMinimumBandwidthWeight
[DscProperty()]
[AllowDisruptiveAction]$AllowDisruptiveAction = 'OnlyIfUnused'
[DscProperty()]
[System.Boolean]$AllowNameDrift
[DscProperty()]
[TrackingMethod]$TrackingMethod = 'Guid'
[DscProperty(NotConfigurable)]
[System.String]$SwitchUniqueId
[DscProperty(NotConfigurable)]
[System.UInt32]$ReferencedVMs
[DscProperty()]
[ConfigurationMode]$ConfigurationMode
[DscProperty()]
[System.UInt32]$ConfigurationModeFrequencyMins
# 'Private' (aka hidden) fields follow camel case notation and declared as hidden
hidden [System.Boolean] $hasRun
hidden [System.DateTime] $lastExecution
hidden [System.DateTime] $startExecution
hidden [System.String[]] $setOperations
hidden [System.String] $actualName
hidden [System.String] $configFile
hidden [System.Management.Automation.PSObject] $localizedData # Not implemented
xVMSwitch()
{
# Get start time
$this.startExecution = [DateTime]::Now
# Attempt to load localized data, if it fails, it will use the default localized data hardcoded on the script
Import-LocalizedData -BindingVariable $script:LocalizedData -ErrorAction Ignore # Not implemented
}
[xVMSwitch] Get()
{
$configuration = $this.GetConfiguration()
foreach ($variableName in ($configuration | Get-Member -MemberType NoteProperty).Name)
{
$this.$variableName = $configuration.$variableName
}
return this;
}
[System.Boolean] Test()
{
Write-Verbose "Starting Test()"
# ValidateParameters() doesn't have a return value. If the validation fails it throws a terminating error
# and we simply don't catch it, so it gets thrown back to the caller (LCM in this case)
$this.ValidateParameters()
$this.LoadConfiguration()
# If it should not run, return $true so LCM does not call Set().
if (-not $this.ShouldRun())
{
return $true
}
$this.setOperations = $this.TestConfiguration()
$this.SaveConfiguration('Test')
# Set to apply and monitor, not apply and correct so don't do anything else now.
if ($this.ConfigurationMode -eq [ConfigurationMode]::ApplyAndMonitor)
{
Write-Verbose "Set to ApplyAndMonitor so will prevent Set() from being called."
return $true
}
return (-not $this.setOperations)
}
[void] Set()
{
Write-Verbose "Starting Set()"
$this.LoadConfiguration()
foreach ($setOperation in $this.setOperations)
{
Write-Verbose "Processing operationg $setOperation"
switch -regex ($setOperation)
{
'^(Delete|Create|Rename)Object$' { $this.$setOperation(); break }
'^Set' { $this.SetObjectProperty($setOperation, 'Set') }
}
}
# Even though calling SetObjectProperty() once per SetProperty is slower and more taxing, it does give much greater flexibility.
# You can find more information regarding my choice and why it provides more flexibility on www.faustonascimento.com
$this.SaveConfiguration('Set')
}
[void] SetObjectProperty([System.String]$operationName, [System.String]$keyword)
{
$propertyName = $operationName.SubString($keyword.Length)
Write-Verbose "Preparing to set property $propertyName"
$cmdletParameters = @{ $propertyName = $this.$propertyName }
Set-VMSwitch -Name $($this.actualName) -ErrorAction Stop @cmdletParameters
}
[void] RenameObject()
{
Write-Verbose "Renaming switch $($this.actualName) to $($this.Name)"
Rename-VMSwitch -Name $this.actualName -NewName $this.Name -ErrorAction Stop
}
[void] DeleteObject()
{
Write-Verbose "Deelting object with name $($this.actualName)"
Remove-VMSwitch -Name $this.actualName -ErrorAction Stop
}
[void] CreateObject()
{
Write-Verbose "Preparing to create switch with name $($this.parameterName)"
$cmdletParameters = @{ }
# List of potential parameters that can be passed to New-VMSwitch
$propertyNames = @('SwitchType', 'NetAdapterName', 'AllowManagementOS', 'MinimumBandwidthMode', 'Notes')
foreach ($propertyName in $propertyNames)
{
if ($this.$propertyName)
{
Write-Verbose "Adding parameter $propertyName to list of parameters to splat"
$cmdletParameters.$propertyName = $this.$propertyName
}
}
New-VMSwitch -Name $($this.Name) -ErrorAction Stop @cmdletParameters
}
# Very simple function that confirms if the configuration for this resource should be run
# Has *nothing* specific to this particular resource, can be easily exported to other modules so long as the variables it relies on exist
[System.Boolean] ShouldRun()
{
Write-Verbose "Determinating if Test() should continue based on ConfigurationMode and ConfigurationModeFrequencyMins values"
# If set to only ApplyOnce and already ran, exit
if ($this.ConfigurationMode -eq [ConfigurationMode]::ApplyOnly -and $this.hasRun)
{
Write-Verbose "Set to apply only (apply once) but have already run this configuration item, will not continue"
return $false
}
# If the ConfigurationModeFrequencyMins is higher than 0 and there is a lastExecution (i.e., not running for first time)
# And the amount of minutes since it last ran is lower than how often is can run, return false
if ($this.ConfigurationModeFrequencyMins -and $this.lastExecution -and ([int] ($this.startExecution - $this.lastRunTime).TotalMinutes) -lt $this.ConfigurationModeFrequencyMins)
{
Write-Verbose "Set to only run once every $($this.ConfigurationModeFrequencyMins) minutes, but last ran $([int] ($this.startExecution - $this.lastRunTime).TotalMinutes) minutes ago, will not continue"
return $false
}
Write-Verbose "Configuration should proceed, continuing with it"
return $true
}
# I find that failing on the very first parameter that is invalid is counter intuitive.
# If the caller had 10 parameters and all were wrong, it would take 10 tries to fix it as only one error would be output per call
# So I think it is better to perform *all* parameter validations. These tests have no impact on how long it takes to run or CPU load
[void] ValidateParameters()
{
Write-Verbose "Validating Parameters"
$results = @()
if ($this.SwitchType -and $this.SwitchType -ne [SwitchType]::External)
{
if ($this.NetAdapterName)
{
$results += 'NetAdapterName can only be set on External switch types.'
}
if ($this.AllowManagementOS)
{
$results += 'AllowManagementOS can only be set on External switch types.'
}
if ($this.IovEnabled)
{
$results += 'IovEnabled can only be set on External switch types.'
}
}
else #If SwitchType -eq 'External' -or -not SwitchType
{
if (-not $this.NetAdapterName)
{
$results += 'For external switch type, NetAdapterName must be specified.'
}
else
{
$netAdapter = Get-NetAdapter -Name $this.NetAdapterName -ErrorAction SilentlyContinue
if (!$netAdapter)
{
$results += "No network adapter with name $this.NetAdapterName exists."
}
}
}
# Weird if statement, but the only way I could find at 5am to ensure that the property MinmumBandwidthMode remains an optional field
if (-not $this.MinimumBandwidthMode -and $this.MinimumBandwidthMode -ne [MinimumBandwidthMode]::Absolute -and $this.MinimumBandwidthMode -ne [MinimumBandwidthMode]::Default -and $this.DefaultFlowMinimumBandwidthAbsolute)
{
$results += 'DefaultFlowMinimumBandwidthAbsolute can only be set on switches with Absolute bandwidth reservation modes.'
}
if ($this.MinimumBandwidthMode -ne [MinimumBandwidthMode]::Width -and $this.DefaultFlowMinimumBandwidthWeight)
{
$results += 'DefaultFlowMinimumBandwidthWeight can only be set on switches with weight bandwidth reservation modes.'
}
if ($this.TrackingMethod -ne [TrackingMethod]::Guid -and $this.AllowNameDrift)
{
$results += 'AllowNameDrift can only be set to $true if TrackingMethod is Guid'
}
if ($results)
{
$results.foreach({ Write-Warning $_ })
$errorRecord = New-ErrorRecord -ErrorMessage "Failed to validate parameters for switch $($this.Name)" -ErrorCategory 'InvalidArgument'
throw $errorRecord
}
}
# Function that returns a PSObject containing the current configuration
# Used by both TestConfiguration() and Get()
[System.Management.Automation.PSObject] GetConfiguration()
{
Write-Verbose "Retrieving switch configuration"
$results = @{ }
if ($this.SwitchUniqueId -and $this.TrackingMethod -eq [TrackingMethod]::Guid)
{
Write-Verbose "Retrieving switch based on Id = $($this.SwitchUniqueId)"
$switch = Get-VMSwitch -Id $this.SwitchUniqueId -ErrorAction SilentlyContinue
}
else
{
Write-Verbose "Retrieving switch based on Name = $($this.Name)"
$switch = Get-VMSwitch -Name $this.Name -ErrorAction SilentlyContinue
if ($switch.Count -gt 1)
{
$errorRecord = New-ErrorRecord -ErrorMessage "Multiple switches with name $($this.Name) found, this is only supported in this resource if TrackingMethod is set to Guid and the Guid is known" -ErrorCategory 'InvalidData'
throw $errorRecord
}
}
# Invert if to simplify reading since the else clause is much bigger
if (-not $switch)
{
Write-Verbose "Switch not found"
$results.Ensure = [Ensure]::Absent
}
else
{
Write-Verbose "Found Switch"
# Preserve the Switch Unique Id
$this.SwitchUniqueId = $switch.Id
$results.Ensure = [Ensure]::Present
$results.Name = $switch.Name
$results.SwitchType = $switch.SwitchType
$results.NetAdapterName = if ($switch.SwitchType -eq [SwitchType]::External) { (Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescription -ErrorAction SilentlyContinue).Name }
$results.AllowManagementOS = $switch.AllowManagementOS
$results.IovEnabled = $switch.IovEnabled
$results.MinimumBandwidthMode = $switch.MinimumBandwidthMode
$results.Notes = $switch.Notes
$results.DefaultFlowMinimumBandwidthAbsolute = $switch.DefaultFlowMinimumBandwidthAbsolute
$results.DefaultFlowMinimumBandwidthWeight = $switch.DefaultFlowMinimumBandwidthWeight
$results.SwitchUniqueId = $switch.Id
$results.ReferencedVMs = (Get-VMNetworkAdapter *).Where({ $_.SwitchName -eq $switch.Name }).Count
}
return (New-Object -TypeName PSObject -Property $results)
}
# We have a single function that oversees all tests. This way the Test() method never has to change.
# Is it worth having this on a module of its own? Same for the other functions currently inside the class?
# Worth checking at some point
[System.String[]] TestConfiguration()
{
Write-Verbose "Testing configuration for switch $($this.Name)"
$results = @()
$switchBeingCreated = $false
$actualConfiguration = $this.GetConfiguration()
if ($this.Ensure -ne $actualConfiguration.Ensure)
{
# If we want to ensure it's absent return now, all other tests are only applicable when Ensure = Present
if ($this.Ensure -eq [Ensure]::Absent)
{
Write-Verbose "Switch exists but Ensure is set to Absent, will delete switch"
return 'DeleteObject'
}
Write-Verbose "Switch does not exist, but ensure is set to Present. Will create switch"
$results += 'CreateObject'
$switchBeingCreated = $true
}
Write-Verbose "Discovered real switch name is $($actualConfiguration.Name)"
$this.actualName = $actualConfiguration.Name
# If we're not allowed to perform disruptive operations, there's no point even checking these
if ($this.AllowDisruptiveAction -eq [AllowDisruptiveAction]::IfNeeded -or ($this.AllowDisruptiveAction -eq [AllowDisruptiveAction]::OnlyIfUnused -and $actualConfiguration.ReferencedVMs -eq 0))
{
# If IovEnabled or is not in the correct state, we need to recreate the switch
if (($this.IovEnabled -ne $actualConfiguration.IovEnabled) -and -not $switchBeingCreated)
{
Write-Verbose "IovEnabled is set to $($actualConfiguration.IovEnabled) but it should be set to $($this.IovEnabled). Will need to recreate switch"
# Recreating a switch is effectively a delete and a create...
$results += 'DeleteObject'
$results += 'CreateObject'
$switchBeingCreated = $true
}
# If MinimumBandwidthMode is not in the correct state, we need to recreate the switch
if ($this.MinimumBandwidthMode -and $this.MinimumBandwidthMode -ne $actualConfiguration.MinimumBandwidthMode -and ($this.MinimumBandwidthMode -ne [MinimumBandwidthMode]::Default -and $actualConfiguration.MinimumBandwidthMode -ne 'Absolute') -and -not $switchBeingCreated)
{
Write-Verbose "MinimumBandwidthMode is set to $($actualConfiguration.MinimumBandwidthMode) but should be set to $($this.MinimumBandwidthMode). Will need to recreate switch."
$results += 'DeleteObject'
$results += 'CreateObject'
$switchBeingCreated = $true
}
# Properties inside this if are set at switch creation so no need to re-check them if the switch is already being created
if ($switchBeingCreated -eq $false)
{
if ($this.SwitchType -and $this.SwitchType -ne $actualConfiguration.SwitchType -and $this.SwitchType -ne [SwitchType]::External)
{
Write-Verbose "SwitchType is currently set to $($actualConfiguration.SwitchType) but should be set to $($this.SwitchType), changing it"
$results += 'SetSwitchType'
}
# We only want to monitor the NetAdapterName for drift if the SwitchType is specified (and set to External, but this is controlled by the parameter validation)
if ($this.NetAdapterName -and $this.SwitchType -and $this.NetAdapterName -ne $actualConfiguration.NetAdapterName)
{
Write-Verbose "NetAdapterName is currently set to $($actualConfiguration.NetAdapterName) but should be set to $($this.NetAdapterName), changing it"
$results += 'SetNetAdapterName'
}
if ($this.AllowManagementOS -ne $null -and $this.AllowManagementOS -ne $actualConfiguration.AllowManagementOs)
{
Write-Verbose "AllowManagementOS is currently set to $($actualConfiguration.AllowManagementOS) but should be set to $($this.AllowManagementOS), changing it"
$results += 'SetAllowManagementOS'
}
}
}
else
{
Write-Verbose "Skipping disruptive tests as we're not allowed to perform disruptive actions. AllowDisruptiveAction = '$($this.AllowDisruptiveAction), ReferencedVMs = '$($this.ReferencedVMs)'"
}
# The notes can be set at switch creation, but they are not a disruptable operation, so it's set outside the if above
if ($this.Notes -and $this.Notes -ne $actualConfiguration.Notes -and -not $switchBeingCreated)
{
Write-Verbose "Notes does not have the correct value, setting it"
$results += 'SetNotes'
}
# If name has drifted and we don't allow name drift, set it back
if ($this.Name -ne $actualConfiguration.Name -and -not $this.AllowNameDrift -and -not $switchBeingCreated)
{
Write-Verbose "Switch does not have the correct name, changing it"
$results += 'RenameObject'
}
if ($this.DefaultFlowMinimumBandwidthAbsolute -and $this.DefaultFlowMinimumBandwidthAbsolute -ne $actualConfiguration.DefaultFlowMinimumBandwidthAbsolute)
{
Write-Verbose "DefaultFlowMinimumBandwidthAbsolute does not have the correct value, setting it"
$results += 'SetDefaultFlowMinimumBandwidthAbsolute'
}
if ($this.DefaultFlowMinimumBandwidthWeight -and $this.DefaultFlowMinimumBandwidthWeight -ne $actualConfiguration.DefaultFlowMinimumBandwidthWeight)
{
Write-Verbose "SetDefaultFlowMinimumBandwidthWeight does not have the correct value, setting it"
$results += 'SetDefaultFlowMinimumBandwidthWeight'
}
return $results
}
[void] SaveConfiguration([System.String] $caller)
{
Write-Verbose "Saving configuration for caller $caller"
$outputXML = @{ }
$outputXML.SwitchUniqueId = $this.SwitchUniqueId
# Whether there are set operations or not defines whether the Set() gets called, and we only want to save the variables below if there's a set still being called.
if ($caller -eq 'Test' -and $this.setOperations)
{
$outputXML.actualName = $this.actualName
$outputXML.setOperations = $this.setOperations
$outputXML.lastExecution = $this.startExecution
}
else
{
$outputXML.lastExecution = $this.startExecution
}
$directoryName = [System.IO.FileInfo]::New($this.configFile).DirectoryName
if (-not (Test-Path $directoryName))
{
New-Item $directoryName -ItemType Directory -Force
}
Export-Clixml -InputObject $outputXML -Path $this.configFile
}
[void] LoadConfiguration()
{
Write-Verbose 'Loading configuration'
# Calculate what the path for the config file should be
$resourceName = [System.IO.Path]::GetFileNameWithoutExtension($PSScriptRoot)
$this.configFile = "$env:APPDATA\DSC\$resourceName\$($this.Name).xml"
if (-not (Test-Path $this.configFile))
{
Write-Verbose "Config file '$($this.configFile)' not found, running for first time for switch '$($this.Name)'"
$this.hasRun = $false
}
else
{
Write-Verbose "Found config file '$($this.configFile)' for switch '$($this.Name)' - importing variables"
$inputXML = Import-Clixml $this.ConfigFile
foreach ($key in $inputXML.Keys)
{
Write-Verbose "Assining value '$($inputXML.$key)' to variable '$key'"
$this.$key = $inputXML.$key
}
}
}
}
#region Helper Functions
function New-ErrorRecord
{
[CmdletBinding(DefaultParameterSetName = 'ErrorMessageSet')]
param
(
[Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'ErrorMessageSet')]
[String]$ErrorMessage,
[Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'ExceptionSet')]
[System.Exception]$Exception,
[Parameter(ValueFromPipelineByPropertyName = $true, Position = 1, ParameterSetName = 'ErrorMessageSet')]
[Parameter(ValueFromPipelineByPropertyName = $true, Position = 1, ParameterSetName = 'ExceptionSet')]
[System.Management.Automation.ErrorCategory]$ErrorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified,
[Parameter(ValueFromPipelineByPropertyName = $true, Position = 2, ParameterSetName = 'ErrorMessageSet')]
[Parameter(ValueFromPipelineByPropertyName = $true, Position = 2, ParameterSetName = 'ExceptionSet')]
[String]$ErrorId,
[Parameter(ValueFromPipelineByPropertyName = $true, Position = 3, ParameterSetName = 'ErrorMessageSet')]
[Parameter(ValueFromPipelineByPropertyName = $true, Position = 3, ParameterSetName = 'ExceptionSet')]
[Object]$TargetObject
)
if (!$Exception)
{
$Exception = New-Object System.Exception $ErrorMessage
}
# Function does not belong to class so does not have a localized string. The idea if for helper functions in the future to be on their own module, just didn't do it as of yet.
Write-Verbose "Creating new error record with the following information: ErrorMessage = '$($Exception.Message)'; ErrorId = '$ErrorId'; ErrorCategory = '$ErrorCategory'"
New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $ErrorCategory, $TargetObject
}
#endregion