I'm using Powershell to set up IIS bindings on a web server, and having a problem with the following code:
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if ($serverIps.length -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
$primaryIp = $serverIps[0]
$secondaryIp = $serverIps[1]
If there's 2+ IPs on the server, fine - Powershell returns an array, and I can query the array length and extract the first and second addresses just fine.
Problem is - if there's only one IP, Powershell doesn't return a one-element array, it returns the IP address (as a string, like "192.168.0.100") - the string has a .length property, it's greater than 1, so the test passes, and I end up with the first two characters in the string, instead of the first two IP addresses in the collection.
How can I either force Powershell to return a one-element collection, or alternatively determine whether the returned "thing" is an object rather than a collection?
Define the variable as an array in one of two ways...
Wrap your piped commands in parentheses with an # at the beginning:
$serverIps = #(gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort)
Specify the data type of the variable as an array:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
Or, check the data type of the variable...
IF ($ServerIps -isnot [array])
{ <error message> }
ELSE
{ <proceed> }
Force the result to an Array so you could have a Count property. Single objects (scalar) do not have a Count property. Strings have a length property so you might get false results, use the Count property:
if (#($serverIps).Count -le 1)...
By the way, instead of using a wildcard that can also match strings, use the -as operator:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration -filter "IPEnabled=TRUE" | Select-Object -ExpandProperty IPAddress | Where-Object {($_ -as [ipaddress]).AddressFamily -eq 'InterNetwork'}
You can either add a comma(,) before return list like return ,$list or cast it [Array] or [YourType[]] at where you tend to use the list.
If you declare the variable as an array ahead of time, you can add elements to it - even if it is just one...
This should work...
$serverIps = #()
gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort | ForEach-Object{$serverIps += $_}
You can use Measure-Object to get the actual object count, without resorting to an object's Count property.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if (($serverIps | Measure).Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
Return as a referenced object, so it never converted while passing.
return #{ Value = #("single data") }
I had this problem passing an array to an Azure deployment template. If there was one object, PowerShell "converted" it to a string. In the example below, $a is returned from a function that gets VM objected according to the value of a tag. I pass the $a to the New-AzureRmResourceGroupDeployment cmdlet by wrapping it in #(). Like so:
$TemplateParameterObject=#{
VMObject=#($a)
}
New-AzureRmResourceGroupDeployment -ResourceGroupName $RG -Name "TestVmByRole" -Mode Incremental -DeploymentDebugLogLevel All -TemplateFile $templatePath -TemplateParameterObject $TemplateParameterObject -verbose
VMObject is one of the template's parameters.
Might not be the most technical / robust way to do it, but it's enough for Azure.
Update
Well the above did work. I've tried all the above and some, but the only way I have managed to pass $vmObject as an array, compatible with the deployment template, with one element is as follows (I expect MS have been playing again (this was a report and fixed bug in 2015)):
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
foreach($vmObject in $vmObjects)
{
#$vmTemplateObject = $vmObject
$asJson = (ConvertTo-Json -InputObject $vmObject -Depth 10 -Verbose) #-replace '\s',''
$DeserializedJson = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property #{MaxJsonLength=67108864}).DeserializeObject($asJson)
}
$vmObjects is the output of Get-AzureRmVM.
I pass $DeserializedJson to the deployment template' parameter (of type array).
For reference, the lovely error New-AzureRmResourceGroupDeployment throws is
"The template output '{output_name}' is not valid: The language expression property 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Expression.Expressions.JTokenExpression'
can't be evaluated.."
There is a way to deal with your situation. Leave most of you code as-is, just change the way to deal with the $serverIps object. This code can deal with $null, only one item, and many items.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
# Always use ".Count" instead of ".Length".
# This works on $null, only one item, or many items.
if ($serverIps.Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
# Always use foreach on a array-possible object, so that
# you don't have deal with this issue anymore.
$serverIps | foreach {
# The $serverIps could be $null. Even $null can loop once.
# So we need to skip the $null condition.
if ($_ -ne $null) {
# Get the index of the array.
# The #($serverIps) make sure it must be an array.
$idx = #($serverIps).IndexOf($item)
if ($idx -eq 0) { $primaryIp = $_ }
if ($idx -eq 1) { $secondaryIp = $_ }
}
}
In PowerShell Core, there is a .Count property exists on every objects. In Windows PowerShell, there are "almost" every object has an .Count property.
I have DHCP script that looks for matching hostnames in all the scopes on the DHCP servers
I first get all the DHCP servers and import a .txt of hostnames
$list = Get-Content C:\script\HostNameList.txt #Defines content it pulls as list
$DHServers = Get-DhcpServerInDC #gives variable name for loop
# Gets all DHCP servers ands scopes
foreach ($Server in $DHServers){
$scopes = Get-DHCPServerv4Scope -ComputerName $Server.dnsname #get all scopes
}
I loop through list of hostnames and scopes looking for a match. Somewhere in here is my issue
$Output = foreach ($hostname in $list) { #Calls each item in list a hostname and sends to output
if (test-connection -count 1 -computername $hostname -quiet) #With 1 ping, check if hostname is online
{
foreach ($scope in $scopes){
if($scope | Get-DhcpServerV4Lease -ComputerName $server.dnsname | Where-Object HostName -like "$hostName*" ) #compares the hostname to lease to find which scope it is in
{ $scope.name } #return scope it found hostname in
}
[PSCustomObject]#{ #Rename varibles in data pull for output file
Asset = $hostname
Location = $scope.name #only want the name of the scope
Status = "Online"
}
}
else #statement if hostname is not online
{
Write-host "$hostname Is offline, only Last Location is known. $hostname was added to the output file." -BackgroundColor DarkRed
[PSCustomObject]#{
Asset = $hostname
Location = $scope.name #only want the name of the scope, since the name = Location
Status = "Offline"
}
}
}
$Output #show output in powershell
$Output | Export-Csv -Path C:\script\Asset_Result.csv -NoTypeInformation #outputs .csv
This is what it is doing, the output repeats the last item on the list of DHCP scopes.
Asset Location Status
----- -------- ------
A847 Public Internet Online
A261 Public Internet Offline
A201 Public Internet Online
This is what it should be doing
Asset Location Status
----- -------- ------
A847 FLoor 1 Online
A261 West 1st FL Offline
A201 Floor 3 Online
How can I get $scope.name in my
if($scope | ... statement to go to my PSCustomObject after each iteration?
This:
foreach ($Server in $DHServers){
$scopes = Get-DHCPServerv4Scope -ComputerName $Server.dnsname #get all scopes
}
is - in net effect - the equivalent of:
$scopes = Get-DHCPServerv4Scope -ComputerName $DHServers[-1].dnsname #get all scopes
That is, you keep reassigning to the same variable ($scopes) in the loop body, replacing the previous value, so that you end up with only the result from the last loop iteration, for the last server stored in $DHServers, i.e. $DHServers[-1].
The best solution is to rely on PowerShell's ability to use statements such as foreach as an expression whose output - even iterative output from a loop - is automatically collected in an [object[]] array (with two or more outputs) by PowerShell:
# Collect the output from *all* Get-DHCPServerv4Scope calls.
[array] $scopes = foreach ($Server in $DHServers) {
Get-DHCPServerv4Scope -ComputerName $Server.dnsname #get all scopes
}
Note: The [array] type constraint (same as: [object[]]) is only necessary if there can situationally be just one output object and you want to ensure that the collected output is always an array.
I need to include 'Installed' and 'Not Installed' data in one CSV
I think I need to incorporate an -OR logical operator to include TRUE/FALSE
output in one CSV. Idk how to do that yet.
There's a folder with many *ServerData files that contain a list of KBs with
possible duplicate KBs.
There is a *ServerData file for each server, with possible duplicate files.
I want to test whether any of them contain KB2151757 and KB4556403.
Then output the results to a csv with a status of either Installed or Not
Installed.
Currently it only returns a list of computers with the KB installed.
If the $patch is not found, it currently returns nothing (null).
For each $computer searched, it needs to return the specified fields for the
[PSCustomObject]
I'm thinking that maybe I just need to take a function to find 'installed' and a function to find 'not installed' and add the results together to export. Idk how to do that. I feel like there must be an easier way.
Click to view a sample of the CSV
$computers = Get-Item -path F:\*ServerData | Select-Object -ExpandProperty basename
$patch = gc -path F:\*ServerData | Sort-Object -Unique | Select-String KB2151757, KB4556403 #'KB(\d+)'
$output = ForEach ($computer in $computers) {
ForEach ($kb in $patch) {
if ($null -eq $patch){
[PSCustomObject]#{
Status = 'Not Installed'
Server = $computer
KB = $kb
}
} else{
[PSCustomObject]#{
Status = 'Installed'
Server = $computer
KB = $kb
}
}
}
}
$output | Export-csv C:\KB-Report.txt -notypeinformation -delimiter ',' -encoding utf8
If you start by grouping the files by the associated computer name, then the procedure becomes straightforward (pseudocode):
for each Computer
for each ExpectedPatch
if ServerData for Computer contains ExpectedPatch
Output object with 'Installed' status for ExpectedPatch on Computer
else
Output object with 'NotInstalled' status for ExpectedPatch on Computer
So let's give that a try:
# Define the articles we're looking for
$ExpectedPatches = 'KB2151757', 'KB4556403'
# Enumerate and group data files by computer name, output as hashtable
# The resulting hashtable will have the computer name is Name and the associated files as its value
$ServerDataPerComputer = Get-Item -Path F:\*ServerData |Group BaseName -AsHashtable
foreach($Computer in $ServerDataPerComputer.GetEnumerator())
{
foreach($Patch in $ExpectedPatches)
{
# Pipe all the file references to Select-String, look for the KB ID, return after the first match if any
$Status = if($Computer.Value |Select-String "\b$Patch\b" |Select-Object -First 1){
'Installed'
}
else {
# Select-String didn't find the KB ID in any of the files
'NotInstalled'
}
[pscustomobject]#{
Status = $Status
Server = $Computer.Name
KB = $Patch
}
}
}
I am comparing a predetermined object (VMHostVirtualSwitch Name) value with all object(VMHostVirtualSwitch Names) values within a collection of objects and want the status to be "FAIL" if the objects don't match
I have written the following code so far but it doesn't seem to be working. I know the objects don't match and I should get "FAIL" as an output
$VMHostVirtualSwitch = Get-VMHostNetwork -VMHost abc.com | Select-Object VirtualSwitch*
$Cluster = Get-Cluster -VMHost abc.com
$VMHosts = Get-Cluster $Cluster | Get-VMHost
[int]$Switchcount=0
foreach ($VMHost in $VMHosts){
$CurrentHostVirtualSwitch = Get-VMHostNetwork -VMHost $VMHost | Select-Object VirtualSwitch*
if ($CurrentHostVirtualSwitch -ne $VMHostVirtualSwitch) {
$Switchcount++
}
}
if($Switchcount -ge 1) {
Write-Output "FAIL"
}
$VMHostVirtualSwitch has the following value
VirtualSwitch
-------------
{vSwitch3}
When I expand the $VMHostVirtualSwitch , I get the following values
Name NumPorts Mtu Notes
---- -------- --- -----
vSwitch3 10562 2340
You problem is PowerShell does not know how to compare those objects. Even if they had the same data they are technically two different objects (a blog post touches on this subject). At the end of the day if you are just comparing the names then do your comparison on just those.
$VMHostVirtualSwitch = (Get-VMHostNetwork -VMHost abc.com).VirtualSwitch.Name
$Cluster = Get-Cluster -VMHost abc.com
$VMHosts = Get-Cluster $Cluster | Get-VMHost
[int]$Switchcount=0
foreach ($VMHost in $VMHosts){
$CurrentHostVirtualSwitch = (Get-VMHostNetwork -VMHost $VMHost).VirtualSwitch.Name
if ($CurrentHostVirtualSwitch -ne $VMHostVirtualSwitch) {
$Switchcount++
}
}
if($Switchcount -ge 1) {
Write-Output "FAIL"
}
Now you should just be comparing strings which will get you more predictable results. I have only change the variable expansion in the above example. You might have some error checking to do to account for.
Something like this might be shorter then your loop
$badHosts = $VMHosts | Where-Object{(Get-VMHostNetwork -VMHost $_).VirtualSwitch.Name -ne $VMHostVirtualSwitch}
if($badHosts.count -ge 1) {
Write-Output "FAIL"
}
Compare-Object would also be a way to go for this, especially if there was multiple properties you were comparing: example. Since we are boiling down to simple strings I think what I propose should suffice.
I am attempting to analyze a group of text files (MSFTP logs) and do counts of IP addresses that have submitted bad credentials. I think I have it worked out except I don't think that the array is passing to/from the function correctly. As a result, I get duplicate entries if the same IP appears in multiple log files. What am I doing wrong?
Function LogBadAttempt($FTPLog,$BadPassesArray)
{
$BadPassEx="PASS - 530"
Foreach($Line in $FTPLog)
{
if ($Line -match $BadPassEx)
{
$IP=($Line.Split(' '))[1]
if($BadPassesArray.IP -contains $IP)
{
$CurrentIP=$BadPassesArray | Where-Object {$_.IP -like $IP}
[int]$CurrentCount=$CurrentIP.Count
$CurrentCount++
$CurrentIP.Count=$CurrentCount
}else{
$info=#{"IP"=$IP;"Count"='1'}
$BadPass=New-Object -TypeName PSObject -Property $info
$BadPassesArray += $BadPass
}
}
}
return $BadPassesArray
}
$BadPassesArray=#()
$FTPLogs = Get-Childitem \\ftpserver\MSFTPSVC1\test
$Result = ForEach ($LogFile in $FTPLogs)
{
$FTPLog=Get-Content ($LogFile.fullname)
LogBadAttempt $FTPLog
}
$Result | Export-csv C:\Temp\test.csv -NoTypeInformation
The result looks like...
Count IP
7 209.59.17.20
20 209.240.83.135
18441 209.59.17.20
13059 200.29.3.98
and would like it to combine the entries for 209.59.17.20
You're making this way too complicated. Process the files in a pipeline and use a hashtable to count the occurrences of each IP address:
$BadPasswords = #{}
Get-ChildItem '\\ftpserver\MSFTPSVC1\test' | Get-Content | ? {
$_ -like '*PASS - 530*'
} | % {
$ip = ($_ -split ' ')[1]
$BadPasswords[$ip]++
}
$BadPasswords.GetEnumerator() |
select #{n='IP';e={$_.Name}}, #{n='Count';e={$_.Value}} |
Export-Csv 'C:\Temp\test.csv' -NoType