Retourner une valeur de PowerShell invoke-command à l'agent SQL-Server

En créant ma propre méthode 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 distants, donc je rédige un mémo pour moi-même, au cas où cela pourrait également servir à quelqu'un d'autre.

Commençons par un script très simple et exécutons-le localement :

$exitcode = $args[0]
Write-Host 'Sortie vers l'hôte.'
Write-Output 'Sortie vers la sortie.'
Write-Host ('Code de sortie : ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Pour exécuter les scripts, j'utiliserai le fichier CMD suivant, je ne le répéterai pas chaque fois :

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

À l'écran, nous verrons le suivant :

Sortie vers l'hôte.
Sortie vers la sortie.
Code de sortie : 1
1
ERRORLEVEL=1


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

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

Et voici le résultat :

Sortie vers l'hôte.
Sortie vers la sortie.
Code de sortie : 2
2
ERRORLEVEL=0

Parfait, Errorlevel a disparu, mais nous devons obtenir 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. Toute la sortie vers Output a disparu :

Sortie vers l'hôte.
Code de sortie : 2
ERRORLEVEL=0

Maintenant, comme une petite digression, je noterai que si vous écrivez Write-Output ou simplement une expression sans l'assigner à une variable à l'intérieur d'une fonction PowerShell (ce qui implique de facto une sortie vers le canal Output), alors même lors d'une exécution locale, rien ne sera affiché à l'écran ! C'est une conséquence de l'architecture en pipeline de PowerShell — chaque fonction a son propre pipeline Output, un tableau est créé pour cela, et tout ce qui y parvient est considéré comme le résultat de l'exécution de la fonction, l'opérateur Return ajoute la valeur retournée en tant que dernier élément de ce même pipeline et passe le contrôle à la fonction appelante. Pour illustrer, exécutons localement le script suivant :

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

Et voici le résultat :

Principal : ParameterValue

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

La fonction principale (le corps du script) a également son propre pipeline Output, et si nous exécutons le premier script à partir de CMD, en redirigeant la sortie vers un fichier,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

nous verrons à l'écran

ERRORLEVEL=1

et dans le fichier

Sortie vers l'hôte.
Sortie vers la sortie.
Code de sortie : 1
1

si nous faisons un appel similaire depuis powershell

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

il y aura à l'écran

Sortie vers l'hôte.
Code de sortie : 1

et dans le fichier

Sortie vers la sortie.
1

Cela se produit parce que CMD lance powershell qui, sans autre indication, mélange deux flux (Host et Output) et les renvoie à CMD, qui envoie dans le fichier tout ce qu'il a reçu. En lançant depuis powershell, ces deux flux existent séparément, et le symbole de redirection n'influence que la sortie.

Revenons au sujet principal, rappelons que le modèle d'objet .NET à l'intérieur de powershell existe pleinement dans le cadre d'un seul ordinateur (un seul OS). Lors du lancement de code à distance via WSMAN, le transfert d'objets se fait par XML-sérialisation, ce qui ajoute beaucoup d'intérêt supplémentaire à nos recherches. Continuons les expériences en lançant 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 :

Sortie vers l'hôte.

Code de sortie : 3

IsPublic IsSerial Nom                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Impossible de convertir l'argument "exitCode", avec la valeur : "System.Object[]", pour "SetShouldExit" au type "System.Int32" : "Impossible de convertir la valeur "System.Object[]" de type "System.Object[]" au type "System.Int32"."
D:sqlagentTestOutput3.ps1:3 caractère : 1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Un excellent résultat ! Cela signifie qu'en appelant Invoke-Command, la séparation des pipelines en deux flux (Host et Output) est conservée, ce qui nous donne de l'espoir pour le succès. Essayons de laisser dans le flux de sortie une seule valeur, ce qui nécessite de modifier le tout premier script que nous exécutons à distance :

$exitcode = $args[0]
Write-Host 'Sortie vers l'hôte.'
#Write-Output 'Sortie vers la sortie.'
Write-Host ('Code de sortie : ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Exécutons-le ainsi :

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

et… OUI, cela semble être une victoire !

Sortie vers l'hôte.
Code de sortie : 4

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


ERRORLEVEL=4

Essayons de comprendre ce qui s'est passé. Nous avons appelé localement PowerShell, qui, à son tour, a appelé PowerShell sur l'ordinateur distant et a exécuté notre script là-bas. Deux flux (Host et Output) depuis la machine distante ont été sérialisés et renvoyés, le flux Output étant converti en type Int32 s'il contenait une valeur numérique unique et transmis sous cette forme à la partie réceptrice, qui l'a utilisé comme code de sortie du PowerShell appelé.

Et pour la dernière vérification, nous allons créer sur le serveur une tâche SQL en un seul étape de type « Système d'exploitation (cmdexec) » avec ce texte :

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

OUAIS ! La tâche s'est terminée par une erreur, le texte dans le journal :

Exécuté au nom de l'utilisateur : DOMAINagentuser. Sortie vers l'hôte. ExitCode : 6. Code de sortie du processus 6. L'étape s'est terminée par une erreur.

Conclusions :

  • Évitez d'utiliser Write-Output et de référencer des expressions sans affectation. Souvenez-vous que déplacer ce code à un autre endroit du script peut entraîner des résultats inattendus.
  • Dans les scripts conçus non pas pour être exécutés manuellement, mais pour être utilisés dans vos mécanismes d'automatisation, notamment pour les appels distants via WINRM, effectuez un traitement manuel des erreurs via Try/Catch, et veillez à ce que, quoi qu'il arrive, ce script envoie dans le flux Output exactement une valeur de type primitif. Si vous souhaitez obtenir un Errorlevel classique, cette valeur doit être numérique.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster