marcimarc85
Goto Top

Foreach Powershell Schleife parallel abarbeiten

Hallo,

Wie kann ich denn eine Foreach Schleife in Powershell parallel abarbeiten?

Aktuell werden auf mehreren Computern nacheinander bestimmte Dinge ausgeführt:

foreach ($computer in $computers) {
mach was
}

Die abarbeitung duaer teils sehr lange, da es meist über 10 Computer sind, die nacheinander abgearbeitet werden. In manchen Fällen werden auch länger laufende Aktionen auf den einzelnen Computern ausgeführt, was dazu führt, dass das Script mehrere Stunden benötigt, bis es mit allen Servern durch ist.
Schön wäre es, wenn die foreach Schleife immer 5 oder mehr Server parallell abarbeitet .

ich hab da jetzt was zu gefunden, wie das mit Jobs funktioniert :https://stackoverflow.com/questions/43685522/running-tasks-parallel-in-p ...

Bin aber soweit in Powershell nicht fit genug um zu wissen, wie ich die Schleife dahingehend umbauen kann.

Content-ID: 24139179460

Url: https://administrator.de/contentid/24139179460

Ausgedruckt am: 22.11.2024 um 05:11 Uhr

12764050420
Lösung 12764050420 25.04.2024 aktualisiert um 09:59:08 Uhr
Goto Top
Hi.
Ab PowerShell 7 gibt es den Parameter -parallel für foreach-object der das für dich übernimmt.
Jobs wurde ich persönlich nicht nutzen wollen da die in der Initialisierung doch um einiges langsamer als Runspaces sind, wenn dich das nicht stört kannst du die natürlich auch nutzen mittels Start-Job und dein Skript im Skriptblock.
Lies dir dazu mal diese Serie durch
Beginning Use of PowerShell Runspaces: Part 1
Beginning Use of PowerShell Runspaces: Part 2
Beginning Use of PowerShell Runspaces: Part 3

Gruß schrick
SlainteMhath
Lösung SlainteMhath 25.04.2024 um 09:44:14 Uhr
Goto Top
Moin,

1. $block anpassen mit deinem Code
$block = {
    Param([string] $computer)
    "mach was"  
}

2. foreach-Schleife anpassen
foreach($computer in $computer){
[,,,]
    Start-Job -Scriptblock $Block -ArgumentList $ccomputer
}

3. ???
4. Profit!

(alles ohne Gewähr)

lg,
Slainte
Crusher79
Crusher79 25.04.2024 aktualisiert um 10:16:23 Uhr
Goto Top
Oben wäre eine Form der Anmeldung. Hier mit Keyfile.

Nur so mal eingeworfen. Wenn man direkt davor sitzt kann Kennwort auch selber kurz eingeben.

Hier sieht man nochmal die Methoden wie man Namen und Passwörter übergeben könnte. Die Keyfile wird so generiert, dass sie auf anderen Windows Rechnern lesbar ist.

Klar ist der Punkt Sicherheit nur so Lala. Hat man die Datei, kann man die verwurschten. Wir haben es auch nun anders gelöst.

Aber so könnte man Windows Creds generieren und damit Script bedienen.

Kern ist und bleibt aber Start-Job.

$KeyFile = "D:\test\pass_admin.key"  
$Key = New-Object Byte[] 32   # You can use 16, 24, or 32 for AES
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
$Key | out-file $KeyFile



$PasswordFile = "D:\test\pass_super_geheim.txt"  
$KeyFile = "D:\test\pw.key"  
$Key = Get-Content $KeyFile
$Password = "SuperGeheimesKennwort" | ConvertTo-SecureString -AsPlainText -Force  
$Password | ConvertFrom-SecureString -key $Key | Out-File $PasswordFile

