I will try to describe what I am trying to achieve.
I have a script for deploying virtual machines on vmware.
I need to validate each value entered befor prociding with another step in script.
In most cases was quite easy to validate see example below
# Getting info about clusters
Write-Host "Select cluster to deploy new virtual machine" -ForegroundColor Green
Get-Datacenter | Get-Cluster | Select-Object Name | Format-Table -AutoSize
do {
$ClusterToDeployVM = Read-Host "Enter cluster name"
} while (($ClusterToDeployVM -like "") -or ($ClusterToDeployVM.StartsWith(" ")) -or ($ClusterToDeployVM.EndsWith(" ")) -or ((Get-Cluster).Name -notcontains $ClusterToDeployVM)
In case of IP configuration, there is much more values at the same time and also there is more things to be checked.
So I was trying to write function with built-in parameter validation but I do not know, how to extract error codes
from these built-in validation parameters.
Here is example (sorry for split but there is some limitation in editor)
function Test-VMipConfiguration {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateScript({-not($_.StartsWith(" "))})]
[ValidateScript({-not($_.EndsWith(" "))})]
[ValidateScript({($_.StartsWith("10"))})]
[string]$VMip,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateScript({-not($_.StartsWith(" "))})]
[ValidateScript({-not($_.EndsWith(" "))})]
[ValidateScript({($_.StartsWith("255"))})]
[string]$VMnetmask,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateScript({-not($_.StartsWith(" "))})]
[ValidateScript({-not($_.EndsWith(" "))})]
[ValidateScript({($_.EndsWith("254"))})]
[string]$VMgw,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateScript({-not($_.StartsWith(" "))})]
[ValidateScript({-not($_.EndsWith(" "))})]
[ValidateScript({($_.StartsWith("10"))})]
[string]$VMdns1,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateScript({-not($_.StartsWith(" "))})]
[ValidateScript({-not($_.EndsWith(" "))})]
[ValidateScript({($_.StartsWith("10"))})]
[string]$VMdns2,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateRange(1, 4094)]
[int]$VMvlan
)
begin {
$VMipOctets = @($VMip.Split('.'))
if ($VMipOctets.Length -gt 4) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMip -Category InvalidArgument
exit
} if ($VMipOctets.Length -lt 4) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMip -Category InvalidArgument
exit
}
$VMnetmaskOctets = @($VMnetmask.Split('.'))
if ($VMnetmaskOctets.Length -gt 4) {
Write-Error -Message "Invalid number of octets in mask address" -TargetObject $VMnetmask -Category InvalidArgument
exit
} if ($VMnetmaskOctets.Length -lt 4) {
Write-Error -Message "Invalid number of octets in mask address" -TargetObject $VMnetmask -Category InvalidArgument
exit
}
$VMgwOctets = @($VMgw.Split('.'))
if ($VMgwOctets.Length -gt 4) {
Write-Error -Message "Invalid number of octets in gateway address" -TargetObject $VMgw -Category InvalidArgument
exit
} if ($VMgwOctets.Length -lt 4) {
Write-Error -Message "Invalid number of octets in gateway address" -TargetObject $VMgw -Category InvalidArgument
exit
}
$VMdns1Octets = @($VMdns1.Split('.'))
if ($VMdns1Octets.Length -gt 4) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns1 -Category InvalidArgument
exit
} if ($VMdns1Octets.Length -lt 4) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns1 -Category InvalidArgument
exit
}
$VMdns2Octets = @($VMdns2.Split('.'))
if ($VMdns2Octets.Length -gt 4) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns2 -Category InvalidArgument
exit
} if ($VMdns2Octets.Length -lt 4) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns2 -Category InvalidArgument
exit
}
}
process {
}
end {
}
}
The problem is, that I need to ask user for input, if the input is not valid, I need to repeat user's input to variables
again and do the validation process again.
So the only option that I figured out so far, is to write function with my own validation (no built-in agruments validation)
Does anyone has an idea for better/other solution?
Thank you for any feedback
When I need to request a decision from the user I’m used to limit the possible input choices to a predefined list of choices. Either I add an index to a given list and the user picks just the index or I use a gridview wtih the parameter -Passthru.
In any case, I would avoid giving the user the opportunity to input arbitrary text.
Use Trim to remove the extra whitespace when prompting:
# Getting info about clusters
Write-Host "Select cluster to deploy new virtual machine" -ForegroundColor Green
#Collect the names one time
$vms = Get-Datacenter | Get-Cluster | Select-Object Name
#Show the data
$vms | Format-Table -AutoSize
do {
#Wrap your call with Trim to remove any whitespace before or after the input
$ClusterToDeployVM = (Read-Host "Enter cluster name").Trim()
#Provide context on why you are re-prompting for input
if ( $vms -contains $ClusterToDeployVM ) {
'{0} vm already exists, choose another name' -f $ClusterToDeployVM
}
} while ( $vms -notcontains $ClusterToDeployVM )
For the function params, try to use a [type] so that you are not trying to validate a string with a complex pattern. For something like IP addresses, another option is a regex and use validate pattern for something more specific like a subnet mask. There isn’t a need to validate null because the type will most likely cover that, but even listing it as mandatory will ensure a value is passed.
PS C:\WINDOWS\system32> function Test-It {
param (
[Parameter(Mandatory=$true)]
[System.Net.IPAddress]$ip
)
begin {}
process {}
end {}
}
Test-It -Ip one
Test-It : Cannot process argument transformation on parameter 'ip'. Cannot convert value "one" to type "System.Net.IPAddress". Error: "An invalid IP address was specified."
At line:11 char:13
+ Test-It -Ip one
+ ~~~
+ CategoryInfo : InvalidData: (:) [Test-It], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-It
PS C:\WINDOWS\system32> Test-It -Ip 1.1.1.1
Hello Olaf, I was trying to make script as flexible as possible (in current state even someone in other company could use it with minimum changes to do).
I agree that predefined list would be a solid solution to avoid wrong inputs but in environment where clusters, folders and other resources are created and revoked frequently, it would require a lot of effort.
Thank you for your input.
Hello Rob, thanks for tip! I have completely forgot about Trim option. I see only one problem,
if variable is empty, it throws an error. I could set $ErrorActionPreference = “SilentlyContinue” but then I would
lost ability to see all error messages (unless I would probably set it only in parts of script where I would need to
suppress it and then set it back to default)
Regarding IP configuration test, I need something that will do the test and based on “True” or “False” statement it will
continue.
So I have created one-purpose function see below.
function Test-VmIpConfiguration {
[CmdletBinding()]
param (
[string]$VMip,
[string]$VMnetmask,
[string]$VMgw,
[string]$VMdns1,
[string]$VMdns2,
[int]$VMvlan
)
begin {
$VMdns1Value = $VMdns1
$VMdns2Value = $VMdns2
$VMipOctets = @($VMip.Split('.'))
$VMnetmaskOctets = @($VMnetmask.Split('.'))
$VMgwOctets = @($VMgw.Split('.'))
$VMdns1Octets = @($VMdns1.Split('.'))
$VMdns2Octets = @($VMdns2.Split('.'))
}
process {
# $VMip test
if ($VMip -like "") {
Write-Error -Message "Argument ""VMip"" is empty" -TargetObject $VMip -Category NotSpecified
$VMip = 0
} elseif (($VMip.StartsWith(" ")) -or ($VMip.EndsWith(" "))) {
Write-Error -Message "Argument ""VMip"" contains empty character" -TargetObject $VMip -Category InvalidArgument
$VMip = 0
} elseif (-not $VMip.StartsWith("10")) {
Write-Error -Message "Argument ""VMip"" does not start with number ""10"" in first octet" -TargetObject $VMip -Category InvalidArgument
$VMip = 0
} elseif (($VMipOctets.Length -lt 4) -or ($VMipOctets.Length -gt 4)) {
Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMip -Category InvalidArgument
$VMip = 0
} else {
$VMip = 1
}
# $VMnetmask test
if ($VMnetmask -like "") {
Write-Error -Message "Argument ""VMnetmask"" is empty" -TargetObject $VMnetmask -Category NotSpecified
$VMnetmask = 0
} elseif (($VMnetmask.StartsWith(" ")) -or ($VMnetmask.EndsWith(" "))) {
Write-Error -Message "Argument ""$VMnetmask"" contains empty character" -TargetObject $VMnetmask -Category InvalidArgument
$VMnetmask = 0
} elseif (-not $VMnetmask.StartsWith("255")) {
Write-Error -Message "Argument ""VMnetmask"" does not start with number ""255"" in first octet" -TargetObject $VMnetmask -Category InvalidArgument
$VMnetmask = 0
} elseif (($VMnetmaskOctets.Length -lt 4) -or ($VMnetmaskOctets.Length -gt 4)) {
Write-Error -Message "Invalid number of octets in network mask" -TargetObject $VMnetmask -Category InvalidArgument
$VMnetmask = 0
} else {
$VMnetmask = 1
}
# $VMgw test
if ($VMgw -like "") {
Write-Error -Message "Argument ""VMgw"" is empty" -TargetObject $VMgw -Category NotSpecified
$VMgw = 0
} elseif (($VMgw.StartsWith(" ")) -or ($VMgw.EndsWith(" "))) {
Write-Error -Message "Argument ""VMgw"" contains empty character" -TargetObject $VMgw -Category InvalidArgument
$VMgw = 0
} elseif (-not $VMgw.EndsWith("254")) {
Write-Error -Message "Argument ""VMgw"" does not end with number ""254"" in last octet" -TargetObject $VMgw -Category InvalidArgument
$VMgw = 0
} elseif (($VMgwOctets.Length -lt 4) -or ($VMgwOctets.Length -gt 4)) {
Write-Error -Message "Invalid number of octets in gateway address" -TargetObject $VMgw -Category InvalidArgument
$VMgw = 0
} else {
$VMgw = 1
}
# $VMdns1 test
if ($VMdns1 -like "") {
Write-Error -Message "Argument ""VMdns1"" is empty" -TargetObject $VMdns1 -Category NotSpecified
$VMdns1 = 0
} elseif (($VMdns1.StartsWith(" ")) -or ($VMdns1.EndsWith(" "))) {
Write-Error -Message "Argument ""VMdns1"" contains empty character" -TargetObject $VMdns1 -Category InvalidArgument
$VMdns1 = 0
} elseif (-not $VMdns1.StartsWith("10")) {
Write-Error -Message "Argument ""VMdns1"" does not start with number ""10"" in first octet" -TargetObject $VMdns1 -Category InvalidArgument
$VMdns1 = 0
} elseif (($VMdns1Octets.Length -lt 4) -or ($VMdns1Octets.Length -gt 4)) {
Write-Error -Message "Invalid number of octets in DNS address" -TargetObject $VMip -Category InvalidArgument
$VMdns1 = 0
} else {
$VMdns1 = 1
}
# $VMdns2 test
if ($VMdns2 -like "") {
Write-Error -Message "Argument ""VMdns2"" is empty" -TargetObject $VMdns2 -Category NotSpecified
$VMdns2 = 0
} elseif (($VMdns2.StartsWith(" ")) -or ($VMdns2.EndsWith(" "))) {
Write-Error -Message "Argument ""VMdns2"" contains empty character" -TargetObject $VMdns2 -Category InvalidArgument
$VMdns2 = 0
} elseif (-not $VMdns2.StartsWith("10")) {
Write-Error -Message "Argument ""VMdns2"" does not start with number ""10"" in first octet" -TargetObject $VMdns2 -Category InvalidArgument
$VMdns2 = 0
} elseif (($VMdns2Octets.Length -lt 4) -or ($VMdns2Octets.Length -gt 4)) {
Write-Error -Message "Invalid number of octets in DNS address" -TargetObject $VMip -Category InvalidArgument
$VMdns2 = 0
} else {
$VMdns2 = 1
}
# DNS duplicity test
if ($VMdns1Value -like $VMdns2Value) {
Write-Error -Message "Argument ""VMdns1"" and ""VMdns2"" cannot be same" -TargetObject $VMdns1 -Category InvalidResult
$VMdnsDuplicity = 0
} else {
$VMdnsDuplicity = 1
}
# VLAN test
if ($VMvlan -like "") {
Write-Error -Message "Argument ""VMvlan"" is empty" -TargetObject $VMvlan -Category NotSpecified
$VMvlan = 0
} elseif (($VMvlan -lt 1) -or ($VMvlan -gt 4094)) {
Write-Error -Message "Argument ""VMvlan"" range is not between 1 and 4096" -TargetObject $VMvlan -Category LimitsExceeded
$VMvlan = 0
} else {
$VMvlan = 1
}
}
end {
if (($VMip -eq 0) -or ($VMnetmask -eq 0) -or ($VMgw -eq 0) -or ($VMdns1 -eq 0) -or ($VMdns2 -eq 0) -or ($VMvlan -eq 0) -or ($VMdnsDuplicity -eq 0)) {
return $false
} else {
return $true
}
}
}
So then I can ask user for input until it is correct
I know there is missing other validation and I guess this is your point to use [System.Net.IPAddress]$ip , to avoid writing validation for number of octets, “0” as first number in octet, etc.
I will test it and rewrite it
Thank you very much for answer, it will make script much more clear and reliable.
Just to make clear what I meant: I do create this indexed lists to choose from dynamically from arrays created during the runtime of the script. I do not create them by hand. … of course that’s a little more effort in advance. But it saves effort you spend to validate the user input.