Vrátit hodnotu z powershell invoke-command agentu SQL-Server

Při vytváření vlastní metodiky správy záloh na mnoha serverech MS-SQL jsem strávil hodně času studiem mechanismu předávání hodnot v PowerShell při vzdálených voláních, proto si píšu poznámku pro sebe, ať se třeba hodí i někomu jinému.

Takže si nejprve vezmeme nejjednodušší skript a spustíme ho lokálně:

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

K spuštění skriptů budu používat následující CMD soubor, pokaždé ho nebudu uvádět:

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

Na obrazovce uvidíme následující:

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


Nyní spustíme stejný skript přes WSMAN (na dálku):

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

A tady máte výsledek:

Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0

Skvěle, Errorlevel někam zmizel, ale potřebujeme přece získat hodnotu ze skriptu! Zkoušíme následující konstrukci:

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

Tady to je ještě zajímavější. Celý výstup do Output někam zmizel:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Nyní jako lyrickou odbočku poznamenám, že pokud uvnitř funkce PowerShell napíšete Write-Output nebo prostě výraz bez přiřazení k nějaké proměnné (což implicitně předpokládá výstup do kanálu Output), tak ani při lokálním spuštění se na obrazovce nic nezobrazí! Je to důsledek konvenční architektury PowerShell — každá funkce má svůj vlastní konvenční Output, pro něj se vytváří pole, a všechno, co do něj přichází, je považováno za výsledek provedení funkce, operátor Return přidává vracenou hodnotu jako poslední prvek do tohoto konvenčního kanálu a předává řízení do volající funkce. Pro ilustraci spusťme lokálně následující skript:

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

A tady je jeho výsledek:

Main: ParameterValue

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Main: Function: ParameterValue
Main: ReturnValue

Hlavní funkce (tělo skriptu) má také svůj vlastní konvenční Output, a pokud spustíme první skript z CMD, přesměrujeme výstup do souboru,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

na obrazovce uvidíme

ERRORLEVEL=1

a v souboru

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

pokud však provedeme obdobné volání z powershellu

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

na obrazovce bude

Out to host.
ExitCode: 1

a v souboru

Out to output.
1

To se děje proto, že CMD spouští powershell, který při absenci dalších pokynů míchá dva proudy (Host a Output) a předává je CMD, které zapisuje do souboru vše, co obdrželo; naopak při spuštění z powershellu existují tyto dva proudy odděleně a symbol přesměrování ovlivňuje pouze Output.

Vrátíme-li se k hlavnímu tématu, připomeňme si, že objektní model .NET v rámci powershellu plně existuje na jednom počítači (jednom OS); při vzdáleném spuštění kódu přes WSMAN se přenos objektů odehrává pomocí XML serializace, což přináší mnoho dalších zajímavostí do našich výzkumů. Pokračujme experimenty spuštěním následujícího kódu:

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

A zde je, co máme na obrazovce:

Out to host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Nelze převést argument "exitCode" s hodnotou: "System.Object[]" na "SetShouldExit" typu "System.Int32": "Nelze převést hodnotu "System.Object[]" typu "System.Object[]" na typ "System.Int32"."
D:sqlagentTestOutput3.ps1:3 znak:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Skvělý výsledek! Znamená to, že při volání Invoke-Command se zachovává dělení konvojů na dva proudy (Host a Output), což nám dává naději na úspěch. Zkusíme ponechat v proudu Output pouze jednu hodnotu, k čemuž změníme úplně první skript, který spouštíme vzdáleně:

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

Spustíme to takto:

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

a… ANO, vypadá to jako vítězství!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Pokusíme se pochopit, co se nám stalo. Místně jsme spustili PowerShell, který pak spustil PowerShell na vzdáleném počítači a provedl náš skript. Dva proudy (Host a Output) z vzdáleného stroje byly serializovány a odeslány zpět, přičemž proud Output, pokud obsahoval jednu číselnou hodnotu, byl převeden na typ Int32 a takto předán přijímající straně, která jej použila jako ukončovací kód volajícího PowerShellu.

A jako poslední kontrolu vytvoříme na serveru SQL úkol z jednoho kroku s typem „Operační systém (cmdexec)“ se následujícím textem:

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

HURÁ! Úkol byl dokončen s chybou, text v protokolu:

Provádí se jménem uživatele: DOMAINagentuser. Out to host. ExitCode: 6. Ukončovací kód procesu 6. Krok skončil chybou.

Závěry:

  • Vyhněte se používání Write-Output a uvádění výrazů bez přiřazení. Pamatujte, že přesun tohoto kódu na jiné místo ve skriptu může vést k nečekaným výsledkům.
  • Ve skriptech určených nikoli k ručnímu spuštění, ale k použití ve vašich automatizačních mechanismech, zejména pro vzdálené volání přes WINRM, provádějte ruční zpracování chyb pomocí Try/Catch a zajistěte, aby tento skript vždy poslal do proudu Output přesně jednu hodnotu primitivního typu. Pokud chcete získat klasickou Errorlevel - tato hodnota by měla být číselná.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster