Running ForEach-Object -Parallel, data missing from export - powershell

I have some working code that basically queries 2 different Graph API endpoints, then searches for a match in the User Principal Name column, and inserts the extension_335d4df9847945fbaa472c8b8fbb5d75_employeeNumber column and values to the exported csv (Thanks to the user #PMental for this solution) This column derives from attribute that was recently extended from our on premises AD.
This code works perfectly fine, however if I try to parallelize it, I get no results in the extension_335d4df9847945fbaa472c8b8fbb5d75_employeeNumber column.
Is this because once it is being parallelized, I'm not able to share variables between the parallel processes? If so, how on earth do I accomplish this?
Code below - if you remove the -Parallel, it works fine:
$graphApiUri = "https://graph.microsoft.com/v1.0/reports/getOffice365ActiveUserDetail(period='D90')"
$Uri = "https://graph.microsoft.com/v1.0/users?`$select=userPrincipalName,extension_335d4df9847945fbaa472c8b8fbb5d75_employeeNumber"
$O365Report = Invoke-RestMethod -Method Get -Uri $graphApiUri -Headers $headerParams | ConvertFrom-Csv
# If the result is more than 999, we need to read the #odata.nextLink to show more than one side of users
$UserDetails = while (-not [string]::IsNullOrEmpty($uri)) {
# API Call
$apiCall = try {
Invoke-RestMethod -Headers $headerParams -Uri $uri -Method Get
}
catch {
$errorMessage = $_.ErrorDetails.Message | ConvertFrom-Json
}
$uri = $null
if ($apiCall) {
# Check if any data is left
$uri = $apiCall.'#odata.nextLink'
$apiCall
}
}
Write-Output "Matching UPN to employeeNumber..."
$O365Report | ForEach-Object -Parallel {
$CurrentEmpNumber = $UserDetails.value |
Where-Object userPrincipalName -eq $_.'User Principal Name' |
Select-Object -ExpandProperty extension_335d4df9847945fbaa472c8b8fbb5d75_employeeNumber -ErrorAction SilentlyContinue
$_ | Add-Member -MemberType NoteProperty -Name extension_335d4df9847945fbaa472c8b8fbb5d75_employeeNumber -Value $CurrentEmpNumber
}
$O365Report | Export-Csv $ReportCSV -NoTypeInformation
Write-Output "Report saved to $ReportCSV."

When inside of a ForEach-Object -Parallel script block, and you are trying to reference variables which were created outside of it, you need to preface the variable name with using: so it would be $using:UserDetails
Examples:
Returns nothing because $test isn't accessible within the scope of the parallel script block:
$test = 1;
0..5 | % -Parallel { $test; };
Returns the value of $test five times because by using $using:test you are now able to see its value:
$test = 1;
0..5 | % -Parallel { $using:test; };
From documenation:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/foreach-object?view=powershell-7.1
The ForEach-Object -Parallel parameter set runs script blocks in parallel on separate process threads. The $using: keyword allows passing variable references from the cmdlet invocation thread to each running script block thread. Since the script blocks run in different threads, the object variables passed by reference must be used safely. Generally it is safe to read from referenced objects that don't change. But if the object state is being modified then you must used thread safe objects, such as .Net System.Collection.Concurrent types (See Example 11).
Personal note:
I would also recommend using -ThrottleLimit to limit its max degrees of paralellism. The default is 5, but you may want more or less than that depending on testing.

Related

add element to array in powershell

I'm trying to add a Totals line to the array $response_table.
The line should be Totals = 56 (number), for example.
I've tried $response_table += #{Name="Total"; Count=55};, it adds a line with rubbish data.
Can you help with how to add this?
Code is below
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$current_month = (Get-Date).month;
$current_year = (Get-Date).year;
$response_table=#();
$what_year=2019
$month_count=12
$current_month, $total_year, $current_date_interval;
$total_monthly=#(); # empty array, will be populated with values from the API
#$numbers += 5, 2, 3; # to add values to array
if ($what_year -eq $current_year)
{
$month_count = $current_month-1;
}
for ($current_month=1; $current_month -le $month_count; $current_month++)
{
$current_date_interval = -join($what_year, "-", $current_month);
$uri="https://data.police.uk/api/crimes-street/bicycle-theft?poly=53.950624,-1.059234:53.951301,-1.049181:53.947361,-1.043420:53.950333,-1.030671:53.952997,-1.016427:53.950189,-1.013653:53.929487,-1.042286:53.942512,-1.054948:53.941936,-1.057172:53.944481,-1.060155s8&date="+$current_date_interval;
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
$response_table += $response | Group month |Select -Property name,count
Write-Host $response_table;
$response | Group month |Select -Property name,count
$total_year += $response.count;
}
Write-Host ($response_table|Measure-object -Property count -sum)
$response_table += #{Name="Total"; Count=55};
# does not work
$response_table | export-csv "list.csv" -notypeinformation
#add-content "list.csv" "Total,$total_year"
Write-Host "Yearly total"$total_year;
As AdminOfThings indicates, you're mistakenly adding a hashtable to the existing array of custom objects (that Select-Object outputs).
To construct a custom object in PSv3+, simply place [pscustomobject] before a hashtable literal, which in your case means:
$response_table += [pscustomobject] #{Name="Total"; Count=55}
Now your Export-Csv command should work as intended.
As an aside: Up to PowerShell 6.x, Export-Csv only (meaningfully) supports custom objects as input, not also hashtables. v7 adds support for hashtables.
Generally avoid using += for appending to arrays:
Arrays are fixed-size data structures, so what PowerShell must do when you "append to" an array with += is to create a new array behind the scenes every time, which is quite inefficient in a loop.
While using an efficiently in-place extensible data structure such as [System.Collections.ArrayList] or [System.Collections.Generic.List[object]] is the next best thing, the simplest and fastest approach is to simply let PowerShell collect multiple output objects in an array ([object[]]) for you, by assigning the entire loop as an expression to a variable:
[array] $response_table = for ($current_month=1; $current_month -le $month_count; $current_month++)
{
$current_date_interval = -join($what_year, "-", $current_month);
$uri="https://data.police.uk/api/crimes-street/bicycle-theft?poly=53.950624,-1.059234:53.951301,-1.049181:53.947361,-1.043420:53.950333,-1.030671:53.952997,-1.016427:53.950189,-1.013653:53.929487,-1.042286:53.942512,-1.054948:53.941936,-1.057172:53.944481,-1.060155s8&date="+$current_date_interval;
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
# *Output* the result of this pipeline, and PowerShell will
# collect all outputs for you
$response | Group month | Select -Property name,count
$total_year += $response.count;
}
# It's fine to use += to add the total, because you're only using it *once*.
$response_table += [pscustomobject] #{Name="Total"; Count=55}
Note: The [array] type constraint ensures that $response_table becomes an array even if the loop happens to have just 1 iteration.

Power shell For Loop not Looping

So the output works fine but I'm having an issue with it only outputing the last line it runs. Is there anyway to check for loops to test in the future?
but i have a list of ip address and im trying to check if the firewall in windows is enabled or disabled.
They are on one LARGE (300+ workgroup). Any help in getting this to loop properly would be appreciated. Security and other things are not a concern cause i have other scripts that run fine. And i dont get any errors. just the single output.
ive already tried moving the array and that didn't help. im thinking it could be the PSCustomObject part as i'm just starting to learn these. Or could it be my input and output formats are different and that's causing issues??
clear
$ComputerList = get-content C:\Users\Administrator\Desktop\DavidsScripts\TurnOffFirewall\input.txt
$Status = #(
foreach ($Computer in $ComputerList) {
netsh -r $Computer advfirewall show currentprofile state})[3] -replace 'State' -replace '\s'
$Object = [PSCustomObject]#{
Computer = $Computer
Firewall = $Status
}
Write-Output $Object
$Object | Export-Csv -Path "C:\FirewallStatus.csv" -Append -NoTypeInformation
Your previous code was not escaping the loop and was only adding the last computer in the loop to the object.
The best way I have found, is to make a temp object and add it to an array list then export that. Much nicer.
$ComputerList = get-content C:\Users\Administrator\Desktop\DavidsScripts\TurnOffFirewall\input.txt
$collectionVariable = New-Object System.Collections.ArrayList
ForEach ($Computer in $ComputerList) {
# Create temp object
$temp = New-Object System.Object
# Add members to temp object
$temp | Add-Member -MemberType NoteProperty -Name "Computer" -Value $Computer
$temp | Add-Member -MemberType NoteProperty -Name "Firewall" -Value $((netsh -r $Computer advfirewall show currentprofile state)[3] -replace 'State' -replace '\s')
# Add the temp object to ArrayList
$collectionVariable.Add($temp)
}
Write-Output $collectionVariable
$collectionVariable | Export-Csv -Path "C:\FirewallStatus.csv" -Append -NoTypeInformation
Here's a streamlined, functional version of your code, using a single pipeline:
Get-Content C:\Users\Administrator\Desktop\DavidsScripts\TurnOffFirewall\input.txt |
ForEach-Object {
[pscustomobject] #{
Computer = $_
Firewall = (-split ((netsh -r $_ advfirewall show currentprofile state) -match '^State'))[-1]
}
} | Export-Csv -Path C:\FirewallStatus.csv -NoTypeInformation
Note:
No intermediate variables are needed; each computer name read from the input file is processed one by one, and each custom object constructed based on it is sent to the output CSV file.
The command for extracting the firewall status from netsh's output was made more robust in order to extract the state information based on the line content (regex ^State, i.e., a line starting with State) rather than a line index ([3]); the unary form of -split splits the line of interest into tokens by whitespace, and index [-1] extracts the last token, which is the state value.
As for what you tried:
Your foreach loop ended before $Object was constructed, so you ended up constructing just 1 object to send to the output file with Export-Csv.
If you had formatted your code properly, that fact would have been more obvious; try using Visual Studio Code with the PowerShell extension, which offers automatic formatting via the >Format Document (Shift+Alt+F) command.

ADFS Powershell script to add additional SamlEndpoints to existing

This question likely doesn't require actual knowledge of ADFS, but I'm providing that for context. The command "Set-AdfsRelyingPartyTrust -Name X -SamlEndpoint Y" overwrites all SAML endpoints with what you specify. What I'd like to do is create a script that takes the existing SAML endpoints and sets them as variables so that I can then add them all back along with the new endpoint.
If there's only one existing endpoint, I can put it into a variable using this and it works:
$EP = New-AdfsSamlEndpoint -Binding "POST" -Protocol "SAMLAssertionConsumer" -Uri "https://test.com" -Index 1
$EP1 = Get-ADFSRelyingPartyTrust -Name "X" | Select-Object -ExpandProperty SamlEndpoints
Set-AdfsRelyingPartyTrust -TargetName "PsTest" -SamlEndpoint $EP,$EP1
The problem with this is that, if multiple endpoints exist, expand-property returns them all as a single value which breaks the function. Using "-limit 1" doesn't work because the whole output of expand-property is considered 1.
What I can do is to generate a numbered list of each index value using this command:
Get-AdfsRelyingPartyTrust -Name "X" | Select-Object -ExpandProperty SamlEndpoints | Select-Object -ExpandProperty Index
and then create a unique variable for each corresponding index value
$EP1 = Get-ADFSRelyingPartyTrust -Name "X" | Select-Object -ExpandProperty SamlEndpoints | Where-Object {$_.Index -eq 2}
But in order to completely script this rather than setting variables by hand, I'd need automate setting "$_.Index -eq" to each index value that's output from "-ExpandProperty Index", and to assign a unique variable to each of those, which is where I'm stuck. What's the best way to approach this?
I don't have access to these command so I am having to guess a little here, but it looks like your command
Set-AdfsRelyingPartyTrust -TargetName "PsTest" -SamlEndpoint $EP,$EP1
accepts an array for the -samlEndpoint parameter.
What I would do it work with the arrays like so.
$EP = New-AdfsSamlEndpoint -Binding "POST" -Protocol "SAMLAssertionConsumer" -Uri "https://test.com" -Index 1
$EndPoints = #(Get-ADFSRelyingPartyTrust -Name "X" | Select-Object -ExpandProperty SamlEndpoints)
$Endpoints += $EP
Set-AdfsRelyingPartyTrust -TargetName "PsTest" -SamlEndpoint $EndPoints