$USERNAME = $WinUserName
            $DOMUSER = $($WinDom)+"\"+$($WinUserName)                  
            $PasswordFile = $WinUserPwdFile
            $KeyFile = $WinUserKeyFile
            $key = Get-Content $KeyFile
            $MyCredential = New-Object -TypeName System.Management.Automation.PSCredential `
                -ArgumentList $USERNAME, (Get-Content $($PasswordFile) | ConvertTo-SecureString -Key $($key))
            $PASSWORD = $MyCredential.GetNetworkCredential().password

            $passwdsec = convertto-securestring -AsPlainText -Force -String $PASSWORD                
            $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $DOMUSER,$passwdsec

             Invoke-Command -ComputerName $WinSrvHost -Credential $cred `
                -ScriptBlock { `
                    $JobA = Start-Job { `
                            stop-service -name 'SQLSERVERAGENT' -force -passThru -ErrorAction Continue `  
                        }; `    
                    Start-Sleep 30 ; `    
                    $JobS = Start-Job { `
                            stop-service -name 'MSSQLSERVER' -force -passThru -ErrorAction Continue `  
                        }; `
                    Start-Sleep 30; `
                    cmd /c taskkill /f /im sql*; `   
                    Start-Sleep 5; `
                    Start-Service -Name 'MSSQLSERVER'; `  
                    #Start-Sleep 30; `
                    Start-Service -Name 'SQLSERVERAGENT'; `                      
                    }
12764050420
12764050420 25.04.2024 aktualisiert um 10:27:44 Uhr
Goto Top
Wenn man sowieso mit Invoke-Command arbeitet und statt mit einer Foreach Schleife alle Computer im Parameter -computername als Array übergibt werden die Computer automatisch parallel abgearbeitet.
Es braucht dann also keinerlei Schleife mehr.

Mit dem Parameter -ThrottleLimit lässt sich dann auch festlegen wie viele Computer max. parallel verarbeitet werden
Invoke-Command -ComputerName "pc1","pc2","pc3" -ThrottleLimit 10 -Scriptblock {  
    # mach was 
}
ThePinky777
ThePinky777 25.04.2024 um 10:30:34 Uhr
Goto Top
Man kann das auch so lösen.

Du hast ein Hauptscript, hier liest du z.B. eine Liste mit Computernamen aus.

Dann machst du ein Subscript, welches nur mit Variablen arbeitet (also Voll Dynamisch ist).

Nun rufst du im for each bereich das subscript auf
nach dem moto >> subscript.ps1 /ComputerName
wobei der Computername Dynamisch mitgegeben wird.
Man muss es aber so starten das nicht drauf gewartet wird das es sich beendet.
Somit starten dann dutzende Subscripts parallel und werden gleichzeitig abgearbeitet.

Nun tut das subscript checken welcher parameter mitgegeben wurde (der Computername)
und führt dann mit dem Parameter >> Variable dann entsprchend alles aus.

Wie das im Detail mit Powershell zu bauen ist, keine Ahnung, hab sowas nur unter VBS Scripts gebaut gehabt.

VBS Code:

Set objShell = WScript.CreateObject("WScript.Shell")   
Set objFSO = CreateObject("Scripting.FileSystemObject")  

CommandLine = "%comspec% /c cscript.exe " & """" & "C:\subscript.vbs" & """" & " /" & PCName  
'wscript.echo CommandLine  
objShell.run(CommandLine),0,false 

Übergebenen Parameter im Subscript auslesen:

Set Args = WScript.Arguments
sPCNameTemp = Replace(Args(0), "/", "")   
sPCNameTemp = Replace(sPCNameTemp, " ", "")   

wscript.echo sPCNameTemp
MarciMarc85
MarciMarc85 25.04.2024 aktualisiert um 11:03:03 Uhr
Goto Top
Zitat von @SlainteMhath:

Moin,

1. $block anpassen mit deinem Code
$block = {
    Param([string] $computer)
    "mach was"  
}

2. foreach-Schleife anpassen
foreach($computer in $computer){
[,,,]
    Start-Job -Scriptblock $Block -ArgumentList $ccomputer
}

3. ???
4. Profit!

(alles ohne Gewähr)

lg,
Slainte

Das sieht schonmal ganz gut aus. ichhab das jetzt mal folgendermaßen umngestezt. Anstatt des eigentlichen To-Do's hab ich zum Testen erstmal nur nen Text eingetragen, der Am Ende sowieso angezeigt wird. Allerdings wird der Servername / IP nicht ausgegeben/ eingelesen:

Aufruf:

$RootPath = Split-Path $PSScriptRoot -Parent
$serverlist = "D:\Update_Scripts\Trunk\serverlist.ini"  
$computers = get-content $serverlist -encoding UTF8 



    
$update_job = {
sleep 10
Write-host "Skript auf PC $computer erfolgreich" -ForegroundColor Yellow   
   }

#Remove all jobs
Get-Job | Remove-Job
$MaxThreads = 5
#Start the jobs. Max 5 jobs running simultaneously.
Foreach ($computer in $computers) {

    While ($(Get-Job -state running).count -ge $MaxThreads){
        Start-Sleep -Milliseconds 3
    }
    
    Start-Job -Scriptblock $update_job -ArgumentList $computer    
}

#Wait for all jobs to finish.
While ($(Get-Job -State Running).count -gt 0){
    start-sleep 1
}
#Get information from each job.
foreach($job in Get-Job){
    $info= Receive-Job -Id ($job.Id)
}

#Remove all jobs created.
Get-Job | Remove-Job

Ergebnis:

PS D:\Update_Scripts\Trunk> D:\Update_Scripts\14.4.x\Untitled1.ps1
cmdlet Untitled1.ps1 at command pipeline position 1
Supply values for the following parameters:
new_version: 14.6.66

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command                  
--     ----            -------------   -----         -----------     --------             -------                  
70     Job70           BackgroundJob   Running       True            localhost            ...                      
72     Job72           BackgroundJob   Running       True            localhost            ...                      
74     Job74           BackgroundJob   Running       True            localhost            ...                      
76     Job76           BackgroundJob   Running       True            localhost            ...                      
78     Job78           BackgroundJob   Running       True            localhost            ...                      
80     Job80           BackgroundJob   Running       True            localhost            ...                      
82     Job82           BackgroundJob   Running       True            localhost            ...                      
84     Job84           BackgroundJob   Running       True            localhost            ...                      
86     Job86           BackgroundJob   Running       True            localhost            ...                      
88     Job88           BackgroundJob   Running       True            localhost            ...                      
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
Starte Skript auf PC 
12764050420
Lösung 12764050420 25.04.2024 aktualisiert um 11:35:09 Uhr
Goto Top
Allerdings wird der Servername / IP nicht ausgegeben/ eingelesen
Du hast die Parameterdefinition im Skriptblock vergessen zu übernehmen. Ein Skriptblock ist wie eine Funktion die braucht entweder eine Parameterdefinition oder die Verwendung von der Variablen $args für die übergebenen Werte. Der Skriptblock wird ja in einem separaten Thread ausgeführt und kennt somit die Variablen des Host-Skriptes nicht.
$update_job = {
    Param([string]$computer)
    sleep 10
    Write-host "Skript auf PC $computer erfolgreich" -ForegroundColor Yellow     
 }
MarciMarc85
MarciMarc85 25.04.2024 um 11:30:03 Uhr
Goto Top
Ahhh.. Ja stimmt. Ich hab das übersehen. Viene Dank!
MarciMarc85
MarciMarc85 25.04.2024 um 12:20:45 Uhr
Goto Top
Zitat von @12764050420:

eine Frage hätte ich noch:

Ich habe nun den $update_job Block so konfiguriert, wie er final aussehen muss. Die Kommandos hatte ich vorher schon so im Script.


...
...
...

$RootPath = Split-Path $PSScriptRoot -Parent
$serverlist = "D:\Update_Scripts\Trunk\serverlist.ini"  
$computers = get-content $serverlist -encoding UTF8 


$body = ""  
$body += "$gruss `n`n" + "die folgenden Systeme sind auf $new_version aktualisiert:`n`n"  

    
$update_job = {

Param([string]$computer)


 $command = "$RootPath\PsExec.exe"  
 $scriptpath ="D:\powershell_tools\update_wizard_silent.ps1"  
 & $command "\\$computer" /s -i "powershell" $scriptpath $new_version      
 Write-host "Skript auf PC $computer erfolgreich" -ForegroundColor Yellow   
   }
...
...
...


Jetzt dachte ich, ich muss den Parameter für $RootPath auch noch so übergeben, wie $computer. Abe rich kann nicht mehrere Parameter untereinander angeben. Egal wie, es kommt immer diese Fehlermeldung:

PS D:\Update_Scripts\Trunk> D:\Update_Scripts\Trunk\test_multiple.ps1
cmdlet test_multiple.ps1 at command pipeline position 1
Supply values for the following parameters:
new_version: 14.6.66

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command                  
--     ----            -------------   -----         -----------     --------             -------                  
292    10.32.6.37      BackgroundJob   Running       True            localhost            ...                      
The term '\PsExec.exe' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.  
    + CategoryInfo          : ObjectNotFound: (\PsExec.exe:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
    + PSComputerName        : localhost
 
Skript auf PC 10.32.6.37 erfolgreich
12764050420
Lösung 12764050420 25.04.2024 aktualisiert um 12:34:35 Uhr
Goto Top
$update_job = {
    Param(
     [string]$computer,
     [string]$rootPath
    )
# ....
#...

und beim Aufruf des Jobs die Variable mit übergeben:

Start-Job -Scriptblock $update_job -ArgumentList $computer,$RootPath
MarciMarc85
MarciMarc85 25.04.2024 um 12:36:42 Uhr
Goto Top
Zitat von @12764050420:


und beim Aufruf des Jobs die Variable mit übergeben:

Start-Job -Scriptblock $update_job -ArgumentList $computer,$RootPath

Danke den Teil hatte ich vergessen