Returnarea unei valori din powershell invoke-command către agentul SQL Server

Când mi-am creat propria metodologie pentru gestionarea backup-urilor pe mai multe servere MS-SQL, am petrecut mult timp studiind mecanismul de transmitere a valorilor în Powershell în timpul apelurilor de la distanță, așa că îmi scriu un memento în cazul în care este util altcuiva.

Deci, să începem cu un script simplu și să-l rulăm local:

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

Pentru a rula scripturi, voi folosi următorul fișier CMD, nu îl voi include de fiecare dată:

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

Pe ecran vom vedea următoarele:

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


Acum să rulăm același script prin WSMAN (de la distanță):

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

Și iată rezultatul pentru tine:

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

Grozav, Errorlevel a dispărut undeva, dar trebuie să obținem valoarea din script! Să încercăm următoarea construcție:

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

Acest lucru este și mai interesant. Mesajul din Output a dispărut undeva:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Acum, ca o digresiune lirică, voi observa că, dacă într-o funcție Powershell scrieți Write-Output sau doar o expresie fără a o atribui vreunei variabile (și aceasta implică implicit ieșire către canalul de ieșire), atunci chiar și atunci când rulați local, nimic nu va fi afișat pe ecran! Aceasta este o consecință a arhitecturii pipeline powershell - fiecare funcție are propria conductă de ieșire, este creată o matrice pentru ea și tot ceea ce intră în ea este considerat rezultatul execuției funcției, operatorul Return adaugă valoarea de returnare la aceeași pipeline ca ultimul element și transferă controlul către funcția de apelare. Pentru a ilustra, să rulăm local următorul script:

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: "+$_) }

Și iată rezultatul:

Main: ParameterValue

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

Funcția principală (corpul scriptului) are și propria conductă de ieșire, iar dacă rulăm primul script din CMD, redirecționând rezultatul către un fișier,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

apoi vom vedea pe ecran

ERRORLEVEL=1

iar în dosar

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

dacă facem un apel similar de la powershell

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

atunci va fi pe ecran

Out to host.
ExitCode: 1

iar în dosar

Out to output.
1

Acest lucru se întâmplă deoarece CMD-ul lansează powershell, care, în absența altor instrucțiuni, amestecă două fire (Host și Output) și le dă CMD-ului, care trimite tot ce a primit într-un fișier, iar în cazul lansării de pe powershell, aceste două fire există separat, iar redirecționările simbolurilor afectează numai Ieșirea.

Revenind la subiectul principal, să ne amintim că modelul de obiecte .NET din interiorul powershell există pe deplin într-un singur computer (un sistem de operare), atunci când rulați codul de la distanță prin WSMAN, transferul de obiecte are loc prin serializare XML, ceea ce aduce mult interes suplimentar. la cercetarea noastră. Să continuăm experimentele rulând următorul cod:

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

Și iată ce avem pe ecran:

Out to host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Не удается преобразовать аргумент "exitCode", со значением: "System.Object[]", для "SetShouldExit" в тип "System.Int32": "Не удается преобразовать значение "System.Object[]" типа "System.Object[]" в тип "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 знак:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Rezultat grozav! Înseamnă că atunci când apelăm la Invoke-Command, se menține împărțirea conductelor în două fire (Gazdă și Ieșire), ceea ce ne dă speranța de succes. Să încercăm să lăsăm o singură valoare în fluxul de ieșire, pentru care vom schimba primul script pe care îl rulăm de la distanță:

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

Să o rulăm astfel:

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

și... DA, pare o victorie!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Să încercăm să ne dăm seama ce s-a întâmplat. Am numit powershell local, care la rândul său a numit powershell pe computerul de la distanță și a executat scriptul nostru acolo. Două fluxuri (gazdă și ieșire) de la mașina de la distanță au fost serializate și transmise înapoi, în timp ce fluxul de ieșire, având o singură valoare digitală în el, a fost convertit la tipul Int32 și, ca atare, a trecut la partea de recepție, iar partea de recepție l-a folosit ca cod de ieșire al apelantului Powershell.

Și, ca o verificare finală, să creăm o lucrare într-un singur pas pe serverul SQL cu tipul „Sistem de operare (cmdexec)” cu următorul text:

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

URA! Sarcina finalizată cu o eroare, text în jurnal:

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Concluzii:

  • Evitați utilizarea Write-Output și specificarea expresiilor fără atribuire. Rețineți că mutarea acestui cod în altă parte în script poate produce rezultate neașteptate.
  • În scripturile destinate nu lansării manuale, ci utilizării în mecanismele dumneavoastră de automatizare, în special pentru apelurile de la distanță prin WINRM, gestionați manual erorile prin Try/Catch și asigurați-vă că, în orice dezvoltare a evenimentelor, acest script trimite exact o valoare de tip primitiv . Dacă doriți să obțineți nivelul clasic de eroare, această valoare trebuie să fie numerică.

Sursa: www.habr.com

Adauga un comentariu