PowerShell TFS REST-API object loop advise

I have a piece of code that i managed to get working, but i feel that it can be written a lot easier. Im new with PowerShell and am trying to understand it better. I have a double foreach below to get the key and value out of the PSCustomObject that comes out of the TFS REST-API call.
For some reason im doing 2 loops, but i dont understand why this is required.
A sample of the contents of $nameCap.userCapabilities is
Name1 Name2
----- -----
Value1 Value2
So basically i want to loop over the "name/value pairs" and get their values.
What can i do better ?
$uri = "$tfsUri/_apis/distributedtask/pools/$global:agentPoolId/agents?api-version=3.0-preview&includeCapabilities=true"
$result = (Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -UseDefaultCredentials).value | select name, userCapabilities, systemCapabilities
#Loop over all agents and their capablities
foreach ($nameCap in $result)
{
$capabilityNamesList = New-Object System.Collections.ArrayList
#Loop over all userCapabilities and store their names
#($nameCap.userCapabilities) | %{
$current_Cap = $_
$req_cap_exists = $false
Get-Member -MemberType Properties -InputObject $current_Cap | %{
$temp_NAME = $_.Name
$temp_Value = Select-Object -InputObject $current_Cap -ExpandProperty $_.Name
[void]$capabilityNamesList.Add($temp_NAME)
}
}
}
I mean if you just need the Name and value, like userCapabilities, then just select for it.
so:
$result | select Name,userCapabilites
And if it doesn't give you a table automatically, then | ft -force

