Renvoi d'une valeur de la commande PowerShell Invocation à l'agent SQL Server

Lors de la création de ma propre méthodologie de gestion des sauvegardes sur plusieurs serveurs MS-SQL, j'ai passé beaucoup de temps à étudier le mécanisme de transmission des valeurs dans Powershell lors des appels à distance, je m'écris donc un rappel au cas où cela serait utile à quelqu'un d'autre.

Commençons donc par un script simple et exécutons-le localement :

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

Pour exécuter des scripts, j'utiliserai le fichier CMD suivant, je ne l'inclurai pas à chaque fois :

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

Sur l'écran, nous verrons ce qui suit :

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


Exécutons maintenant le même script via WSMAN (à distance) :

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

Et voici le résultat:

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

Super, Errorlevel a disparu quelque part, mais nous devons récupérer la valeur du script ! Essayons la construction suivante :

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

C'est encore plus intéressant. Le message dans Output a disparu quelque part :

Out to host.
ExitCode: 2
ERRORLEVEL=0

Maintenant, en guise de digression lyrique, je noterai que si à l'intérieur d'une fonction Powershell vous écrivez Write-Output ou simplement une expression sans l'attribuer à une variable (et cela implique implicitement une sortie sur le canal de sortie), alors même lors d'une exécution locale, rien ne s'affichera à l'écran ! Ceci est une conséquence de l'architecture du pipeline PowerShell - chaque fonction a son propre pipeline de sortie, un tableau est créé pour elle et tout ce qui y entre est considéré comme le résultat de l'exécution de la fonction, l'opérateur Return ajoute la valeur de retour au même pipeline comme dernier élément et transfère le contrôle à la fonction appelante. Pour illustrer, exécutons le script suivant localement :

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

Et voici le résultat:

Main: ParameterValue

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

La fonction principale (corps du script) possède également son propre pipeline de sortie, et si nous exécutons le premier script depuis CMD, redirigeant la sortie vers un fichier,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

alors nous verrons sur l'écran

ERRORLEVEL=1

et dans le fichier

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

si nous faisons un appel similaire depuis PowerShell

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

alors ce sera à l'écran

Out to host.
ExitCode: 1

et dans le fichier

Out to output.
1

Cela se produit parce que le CMD lance PowerShell, qui, en l'absence d'autres instructions, mélange deux threads (Host et Output) et les donne au CMD, qui envoie tout ce qu'il a reçu dans un fichier, et en cas de lancement depuis PowerShell, ces deux threads existent séparément et les redirections de symboles n'affectent que la sortie.

Revenant au sujet principal, rappelons que le modèle objet .NET à l'intérieur de PowerShell existe pleinement au sein d'un ordinateur (un seul système d'exploitation), lors de l'exécution de code à distance via WSMAN, le transfert d'objets s'effectue via la sérialisation XML, ce qui apporte beaucoup d'intérêt supplémentaire. à nos recherches. Poursuivons nos expérimentations en exécutant le code suivant :

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

Et voici ce que nous avons à l'écran :

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

Super résultat ! Cela signifie que lors de l'appel d'Invoke-Command, la division des pipelines en deux threads (Host et Output) est maintenue, ce qui nous laisse espérer du succès. Essayons de ne laisser qu'une seule valeur dans le flux de sortie, pour laquelle nous modifierons le tout premier script que nous exécutons à distance :

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

Exécutons-le comme ceci :

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

et... OUI, cela ressemble à une victoire !

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Essayons de comprendre ce qui s'est passé. Nous avons appelé PowerShell localement, qui à son tour a appelé PowerShell sur l'ordinateur distant et y a exécuté notre script. Deux flux (hôte et sortie) de la machine distante ont été sérialisés et renvoyés, tandis que le flux de sortie, contenant une seule valeur numérique, a été converti en type Int32 et, en tant que tel, transmis au côté réception, et le côté réception l'a utilisé. comme code de sortie du PowerShell appelant.

Et comme dernière vérification, créons un travail en une étape sur le serveur SQL avec le type « Système d'exploitation (cmdexec) » avec le texte suivant :

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

Hourra ! La tâche terminée avec une erreur, texte dans le journal :

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

Conclusions:

  • Évitez d'utiliser Write-Output et de spécifier des expressions sans affectation. Sachez que déplacer ce code ailleurs dans le script peut produire des résultats inattendus.
  • Dans les scripts destinés non pas à un lancement manuel, mais à une utilisation dans vos mécanismes d'automatisation, notamment pour les appels à distance via WINRM, effectuez une gestion manuelle des erreurs via Try/Catch et assurez-vous que, dans tout développement d'événements, ce script envoie exactement une valeur de type primitif. . Si vous souhaitez obtenir le niveau d'erreur classique, cette valeur doit être numérique.

Source: habr.com

Ajouter un commentaire