Waarde retourneren van PowerShell invoke-command naar SQL-Server agent

Bij het creƫren van mijn eigen back-upbeheer techniek op meerdere MS-SQL-servers heb ik veel tijd besteed aan het bestuderen van de mechaniek van waardeoverdracht in PowerShell tijdens afstandsaanroepen, daarom schrijf ik een herinnering voor mezelf, hopelijk is het ook nuttig voor iemand anders.

Laten we beginnen met een eenvoudig script en dit lokaal uitvoeren:

$exitcode = $args[0]
Write-Host 'Uit naar host.'
Write-Output 'Uit naar output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Voor het uitvoeren van scripts zal ik het volgende CMD-bestand gebruiken, ik zal het niet elke keer herhalen:

@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%

Op het scherm zullen we het volgende zien:

Uit naar host.
Uit naar output.
ExitCode: 1
1
ERRORLEVEL=1


Laten we nu hetzelfde script opnieuw uitvoeren via WSMAN (op afstand):

Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

En hier is het resultaat:

Uit naar host.
Uit naar output.
ExitCode: 2
2
ERRORLEVEL=0

Geweldig, de Errorlevel is ergens verdwenen, maar we willen toch de waarde uit het script ontvangen! Laten we de volgende constructie proberen:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

Het wordt nog interessanter. De volledige output in Output is ergens verdwenen:

Uit naar host.
ExitCode: 2
ERRORLEVEL=0

Nu als een zij-stuk wil ik opmerken dat als je binnen een PowerShell-functie Write-Output of gewoon een expressie zonder deze aan een variabele toe te wijzen schrijft (wat impliciet uitvoer naar het Output-kanaal betekent), er zelfs bij lokale uitvoeringen niets op het scherm verschijnt! Dit is een gevolg van de pijplijnarchitectuur van PowerShell — elke functie heeft zijn eigen Output-pijplijn, waarvoor een array wordt aangemaakt, en alles wat daarin komt wordt als resultaat van de functie-uitvoering beschouwd; de Return-operator voegt de teruggegeven waarde als laatste element aan deze pijplijn toe en geeft de controle terug aan de aanroeping functie. Om dit te illustreren, laten we lokaal het volgende script uitvoeren:

Function Write-Log {
  Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
  Write-Output ("Functie: "+$OutString)
  Return "ReturnValue"
}
Write-Output ("Hoofd: "+"ParameterValue")
$res = Write-Log "ParameterValue"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Hoofd: "+$_) }

En hier is het resultaat:

Hoofd: ParameterValue

IsPublic IsSerial Naam                                     BasisType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Hoofd: Functie: ParameterValue
Hoofd: ReturnValue

De hoofdfunctie (scriptlichaam) heeft ook zijn eigen Output-pijplijn, en als we het eerste script vanuit CMD uitvoeren en de output naar een bestand omleiden,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

zullen we op het scherm zien

ERRORLEVEL=1

en in het bestand

Out to host.
Out to output.
ExitCode: 1
1

als we een soortgelijke aanroep vanuit PowerShell doen

PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txt

zullen we op het scherm zien

Out to host.
ExitCode: 1

en in het bestand

Out to output.
1

Dit gebeurt omdat CMD PowerShell start, dat zonder verdere aanwijzingen de twee streams (Host en Output) mengt en deze naar CMD teruggeeft, die alles wat het ontvangt naar het bestand schrijft, terwijl bij het uitvoeren vanuit PowerShell deze twee streams apart bestaan, en het omleidingssymbool alleen van invloed is op Output.

Terugkomend op het hoofdonderwerp, laten we ons herinneren dat het objectmodel .NET binnen PowerShell volledig bestaat binnen ƩƩn computer (ƩƩn OS), bij het op afstand uitvoeren van code via WSMAN gebeurt de overdracht van objecten via XML-serialisatie, wat veel extra interessantheid toevoegt aan onze onderzoeken. Laten we de experimenten voortzetten door de volgende code uit te voeren:

$res=Invoke-Command -ComputerName . -ScriptBlock { 'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)

En dit is wat we op het scherm hebben:

Out to host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Kan argument "exitCode" niet converteren, met waarde: "System.Object[]", voor "SetShouldExit" naar type "System.Int32": "Kan waarde "System.Object[]" van type "System.Object[]" niet converteren naar type "System.Int32"."
D:sqlagentTestOutput3.ps1:3 teken:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + Categorie-informatie          : Niet gespecificeerd: (:) [], MethodException
    + Volledig gekwalificeerde fout-id : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Prachtig resultaat! Dit betekent dat bij het aanroepen van Invoke-Command de splitsing van pijplijnen in twee streams (Host en Output) behouden blijft, wat ons hoop geeft op succes. Laten we proberen om alleen ƩƩn waarde in de Output-stream te laten, hiervoor veranderen we het allereerste script dat we op afstand starten:

$exitcode = $args[0]
Write-Host 'Out to host.'
#Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Laten we het zo uitvoeren:

$res=Invoke-Command -ComputerName . -ScriptBlock { 'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)

en… JA, het lijkt erop dat dit een overwinning is!

Out to host.
ExitCode: 4

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


ERRORLEVEL=4

Laten we proberen te begrijpen wat er is gebeurd. We hebben lokaal PowerShell aangeroepen, dat op zijn beurt PowerShell op de externe computer heeft aangeroepen en ons script daar heeft uitgevoerd. Twee streams (Host en Output) van de externe machine zijn geserialiseerd en teruggestuurd, waarbij de Output-stream, indien deze een numerieke waarde bevatte, werd omgezet naar het type Int32 en in die vorm naar de ontvangende partij werd gestuurd, die het gebruikte als exitcode voor de aanroepende PowerShell.

En als laatste controle zullen we aanmaken op de server een SQL-taak met ƩƩn stap van het type 'Besturingssysteem (cmdexec)' met de volgende tekst:

PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"

HOERA! De taak is beƫindigd met een fout, tekst in het logboek:

Wordt uitgevoerd onder de gebruikersnaam: DOMAINagentuser. Out to host. ExitCode: 6. Proces exitcode 6. Stap is mislukt met een fout.

Conclusies:

  • Vermijd het gebruik van Write-Output en het opgeven van expressies zonder toewijzing. Houd er rekening mee dat het verplaatsen van deze code naar een andere plaats in het script onverwachte resultaten kan opleveren.
  • In scripts die niet bedoeld zijn voor handmatige uitvoering, maar voor gebruik in uw automatiseringssystemen, vooral voor externe aanroepen via WINRM, zorg voor handmatige foutafhandeling via Try/Catch, en zorg ervoor dat dit script bij elke gebeurtenis precies ƩƩn waarde van primair type naar de Output-stream verzendt. Als je een klassieke Errorlevel wilt verkrijgen, moet deze waarde numeriek zijn.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster