使用 runspacepools 导入联系人

Contacts import using runspacepools

我正在尝试使用 RunspacePools 导入联系人,但无法正常工作。如果我把它从 运行 空间逻辑中取出来,它工作正常,只是需要很长时间。我真的很想使用 运行spacepools 来加快导入过程并使它 运行 多线程以便导入更快。平均每个用户每次导入大约需要 5-6 分钟,而我有大约 500 个用户,因此 运行.

最多可能需要 3000 分钟

这是我目前拥有的:

#---------------------------------------------
. $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
    }
}

我的“UpdateContacts.ps1”文件如下所示:

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
        }
}

感谢任何帮助。

编辑: 这是完整的脚本(ContactUploader 除外。ps1 脚本。该函数位于单独的脚本中,但整个代码(函数)已在上面发布)。

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" ""

有一大堆代码需要完成,所以我将为您提供一个蓝图,说明如何使用 ThreadJob.

处理 $users 中的所有用户

因此,我会逐步添加我认为合适的评论,以指导您完成思考过程。

我不确定你的函数的输出是什么,因为我在 Invoke-RestMethod 的末尾看到一个 | Out-Null。您需要对此进行澄清。

# 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