I'm trying to import contacts using RunspacePools, but I'm having trouble getting it to work. If I take it out of the runspace logic, it works fine, just takes a long time. I'd really like to use runspacepools to speed up the import process and make it run multithreaded so it imports faster. On avg each import takes about 5-6 mins per user, and I have about 500 users, so it can take up to 3000 mins to run.
Here is what I currently have:
#---------------------------------------------
. $rootPath\UpdateContacts\UpdateContacts.ps1
# Set up runspace pool
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,10)
$RunspacePool.Open()
# Assign new jobs/runspaces to a variable
$Runspaces = foreach ($User in $Users)
{
# Create new PowerShell instance to hold the code to execute, add arguments
$PSInstance = [powershell]::Create().AddScript({
$Users | ForEach{ UpdateContacts($_) }
}).AddParameter('$_')
# Assing PowerShell instance to RunspacePool
$PSInstance.RunspacePool = $RunspacePool
# Start executing asynchronously, keep instance + IAsyncResult objects
New-Object psobject -Property #{
Instance = $PSInstance
IAResult = $PSInstance.BeginInvoke()
Argument = $User
}
}
# Wait for the the runspace jobs to complete
while($Runspaces |Where-Object{-not $_.IAResult.IsCompleted})
{
Start-Sleep -Milliseconds 500
}
# Collect the results
$Results = $Runspaces |ForEach-Object {
$Output = $_.Instance.EndInvoke($_.IAResult)
New-Object psobject -Property #{
User = $User
}
}
And my "UpdateContacts.ps1" file looks like this:
Function UpdateContacts($User)
{
Write-host "Importing Contacts. This could take several minutes."
#FirstName, MiddleName, LastName, DisplayName, SamAccountName, Email, Mobile, TelephoneNumber, Title, Dept, Company, Photo, ExtensionAttribute2
$ContactsBody = #"
{
"givenName" : "$($User.FirstName)",
"middleName" : "$($User.MiddleName)",
"surname" : "$($User.LastName)",
"displayName" : "$($User.DisplayName)",
"jobTitle" : "$($User.Title)",
"companyName" : "$($User.Company)",
"department" : "$($User.Dept)",
"mobilePhone" : "$($User.Mobile)",
"homePhones" : ["$($User.TelephoneNumber)"],
"emailAddresses":
[
{
"address": "$($User.Email)",
"name": "$($User.DisplayName)"
}
]
}
"#
Try
{
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId/contacts" -Headers $headers -Body $ContactsBody -Method Post -ContentType 'application/json' | Out-Null
#After each user clear the info
$User = $NULL
}
Catch
{
if($error)
{
$User
$error
pause
}
$_.Exception.Message
Write-Host "--------------------------------------------------------------------------------------"
$_.Exception.ItemName
}
}
Any help is appreciated.
EDIT:
Here is the full script (with the exception of the ContactUploader.ps1 script. That function is in a separate script but the whole code (Function) is posted above).
CLS
##################### Import Thread/Job Module to perform multithreading #####################
if(!(Get-Module -ListAvailable -Name ThreadJob))
{
$NULL = Install-Module -Name ThreadJob -Scope CurrentUser -Force -Confirm:$False
}
##################### ------------------------------------------------------------- #####################
#Root Path
$rootPath = $(Split-path $MyInvocation.MyCommand.path -Parent)
#Prevent connection from closing on us when we use "Invoke-RestMethod"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
Add-Content "$rootPath\progress.txt" ""
Add-Content "$rootPath\progress.txt" "********** Starting Script $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
##################### Connect to Microsoft Graph API and configure all of our Variables #####################
Add-Content "$rootPath\progress.txt" "********** Connecting to Graph API $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$ApplicationID = "ApplicationID"
$TenatDomainName = "domain.com"
$AccessSecret = "ItsASecret"
$global:Body = #{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
client_Id = $ApplicationID
Client_Secret = $AccessSecret
}
$ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenatDomainName/oauth2/v2.0/token" -Method POST -Body $Body
Add-Content "$rootPath\progress.txt" "********** Finished Connecting to Graph API $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$global:token = $ConnectGraph.access_token
$global:UPN = "user#domain.com"
$global:AccessToken = $token
$global:User = $NULL
$global:Contact = $NULL
$global:NeedsToBeAdded = $NULL
$global:NeedsToBeDeleted = $NULL
$global:folderId = $NULL
$global:NewContactFolder = $NULL
$global:FolderName = "Test Contacts"
$global:headers = #{
"Authorization" = "Bearer $AccessToken"
"Accept" = "application/json;odata.metadata=none"
"Content-Type" = "application/json; charset=utf-8"
"ConsistencyLevel" = "eventual"
}
#Create Contact Folder if it doesn't exist
$global:ContactsFolderBody = #"
{
"parentFolderId": "$ParentFolderID",
"displayName": "Test Contacts"
}
"#
Add-Content "$rootPath\progress.txt" "********** Grabbing Contact Folder Info $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$global:folders = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders" -Headers $headers
$global:ParentFolderID = $folders[0].value.parentFolderId
#Get Folder ID we are working with
foreach($folder in $folders.value)
{
#Reset the Value
$folderId = $NULL
if($FolderName -eq $folder.displayName)
{
$folderId = $folder.id
break
}
}
Add-Content "$rootPath\progress.txt" "********** Finished Grabbing Contact Folder Info $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
##################### Check if our Contacts Folder exists. If it doesn't, create it. #####################
if($NULL -eq $folderId)
{
Add-Content "$rootPath\progress.txt" "********** Creating Contact Folder $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$Start = Get-Date
Write-Host "Creating Contacts Folder"
Try
{
while($NULL = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId" -Headers $headers -Method get))
{
$NewContactFolder = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders" -Body $ContactsFolderBody -Headers $headers -Method post -ContentType 'application/json'
sleep -Milliseconds 1
$folderId = $($NewContactFolder.id)
}
}
Catch
{
Out-Null
}
$End = Get-Date
Write-Host "Contacts Folder created in $($Start - $End) seconds"
Write-Host ""
Add-Content "$rootPath\progress.txt" "********** Finished Creating Contact Folder $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
}
##################### Grab all of our User Information from AD #####################
Add-Content "$rootPath\progress.txt" "********** Grabbing AD User Info $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$Start = Get-Date
$searcher=[adsisearcher]""
$searcher.Sort.PropertyName = "sn"
$searcher.Filter = "(&(objectcategory=person)(objectclass=user)(extensionAttribute2=custom)(|(mobile=*)(telephonenumber=*)))"
$colProplist = #(
'givenname', 'extensionattribute2'
'initials', 'mobile', 'telephonenumber'
'sn', 'displayname', 'company'
'title', 'mail', 'department'
'thumbnailphoto', 'samaccountname'
)
$colPropList | & { process {
$NULL = $searcher.PropertiesToLoad.Add($_)
}}
$End = Get-Date
Write-Host "User info took $($Start - $End) seconds"
Write-Host ""
Add-Content "$rootPath\progress.txt" "********** Finished Grabbing AD User Info $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
##################### Create our User Hashtable #####################
Add-Content "$rootPath\progress.txt" "********** Creating User Hashtable $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
Write-Host "Creating User Hashtable"
$Start = Get-Date
$users = $searcher.FindAll() | & { process {
[pscustomobject]#{
FirstName = [string]$_.properties.givenname
MiddleName = [string]$_.properties.initials
LastName = [string]$_.properties.sn
DisplayName = [string]$_.properties.displayname
SamAccountName = [string]$_.properties.samaccountname
Email = [string]$_.properties.mail
Mobile = [string]$_.properties.mobile
TelephoneNumber = [string]$_.properties.telephonenumber
Title = [string]$_.properties.title
Dept = [string]$_.properties.department
Company = [string]$_.properties.company
Photo = [string]$_.properties.thumbnailphoto
ExtensionAttribute2 = [string]$_.properties.extensionattribute2
}
}}
Write-Host "User Hashtable took $($Start - $End) seconds"
Write-Host ""
Add-Content "$rootPath\progress.txt" "********** Finished Creating User Hashtable $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
##################### Get Existing Contacts (Only if the Contacts Folder wasn't newly created )#####################
if($NULL -ne $folderId)
{
Add-Content "$rootPath\progress.txt" "********** Grabbing Contact Info $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$Start = Get-Date
$AllContacts = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId/contacts?`$top=999&`$Orderby=Surname" -Headers $headers -Method Get
$End = Get-Date
Write-Host "Contact info took $($Start - $End) seconds"
Write-Host ""
Add-Content "$rootPath\progress.txt" "********** Finished Grabbing Contact Info $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
##################### Create our Contact Hashtable #####################
Add-Content "$rootPath\progress.txt" "********** Creating Contact Hashtable $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
Write-Host "Creating Contact Hashtable"
$Start = Get-Date
$Contacts = $AllContacts.value | & { process {
[PSCustomObject]#{
'FirstName' = [string]$_.givenName
'MiddleName' = [string]$_.initials
'LastName' = [string]$_.surname
'DisplayName' = [string]$_.displayName
'Email' = [string](($_.emailAddresses) | %{$_.Address})
'Mobile' = [string]$_.mobilePhone
'TelephoneNumber' = [string]$_.homePhones
'Title' = [string]$_.jobTitle
'Dept' = [string]$_.department
'Company' = [string]$_.companyName
}
}}
$End = Get-Date
Write-Host "Contact HashTable took $($Start - $End) seconds"
Write-Host ""
Add-Content "$rootPath\progress.txt" "********** Finished Creating Contact Hashtable $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
}
##################### Start Comparing Data #####################
#Our Array of values we will be comparing
[array]$CompareValues = "FirstName","MiddleName","LastName","DisplayName","Email","Mobile","TelephoneNumber","Title","Dept","Company"
for($i=0; $i -lt $CompareValues.Count; $i++)
{
#First let's create 2 variables that will hold the info we want
$A = ($Users).($CompareValues[$i])
$B = ($Contacts).($CompareValues[$i])
##################### Update Contacts #####################
#Only Run if there are contacts; otherwise there is nothing for us to compare
if(($NULL -ne $B))
{
#Displays all differences
#$Differences = [string[]]([Linq.Enumerable]::Except([object[]]$a, [object[]]$b) + [Linq.Enumerable]::Except([object[]]$b, [object[]]$a))
#Displays what accounts we need to import
$NeedsToBeAdded = [string[]]([Linq.Enumerable]::Except([object[]]$a, [object[]]$b))
#Displays what accounts we need to delete because they no longer exist
$NeedsToBeDeleted = [string[]]([Linq.Enumerable]::Except([object[]]$b, [object[]]$a))
}
##################### Import All Contacts #####################
Else
{
Add-Content "$rootPath\progress.txt" "********** Importing All Contacts $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
$Start = Get-Date
<#
#---------------------------------------------
. $rootPath\UpdateContacts\UpdateContacts.ps1
# Set up runspace pool
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,10)
$RunspacePool.Open()
# Assign new jobs/runspaces to a variable
$Runspaces = foreach ($User in $Users)
{
# Create new PowerShell instance to hold the code to execute, add arguments
$PSInstance = [powershell]::Create().AddScript({
$Users | ForEach{ UpdateContact($_) }
}).AddParameter('$_')
# Assing PowerShell instance to RunspacePool
$PSInstance.RunspacePool = $RunspacePool
# Start executing asynchronously, keep instance + IAsyncResult objects
New-Object psobject -Property #{
Instance = $PSInstance
IAResult = $PSInstance.BeginInvoke()
Argument = $User
}
}
# Wait for the the runspace jobs to complete
while($Runspaces |Where-Object{-not $_.IAResult.IsCompleted})
{
Start-Sleep -Milliseconds 500
}
# Collect the results
$Results = $Runspaces |ForEach-Object {
$Output = $_.Instance.EndInvoke($_.IAResult)
New-Object psobject -Property #{
User = $User
}
}
#---------------------------------------------
#>
Write-host "Importing Contacts. This could take several minutes."
#There are no contacts, so let's import them
#Path to our script that imports Contacts
. $rootPath\UpdateContacts\UpdateContacts.ps1
#$Users | & { process { UpdateContacts($_) } }
#Start-ThreadJob -ScriptBlock { $Users | & { process { UpdateContacts($_) } } }
Start-ThreadJob -ScriptBlock { $Users | ForEach{ UpdateContacts($_) } }
Get-Job
$End = Get-Date
Write-Host "Contact Import took $($Start - $End) seconds"
Write-Host ""
Add-Content "$rootPath\progress.txt" "********** Finished Importing All Contacts $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
break
}
}
Add-Content "$rootPath\progress.txt" "********** Finished Script $(Get-Date -Format "HH:mm ss")**********"
Add-Content "$rootPath\progress.txt" ""
There is a bunch of code to go through so I'm gonna give you a blueprint of how you can achieve processing all users in $users using ThreadJob.
So, step by step, I'll try to add as much comments as I consider appropriate to guide you through the thought process.
I'm not sure what is the output of your function since I see an | Out-Null at the end of the Invoke-RestMethod. You would need to clarify on this.
# requires -Modules ThreadJob
# Load UpdateContacts function in memory
. "$rootPath\UpdateContacts\UpdateContacts.ps1"
# Save the function in a scriptBlock, we need this
# so we can pass this function in the scope of the ThreadJobs
$updateContacts = "function UpdateContacts { $function:updateContacts }"
# Define the Number of Threads we are going to use
# (Get-CimInstance win32_processor).NumberOfLogicalProcessors
# Can give you a good perspective as to how many Threads is safe to use.
$numberOfThreads = 10
# I'm assuming that $users is the array we want to process with
# the UpdateContacts function. Around 500 as you said in your question.
# Here I'm grouping the users in chunks so each running Job can process
# a chunk of users. Each chunk will contain around 50 users to process.
$groupSize = [math]::Ceiling($users.Count / $numberOfThreads)
$counter = [pscustomobject]#{ Value = 0 }
$chunks = $users | Group-Object -Property {
[math]::Floor($counter.Value++ / $groupSize)
}
# Here is the magic
foreach($chunk in $chunks)
{
# Capture this chunk of users in a variable
$thisGroup = $chunk.Group
# This is what we are running inside the scope
# of our threadJob
$scriptBlock = {
# As in my comments, these variables don't exist inside here,
# you need to pass them to these scope
$UPN = $using:UPN
$folderID = $using:folderId
$headers = $using:headers
$contactsBody = $using:contactsBody
# First we need to define the function inside
# this scope
. ([scriptBlock]::Create($using:updateContacts))
# Loop through each user
foreach($user in $using:thisGroup)
{
UpdateContacts -User $user
}
} # EOF Job's ScriptBlock
# ThrottleLimit is the number of Jobs that can run at the same time.
# Be aware, a higher number of Jobs running does NOT mean that the
# task will perform faster. This always depends on your CPU & Memory.
# And, this case in particular, the number of requests your URI is able to handle
Start-ThreadJob -ScriptBlock $scriptBlock -ThrottleLimit $numberOfThreads
}
# Now we should have 10 Jobs running at the same time, each Job
# is processing a chunk of 50 users aprox. (500 users / 10)
# Note: As in my previous comments, I see an Out-Null in your function
# not sure what is meant to return but in case, this is how you capture
# the output of all Jobs:
$result = Get-Job | Receive-Job -Wait
# Free up memory:
Get-Job | Remove-Job
Related
I need to Run this PowerShell script in different servers remotely with credentials & collect data in single csv and send to email. i Include the Invoke Command, but getting error.
$servers = Get-Content "c:\serverlist\servers.txt"
$Report = foreach ($server in $servers){
Invoke-Command -ComputerName $server -ScriptBlock
}
{
$props = #(
#{
N='ServerName'
E={ $env:COMPUTERNAME }
}
'StartDate'
'EndDate'
'ConnectorName'
'RunProfileName'
'StepResult'
)
$profiles = #(
'Delta Import'
'Delta Synchronization'
'Full Import'
'Full Synchronization'
) -join '|'
Get-ADSyncToolsRunstepHistory | Group-Object RunProfileName |
Where-Object Name -Match $profiles | ForEach-Object {
$_.Group | Select-Object -First 2
} } | Select-Object $props | Export-Csv c:\serverlist\export.csv -NoTypeInformation
I followed the steps to Send the date of the csv file to the email
In PowerShell ISE I have created a file and replaced the code below
the other method is a powerShell
$CSVFile ="C:\Users\Public\Downloads\users.csv"
$userList = import-csv $CSVFile
$subject = " Here is your Details"
$from ="yourmail"
$passwrod ="your password"|convertTo-secureString -asPlainText -force
$creds = new-object -TypeName System.Management.Automation.PSCredential -ArgumentList $from ,$passwrod
foreach ($user in $userList){
$body = "your Login Id is" +$user.loginname
$body += "your password is" + $user.Passwrod
$body +="your email id is " + $user.mail
Send-MailMessage -From $from $user.mail -Subject $subject -body $body - SmtpServer "pujarisampath80#gmail.com" -UseSs1 -port 587 -Credential $creds
}
I have created csv file In Excel
With .csv extension
Provide your Email address and password and click on Run and check the mail
I am trying to work on an existing script that I had some assistance from in another thread. With some member assistance I was able to get my script to run using the "ThreadJob" module, however I was hoping I can also make use of runspacepools in conjunction with the ThreadJob to make it run faster.
In my code I post below, I am printing out a line to notify me that the function get's called. And I can see it is getting called. So it makes me think that line 56 and line 59 are incorrectly being called and I can't figure out how to call them.
if I run the "$rootPath\UpdateContacts\UpdateContacts.ps1" file manually through powershell ISE, it runs fine (obviously outside of the runspace), but I'd like to try and get it to work within the runspacepool.
Here is what I'm working with. I think I am pretty close.
begin
{
CLS
[switch]$MultiThread=$true
$rootPath = $(Split-path $MyInvocation.MyCommand.path -Parent)
$userEmail = "user#domain.com"
$SessionState = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 10,$Sessionstate, $Host)
$RunspacePool.Open()
$Jobs = #()
}
process
{
function ContactUpdater()
{
##################### Start Comparing Data #####################
#Our Array of values we will be comparing
[array]$CompareValues = "FirstName","MiddleName","LastName","DisplayName","Email","Mobile","TelephoneNumber","Title","Dept","Company"
for($i=0; $i -lt $CompareValues.Count; $i++)
{
#First let's create 2 variables that will hold the info we want
$A = ($Users).($CompareValues[$i])
$B = ($Contacts).($CompareValues[$i])
##################### Update Contacts #####################
#Only Run if there are contacts; otherwise there is nothing for us to compare
if(($NULL -ne $B))
{
#Displays all differences
#$Differences = [string[]]([Linq.Enumerable]::Except([object[]]$a, [object[]]$b) + [Linq.Enumerable]::Except([object[]]$b, [object[]]$a))
#Displays what accounts we need to import
$NeedsToBeAdded = [string[]]([Linq.Enumerable]::Except([object[]]$a, [object[]]$b))
#Displays what accounts we need to delete because they no longer exist
$NeedsToBeDeleted = [string[]]([Linq.Enumerable]::Except([object[]]$b, [object[]]$a))
}
}
##################### Import All Contacts #####################
if($NULL -eq $Contacts)
{
Write-Host "I am in the import"
# Load UpdateContacts function in memory
. "$rootPath\UpdateContacts\UpdateContacts\UpdateContacts.ps1"
#Write-host "Importing Contacts. This could take several minutes."
& "$rootPath\UpdateContacts\UpdateContacts.ps1"
}
}
if($MultiThread)
{
foreach($userEmail in $EmailAddress)
{
try
{
##################### Create Contact Folder #####################
if($NULL -eq $folderId)
{
$start = [datetime]::UtcNow
Write-Host "Creating Contacts Folder"
Try
{
while($NULL = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId" -Headers $headers -Method get))
{
$NewContactFolder = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders" -Body $ContactsFolderBody -Headers $headers -Method post -ContentType 'application/json'
sleep -Milliseconds 1
$folderId = $($NewContactFolder.id)
}
}
Catch
{
Out-Null
}
Add-Content "$rootPath\progress.txt" "`t`tCreated Contacts Folder in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
Add-Content "$rootPath\progress.txt" ""
}
##################### Getting All User Info #####################
$start = [datetime]::UtcNow
$Users = & $rootPath\GetUserInfo\GetUserInfo.ps1
Add-Content "$rootPath\progress.txt" "`t`tFinished Getting all User Info in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
Add-Content "$rootPath\progress.txt" ""
##################### Getting Contact Info #####################
if($NULL -ne $folderId)
{
$start = [datetime]::UtcNow
$Contacts = & $rootPath\GetContactInfo\GetContactInfo.ps1
Add-Content "$rootPath\progress.txt" "`t`tFinished Getting all Contact Info in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
Add-Content "$rootPath\progress.txt" ""
}
##################### Import Contacts #####################
$start = [datetime]::UtcNow
CLS
if($NULL -eq $ImportMsg)
{
Write-host "Importing Contacts. This could take several minutes."
$ImportMsg = "Ran"
}
$ContactImporter = ContactUpdater
Add-Content "$rootPath\progress.txt" "`t`tFinished Importing Contact Info in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
}
catch
{
$LogFile = "$rootPath\log.txt"
$errcond = $_.Exception.Message
$timestamp = (get-date).DateTime
"Time of exception: $timestamp" | Out-File $LogFile -Append
"User: $userEmail" | out-file $LogFile -Append
$errcond | out-file -FilePath $LogFile -append
}
1..10 | Foreach-Object {
$PowershellThread = [powershell]::Create()
$PowershellThread.RunspacePool = $RunspacePool
$PowershellThread.AddScript($ContactImporter).AddArgument($userEmail)
$Jobs += $PowershellThread.BeginInvoke()
}
}
}
}
end
{
if($MultiThread)
{
while ($Jobs.IsCompleted -contains $false)
{
Start-Sleep -Milliseconds 100
}
$RunspacePool.Close() | Out-Null
$RunspacePool.Dispose() | Out-Null
}
}
The part in the "Import all Contacts" section within the ContactUpdater() Function, should call the script:
& "$rootPath\UpdateContacts\UpdateContacts.ps1"
That script looks like this:
# Save the function in a scriptBlock, we need this
# so we can pass this function in the scope of the ThreadJobs
$updateContacts = "function UpdateContacts { $function:updateContacts }"
# Define the Number of Threads we are going to use
# (Get-CimInstance win32_processor).NumberOfLogicalProcessors
# Can give you a good perspective as to how many Threads is safe to use.
$numberOfThreads = 10
# $users is the array we want to process with
# the UpdateContacts function.
# Grouping the users in chunks so each running Job can process
# a chunk of users. Each chunk will contain around 50 users to process.
$groupSize = [math]::Ceiling($users.Count / $numberOfThreads)
$counter = [pscustomobject]#{ Value = 0 }
$chunks = $users | Group-Object -Property {
[math]::Floor($counter.Value++ / $groupSize)
}
foreach($chunk in $chunks)
{
# Capture this chunk of users in a variable
$thisGroup = $chunk.Group
# This is what we are running inside the scope
# of our threadJob
$scriptBlock = {
# Pass our variables to this scope
$UPN = $using:UPN
$folderID = $using:folderId
$headers = $using:headers
$contactsBody = $using:contactsBody
$ImportMsg = $using:ImportMsg
# First we need to define the function inside this scope
. ([scriptBlock]::Create($using:updateContacts))
# Loop through each user
foreach($user in $using:thisGroup)
{
UpdateContacts -User $user
}
}
# ThrottleLimit is the number of Jobs that can run at the same time.
# Be aware, a higher number of Jobs running does NOT mean that the
# task will perform faster. This always depends on your CPU & Memory.
# And, this case in particular, the number of requests your URI is able to handle
Start-ThreadJob -ScriptBlock $scriptBlock -ThrottleLimit $numberOfThreads
}
# Now we should have 10 Jobs running at the same time, each Job
# is processing a chunk of 50 users aprox. (500 users / 10)
# the output of all Jobs:
$result = Get-Job | Receive-Job -Wait
# Free up memory:
Get-Job | Remove-Job
That code above, starts a threadjob and launches another function in "$rootPath\UpdateContacts\UpdateContacts\UpdateContacts.ps1"
And that script looks like this:
Function UpdateContacts($User)
{
#FirstName, MiddleName, LastName, DisplayName, SamAccountName, Email, Mobile, TelephoneNumber, Title, Dept, Company, Photo, ExtensionAttribute2
$ContactsBody = #"
{
"givenName" : "$($User.FirstName)",
"middleName" : "$($User.MiddleName)",
"surname" : "$($User.LastName)",
"fileAs" : "$($User.LastName)",
"displayName" : "$($User.DisplayName)",
"jobTitle" : "$($User.Title)",
"companyName" : "$($User.Company)",
"department" : "$($User.Dept)",
"mobilePhone" : "$($User.Mobile)",
"homePhones" : ["$($User.TelephoneNumber)"],
"emailAddresses":
[
{
"address": "$($User.Email)",
"name": "$($User.DisplayName)"
}
]
}
"#
Try
{
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId/contacts" -Headers $headers -Body $ContactsBody -Method Post -ContentType 'application/json' | Out-Null
#After each user clear the info
$User = $NULL
}
Catch
{
if($error)
{
$User
$error
pause
}
$_.Exception.Message
Write-Host "--------------------------------------------------------------------------------------"
$_.Exception.ItemName
}
}
I have an Azure SQL database sync group that is scheduled to run each hour.
Question:
Can i sent this logs to a log analytics workspace by enabling the diagnostic settings?
If yes, what will be the best way to filter them out?
I can successfully get the logs from powershell, but my end goal here is to create an alert based on the sync logs.
Thanks you in advance!
If you want to send Azure SQL database sync group to a log analytics workspace, you can implement it with HTTP Data Collector API.
For example
$SubscriptionId = "SubscriptionId"
$DS_ResourceGroupName = ""
$DS_ServerName = ""
$DS_DatabaseName = ""
$DS_SyncGroupName = ""
# Replace with your OMS Workspace ID
$CustomerId = "OMSCustomerID"
# Replace with your OMS Primary Key
$SharedKey = "SharedKey"
# Specify the name of the record type that you'll be creating
$LogType = "DataSyncLog"
# Specify a field with the created time for the records
$TimeStampField = "DateValue"
Connect-AzureRmAccount
select-azurermsubscription -SubscriptionId $SubscriptionId
#get log
$endtime =[System.DateTime]::UtcNow
$StartTime = ""
$Logs = Get-AzureRmSqlSyncGroupLog -ResourceGroupName $DS_ResourceGroupName `
-ServerName $DS_ServerName `
-DatabaseName $DS_DatabaseName `
-SyncGroupName $DS_SyncGroupName `
-starttime $StartTime `
-endtime $EndTime;
if ($Logs.Length -gt 0)
{
foreach ($Log in $Logs)
{
$Log | Add-Member -Name "SubscriptionId" -Value $SubscriptionId -MemberType NoteProperty
$Log | Add-Member -Name "ResourceGroupName" -Value $DS_ResourceGroupName -MemberType NoteProperty
$Log | Add-Member -Name "ServerName" -Value $DS_ServerName -MemberType NoteProperty
$Log | Add-Member -Name "HubDatabaseName" -Value $DS_DatabaseName -MemberType NoteProperty
$Log | Add-Member -Name "SyncGroupName" -Value $DS_SyncGroupName -MemberType NoteProperty
#Filter out Successes to Reduce Data Volume to OMS
#Include the 5 commented out line below to enable the filter
#For($i=0; $i -lt $Log.Length; $i++ ) {
# if($Log[$i].LogLevel -eq "Success") {
# $Log[$i] =""
# }
# }
}
$json = ConvertTo-JSON $logs
$result = Post-OMSData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($json)) -logType $logType
if ($result -eq 200)
{
Write-Host "Success"
}
if ($result -ne 200)
{
throw
#"
Posting to OMS Failed
Runbook Name: DataSyncOMSIntegration
"#
}
Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource)
{
$xHeaders = "x-ms-date:" + $date
$stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource
$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
$keyBytes = [Convert]::FromBase64String($sharedKey)
$sha256 = New-Object System.Security.Cryptography.HMACSHA256
$sha256.Key = $keyBytes
$calculatedHash = $sha256.ComputeHash($bytesToHash)
$encodedHash = [Convert]::ToBase64String($calculatedHash)
$authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash
return $authorization
}
# Create the function to create and post the request
Function Post-OMSData($customerId, $sharedKey, $body, $logType)
{
$method = "POST"
$contentType = "application/json"
$resource = "/api/logs"
$rfc1123date = [DateTime]::UtcNow.ToString("r")
$contentLength = $body.Length
$signature = Build-Signature `
-customerId $customerId `
-sharedKey $sharedKey `
-date $rfc1123date `
-contentLength $contentLength `
-method $method `
-contentType $contentType `
-resource $resource
$uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"
$headers = #{
"Authorization" = $signature;
"Log-Type" = $logType;
"x-ms-date" = $rfc1123date;
"time-generated-field" = $TimeStampField;
}
$response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing
return $response.StatusCode
}
After doing that, you can alert vai log search in Azure log analysis. For more detail, please refer to the blog.
I have a PowerShell script that is running a web request as part of a job. With the script block that is run by the job I am logging the Endpoint Uri and the response time. and then after all the jobs are finished but before I remove the jobs I am trying to export the result into excel.
PS Verion:
Major Minor Build Revision
----- ----- ----- --------
5 1 17763 503
Code:
$Code = {
Param(
[array]$Domain,
[array]$Services,
[array]$TestData,
[string]$Path
)
$Log = #()
## Get Random School ##
$Random = Get-Random -InputObject $TestData -Count 1
## Get Random Service ##
$RandService = Get-Random -InputObject $Services
## Get Random Endpoint ##
$ServiceEndpoints = Get-Content "$Path\Service.Endpoints.json" | Out-String | ConvertFrom-Json
$GatewayEndpoints = $ServiceEndpoints.Services.$RandService.Gateway
$RandomEndpoint = Get-Random -InputObject $GatewayEndpoints
$Headers = #{
"Authorization" = "Bearer" + ' ' + $Random.Token
}
$Uri = 'https://' + $Domain + $RandomEndpoint
Try {
$TimeTaken = Measure-Command -Expression {
$JsonResponse = Invoke-WebRequest -Uri $Uri -Headers $Headers -ContentType 'application/json' -Method Get -UseBasicParsing
}
}
Catch {
}
$ResponseTime = [Math]::Round($TimeTaken.TotalMilliseconds, 1)
$LogItem = New-Object PSObject
$LogItem | Add-Member -type NoteProperty -Name 'Endpoint' -Value $Uri
$LogItem | Add-Member -type NoteProperty -Name 'Time' -Value $ResponseTime
$Log += $LogItem
Write-Host $Log
}
#Remove all jobs
Get-Job | Remove-Job
#Start the jobs. Max 4 jobs running simultaneously.
foreach($Row in $TestData){
While ($(Get-Job -state running).count -ge $MaxThreads){
Start-Sleep -Milliseconds 3
}
Start-Job -Scriptblock $Code -ArgumentList $Domain, $Services, $TestData, $Path
}
#Wait for all jobs to finish.
While ($(Get-Job -State Running).count -gt 0) {
start-sleep 1
}
$Log | Export-XLSX -Path .\Test.Results\Performance\Performance.Test.Log.xlsx -ClearSheet
#Get information from each job.
foreach($Job in Get-Job) {
$Info = Receive-Job -Id ($Job.Id)
}
#Remove all jobs created.
Get-Job | Remove-Job
I cannot seem to get the endpoint uri and the response time out of the script block. When I try to export the $Log, all that happens is it creates an empty excel file.
Write-Host $Log
#{Endpoint=https://domain/customer/v1/years/2019/marks; Time=1233.3}
#{Endpoint=https://domain/customer/v1/years/2019/marks; Time=2131.7}
You've to return $Log in your script block, since $Log lives in another scope. You can return $Log in your $Code script block, and finally, fetch it via Receive-Job.
Change your code to:
$Code = {
...
$Log += $LogItem
Write-Host $Log
$Log # return via pipeline to the caller
}
As Niraj Gajjar's answer suggests you can use Export-Csv cmdlet to create an Excel file:
#Get information from each job.
foreach($Job in Get-Job) {
Receive-Job -Id ($Job.Id) | Export-CSV -Path .\Test.Results\Performance\Performance.Test.Log.xlsx -Append -NoTypeInformation
}
Global variables inside of jobs aren't visible in the main script since job is running in a new session with its own global space.
You can use Export-Csv to open file in excel.
sample code with multiple jobs to CSV format :
$jobs = #() # INITILIZING ARRAY
$jobs += Start-Job { appwiz.cpl } # START JOB 1 AND ADDING TO ARRAY
$jobs += Start-Job { compmgmt.msc } # START JOB 2 AND ADDING TO ARRAY
$jobs += Start-Job { notepad.exe } # START JOB 3 AND ADDING TO ARRAY
foreach ( $job in $jobs) # INTERATION OF JOBS
{
Export-Csv -InputObject $job "C:\result.csv" -Append # SAVING TO FILE
}
Note : There are some internal properties of job which are not converted by CSV because depth is 1 but some basic properties are available.
Adapting a script to do multiple functions, starting with test-connection to gather data, will be hitting 6000+ machines so I am using RunspacePools adapted from the below site;
http://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
The data comes out as below, I would like to get it sorted into an array (I think that's the terminology), so I can sort the data via results. This will be adapted to multiple other functions pulling anything from Serial Numbers to IAVM data.
Is there any way I can use the comma delimited data and have it spit the Values below into columns? IE
Name IPAddress ResponseTime Subnet
x qwe qweeqwe qweqwe
The added values aren't so important at the moment, just the ability to add the values and pull them.
Name Value
—- —–
x-410ZWG \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410ZWG",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-47045Q \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-47045Q",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-440J26 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J26",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410Y45 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410Y45",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DJKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DJKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
nonexistant
x-DDMVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DDMVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-470481 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-470481",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DHKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DHKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-430XXF \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-430XXF",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DLKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DLKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410S86 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410S86",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-SCH004 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-SCH004",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-431KMS
x-440J22 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J22",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
Thank for any help!
Code currently
Function Get-RunspaceData {
[cmdletbinding()]
param(
[switch]$Wait
)
Do {
$more = $false
Foreach($runspace in $runspaces) {
If ($runspace.Runspace.isCompleted) {
$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
} ElseIf ($runspace.Runspace -ne $null) {
$more = $true
}
}
If ($more -AND $PSBoundParameters['Wait']) {
Start-Sleep -Milliseconds 100
}
#Clean out unused runspace jobs
$temphash = $runspaces.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
Write-Verbose ("Removing {0}" -f $_.computer)
$Runspaces.remove($_)
}
Write-Host ("Remaining Runspace Jobs: {0}" -f ((#($runspaces | Where {$_.Runspace -ne $Null}).Count)))
} while ($more -AND $PSBoundParameters['Wait'])
}
#Begin
#What each runspace will do
$ScriptBlock = {
Param ($computer,$hash)
$Ping = test-connection $computer -count 1 -ea 0
$hash[$Computer]= $Ping
}
#Setup the runspace
$Script:runspaces = New-Object System.Collections.ArrayList
# Data table for all of the runspaces
$hash = [hashtable]::Synchronized(#{})
$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$runspacepool = [runspacefactory]::CreateRunspacePool(1, 100, $sessionstate, $Host)
$runspacepool.Open()
#Process
ForEach ($Computer in $Computername) {
#Create the powershell instance and supply the scriptblock with the other parameters
$powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($hash)
#Add the runspace into the powershell instance
$powershell.RunspacePool = $runspacepool
#Create a temporary collection for each runspace
$temp = "" | Select-Object PowerShell,Runspace,Computer
$Temp.Computer = $Computer
$temp.PowerShell = $powershell
#Save the handle output when calling BeginInvoke() that will be used later to end the runspace
$temp.Runspace = $powershell.BeginInvoke()
Write-Verbose ("Adding {0} collection" -f $temp.Computer)
$runspaces.Add($temp) | Out-Null
}
# Wait for all runspaces to finish
#End
Get-RunspaceData -Wait
$stoptimer = Get-Date
#Display info, and display in GridView
Write-Host
Write-Host "Availability check complete!" -ForegroundColor Cyan
"Execution Time: {0} Minutes" -f [math]::round(($stoptimer – $starttimer).TotalMinutes , 2)
$hash | ogv
When you use runspaces, you write the scriptblock for the runspace pretty much the same way you would for a function. You write whatever you want the return to be to the pipeline, and then either assign it to a variable, pipe it to another cmdlet or function, or just let it output to the console. The difference is that while the function returns it's results automatically, with the runspace they collect in the runspace output buffer and aren't returned until you do the .EndInvoke() on the runspace handle.
As a general rule, the objective of a Powershell script is (or should be) to create objects, and the objective of using the runspaces is to speed up the process by multi-threading. You could return string data from the runspaces back to the main script and then use that to create objects there, but that's going to be a single threaded process. Do your object creation in the runspace, so that it's also multi-threaded.
Here's a sample script that uses a runspace pool to do a pingsweep of a class C subnet:
Param (
[int]$timeout = 200
)
$scriptPath = (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent)
While (
($network -notmatch "\d{1,3}\.\d{1,3}\.\d{1,3}\.0") -and -not
($network -as [ipaddress])
)
{ $network = read-host 'Enter network to scan (ex. 10.106.31.0)' }
$scriptblock =
{
Param (
[string]$network,
[int]$LastOctet,
[int]$timeout
)
$options = new-object system.net.networkinformation.pingoptions
$options.TTL = 128
$options.DontFragment = $false
$buffer=([system.text.encoding]::ASCII).getbytes('a'*32)
$Address = $($network.trim("0")) + $LastOctet
$ping = new-object system.net.networkinformation.ping
$reply = $ping.Send($Address,$timeout,$buffer,$options)
Try { $hostname = ([System.Net.Dns]::GetHostEntry($Address)).hostname }
Catch { $hostname = 'No RDNS' }
if ( $reply.status -eq 'Success' )
{ $ping_result = 'Yes' }
else { $ping_result = 'No' }
[PSCustomObject]#{
Address = $Address
Ping = $ping_result
DNS = $hostname
}
}
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(100,100)
$RunspacePool.Open()
$Jobs =
foreach ( $LastOctet in 1..254 )
{
$Job = [powershell]::Create().
AddScript($ScriptBlock).
AddArgument($Network).
AddArgument($LastOctet).
AddArgument($Timeout)
$Job.RunspacePool = $RunspacePool
[PSCustomObject]#{
Pipe = $Job
Result = $Job.BeginInvoke()
}
}
Write-Host 'Working..' -NoNewline
Do {
Write-Host '.' -NoNewline
Start-Sleep -Seconds 1
} While ( $Jobs.Result.IsCompleted -contains $false)
Write-Host ' Done! Writing output file.'
Write-host "Output file is $scriptPath\$network.Ping.csv"
$(ForEach ($Job in $Jobs)
{ $Job.Pipe.EndInvoke($Job.Result) }) |
Export-Csv $scriptPath\$network.ping.csv -NoTypeInformation
$RunspacePool.Close()
$RunspacePool.Dispose()
The runspace script does a ping on each address, and if it gets successful ping attempts to resolve the host name from DNS. Then it builds a custom object from that data, which is output to the pipeline. At the end, those objects are returned when the .EndInvoke() is done on the runspace jobs and piped directly into Export-CSV, but it could just as easily be output to the console, or saved into a variable.