PowerShell export multiple objects as csv

I have two columns of data the first is a string array the second is actually an object. I am looking for a simple way of exporting this as a csv. I have a version with a foreach loop that builds each string up but it seams like over kill. I have been trying to use select and select object to get it out somehow. Note I am just a beginner at powershell so I may be missing something.
My first attempt:
$data | Select-Object -ExpandProperty reports | Select -ExpandProperty data | Select -ExpandProperty rows | Format-Table $_.dimensions
Results in:
dimensions metrics
---------- -------
{New Visitor, "Mozilla} {#{values=System.Object[]}}
My second one went as far as looping
foreach ($report in $data.reports) {
"Rows:" + $report.data.rows.Count
foreach ($row in $report.data.rows) {
$output = ""
foreach($dim in $row.dimensions) {
$output += $dim + $seporator
}
foreach($met in $row.metrics) {
foreach($v in $met.values) {
$output += $v + $seporator
}
}
#| Out-File -Append C:\Users\linda_l\Documents\debug.txt
$output
}
}
There is a potential for a lot of data here so I would really like to avoid the string building solution.
Note: Data comes from the Google Analytics reporting api
$data = Invoke-RestMethod -ContentType 'application/json' -Uri "https://analyticsreporting.googleapis.com/v4/reports:batchGet?access_token=$($token.access_token)" -Method POST -Body $analyticsRequest
reports : {#{columnHeader=; data=}}
From comment:
$data | Export-Csv -Path "C:\Users\linda_l\Documents\debug2.csv"
System.Management.Automation.PSCustomObject reports System.Object[]
Optimal output csv
New Visitor,S40 Ovi Browser,1,2
New Visitor,Safari,3,4
Note its up on Github steps for getting a refreshtoken
Data is coming from the Google analytics reporting API