Restituzione dei valori dall'invoke-command di PowerShell all'agente SQL Server

Nel creare il mio metodo di gestione dei backup su molti server MS-SQL, ho speso un sacco di tempo a studiare il meccanismo di passaggio dei valori in PowerShell durante le chiamate remote, quindi scrivo questo promemoria per me stesso, nel caso possa essere utile a qualcun altro.

Cominciamo con uno script semplice e eseguiamolo localmente:

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

Per eseguire gli script userò il seguente file CMD, non lo riporterò ogni volta:

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

Sullo schermo vedremo il seguente output:

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


Ora eseguiamo lo stesso script tramite WSMAN (a distanza):

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

Ecco il risultato:

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

Ottimo, l'Errorlevel è scomparso, ma dobbiamo ottenere un valore dallo script! Proviamo la seguente costruzione:

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

Qui la situazione è ancora più interessante. Tutto l'output scompare:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Ora, come parentesi lirica, voglio sottolineare che se all'interno di una funzione PowerShell scrivete Write-Output o semplicemente un'espressione senza assegnarla a una variabile (il che implica implicitamente l'output nel canale Output), anche in esecuzione locale non verrà visualizzato nulla sullo schermo! Questo è il risultato dell'architettura a pipeline di PowerShell: ogni funzione ha la propria pipeline Output, viene creato un array per essa e tutto ciò che vi entra è considerato il risultato dell'esecuzione della funzione. L'operatore Return aggiunge il valore di ritorno a quest'ultima pipeline come ultimo elemento e restituisce il controllo alla funzione chiamante. Per illustrare, eseguiamo localmente il seguente 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: "+$_) }

Ed ecco il suo risultato:

Main: ParameterValue

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

La funzione principale (corpo dello script) ha anche il suo pipeline di output, e se eseguiamo il primo script da CMD, reindirizzando l'output in un file,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

sullo schermo vedremo

ERRORLEVEL=1

e nel file

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

se effettuiamo una chiamata analoga da PowerShell

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

sullo schermo ci sarà

Out to host.
ExitCode: 1

e nel file

Out to output.
1

Questo accade perché CMD avvia PowerShell, che in assenza di ulteriori indicazioni mescola i due flussi (Host e Output) e li restituisce a CMD, che invia nel file tutto ciò che ha ricevuto; mentre, nel caso dell'esecuzione da PowerShell, questi due flussi esistono separatamente e il simbolo di reindirizzamento influisce solo su Output.

Tornando al tema principale, ricordiamo che il modello a oggetti .NET all'interno di PowerShell esiste pienamente nell'ambito di un singolo computer (un singolo OS); durante l'esecuzione di codice remoto tramite WSMAN, il trasferimento di oggetti avviene tramite serializzazione XML, il che aggiunge molto interesse alle nostre indagini. Continuiamo gli esperimenti eseguendo il seguente codice:

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

Ecco cosa abbiamo sullo schermo:

In uscita per l'hosting.

Codice di uscita: 3

IsPublic IsSerial Nome                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Impossibile convertire l'argomento "exitCode", con valore: "System.Object[]", per "SetShouldExit" in tipo "System.Int32": "Impossibile convertire il valore "System.Object[]" di tipo "System.Object[]" in tipo "System.Int32"."
D:sqlagentTestOutput3.ps1:3 posizione:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Risultato eccellente! Significa che, quando si chiama Invoke-Command, si conserva la separazione dei pipeline in due flussi (Host e Output), il che ci dà speranza di successo. Proviamo a mantenere nel flusso Output solo un valore, per cui modificheremo il primo script che eseguiamo in remoto:

$exitcode = $args[0]
Write-Host 'In uscita per l'hosting.'
#Write-Output 'In uscita per l'output.'
Write-Host ('Codice di uscita: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Eseguiamolo in questo modo:

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

e… SÌ, sembra una vittoria!

In uscita per l'hosting.
Codice di uscita: 4

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


ERRORLEVEL=4

Cerchiamo di capire cosa è successo. Abbiamo invocato localmente PowerShell, il quale ha a sua volta chiamato PowerShell su un computer remoto ed eseguito il nostro script lì. Due stream (Host e Output) dalla macchina remota sono stati serializzati e restituiti, mentre lo stream Output, se contenente un valore numerico, è stato convertito in tipo Int32 e passato al destinatario, il quale lo ha utilizzato come codice di uscita per PowerShell.

E come ultima verifica, creeremo su server un'attività SQL in un singolo passaggio di tipo "Sistema operativo (cmdexec)" con il seguente testo:

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

Evviva! L'attività è terminata con errore, testo nel log:

Eseguito a nome dell'utente: DOMAINagentuser. Output al host. ExitCode: 6. Codice di uscita del processo 6. Passaggio terminato con errore.

Conclusioni:

  • Evitare di utilizzare Write-Output e di specificare espressioni senza assegnazione. Ricordate che trasferire questo codice in un'altra parte dello script potrebbe portare a risultati inaspettati.
  • Nei script destinati non all'avvio manuale, ma per l'uso nei vostri meccanismi di automazione, in particolare per le chiamate remote tramite WINRM, effettuate una gestione degli errori manuale tramite Try/Catch, assicurandovi che, in qualsiasi situazione, questo script invii nel flusso Output esattamente un valore di tipo primitivo. Se desiderate un classico Errorlevel, questo valore deve essere numerico.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster