Kthimi i vlerës nga powershell invoke-command te agjenti SQL-Server

Kur krijova metodologjinë time për menaxhimin e kopjimeve rezervë në shumë servera MS-SQL, kalova një sasi të madhe kohe duke studiuar mekanizmin e kalimit të vlerave në powershell gjatë thirrjeve të largëta, prandaj po shkruaj një shënim për veten time, ndoshta do t'i shërbejë dikujt tjetër.

Pra, le të marrim për fillim një skenar shumë të thjeshtë dhe ta ekzekutojmë lokal:

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

Për të ekzekutuar skriptet, do të përdor këtë skedarin CMD, nuk do ta sjell çdo herë:

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

Në ekran do të shohim këtë:

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


Tani le të ekzekutojmë këtë skript përmes WSMAN (në distancë):

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

Dhe ja rezultati:

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

Mrekullisht, Errorlevel ka humbur diku, por na nevojitet të marrim vlerën nga skripti! Provojmë këtë konstrukcion:

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

Këtu është edhe më interesante. I tërë rezultati në Output është zhdukur diku:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Tani, si një shmangie poetike, do të theksoj se nëse brenda funksionit Powershell shkruani Write-Output ose thjesht një shprehje pa e caktuar atë ndonjë variabli (dhe kjo nënkupton implicit që do të dalë në kanal OutPut), atëherë madje edhe kur ekzekutohet lokal, në ekran nuk do të del asgjë! Kjo është një pasojë e arkitekturës konvencionale të powershell — secili funksion ka kanalin e vet Output, për të krijohet një masiv, dhe gjithçka që hyn në të konsiderohet si rezultat i ekzekutimit të funksionit, operatori Return e shton vlerën e kthyer në këtë kanal si elementin e fundit dhe e kalon kontrollin në funksionin që bëri thirrjen. Për ilustrim, do të ekzekutojmë lokalisht skriptin e mëposhtëm:

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

Dhe ja rezultati i tijë:

Main: ParameterValue

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

Funksioni kryesor (trupi i skriptit) gjithashtu ka një konvejer Output, dhe nëse e ekzekutojmë skriptin e parë nga CMD, duke redirekcionuar daljen në një skedar,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

atëherë në ekran do të shohim

ERRORLEVEL=1

ndërsa në skedarin

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

nëse bëjmë një thirrje të ngjashme nga powershell

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

atëherë në ekran do të ketë

Out to host.
ExitCode: 1

ndërsa në skedarin

Out to output.
1

Kjo ndodh sepse CMD ekzekuton powershell, i cili në mungesë të udhëzimeve të tjera përzien dy rrjedha (Host dhe Output) dhe ia jep ato CMD, e cila dërgon në skedar gjithçka që ka marrë, kurse në rastin e ekzekutimit nga powershell këto dy rrjedha ekzistojnë veçmas, dhe simbole redirektimi ndikon vetëm në Output.

Duke u kthyer në temën kryesore, le të kujtojmë se modeli objektiv .NET brenda powershell ekziston plotësisht në një kompjuter (një OS), gjatë ekzekutimit të kodit në distancë përmes WSMAN, transmetimi i objekteve ndodh përmes XML-serializimit, që sjell shumë interes shtesë në hulumtimet tona. Të vazhdojmë eksperimentet duke ekzekutuar kodin e ardhshëm:

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

Dhe ja çfarë kemi në ekran:

Deri te host.

Kod dalës: 3

IsPublic IsSerial Emri                                     TipiBazë
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Nuk është e mundur të konvertohet argumenti "exitCode", me vlerë: "System.Object[]", për "SetShouldExit" në tipin "System.Int32": "Nuk është e mundur të konvertohet vlera "System.Object[]" tipin "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 shenja:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + KategoriaInfo          : NotSpecified: (:) [], MethodException
    + Identifikimi i Plotë i Kategorisë : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Rezultat i shkëlqyer! Kjo do të thotë se gjatë thirrjes së Invoke-Command, ndarja e tubacioneve në dy rrjedha (Host dhe Output) ruhet, që na jep shpresë për sukses. Le të provonim të lëmë në rrjedhën Output vetëm një vlerë, për të cilën do të ndryshojmë skriptin e parë që ekzekutojmë nga distanca:

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

Ta ekzekutojmë kështu:

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

dhe… PO, duket se është fitorja!

Deri te host.
Kod dalës: 4

IsPublic IsSerial Emri                                     TipiBazë
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


ERRORLEVEL=4

Le të shohim se çfarë ndodhi. Ne thirrëm lokalisht PowerShell, i cili nga ana e tij thirri PowerShell në kompjuterin e largët dhe ekzekutoi skenarin tonë atje. Dy rrjedha (Host dhe Output) nga makina e largët u serializuan dhe u dërguan përsëri, ku rrjedha Output, nëse kishte një vlerë numerike, u konvertua në tipin Int32 dhe u dërgua kështu palës marrëse, ndërsa pala marrëse e përdori atë si kod përfundimi për PowerShell-in e thirrur.

Dhe si një verifikim të fundit do të krijojmë në serveri një detyrë SQL me një hap të tipit 'Sistemi operativ (cmdexec)' me tekstin:

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

URIME! Detyra përfundoi me gabim, teksti në regjistër:

Ekzekutohet në emër të përdoruesit: DOMAINagentuser. Jashtë për host. ExitCode: 6. Kodi përfundimi i procesit 6. Hapi përfundoi me gabim.

Konkluzione:

  • Shmangni përdorimin e Write-Output dhe specifikimin e shprehjeve pa caktim. Kini parasysh se transferimi i këtij kodi në një vend tjetër të skenarit mund të çojë në rezultate të papritura.
  • Në skriptet që nuk janë destinuar për ekzekutim manual, por për t'u përdorur në mekanizmat tuaj të automatizimit, veçanërisht për thirrje të largëta përmes WINRM, bëni përpunim manual të gabimeve përmes Try/Catch, dhe sigurohuni që, në çdo rast, ky skript të dërgojë në stream-in Output saktësisht një vlerë primitive. Nëse dëshironi të merrni një Errorlevel klasik — kjo vlerë duhet të jetë numerike.

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster