Връщане на стойност от powershell invoke-command към агент на SQL Server

Когато създавах моя собствена методология за управление на резервни копия на множество MS-SQL сървъри, прекарах много време в изучаване на механизма за предаване на стойности в Powershell по време на отдалечени повиквания, така че пиша напомняне на себе си, в случай че е полезно на някой друг.

И така, нека започнем с прост скрипт и да го стартираме локално:

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

За да изпълнявам скриптове, ще използвам следния CMD файл, няма да го включвам всеки път:

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

На екрана ще видим следното:

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


Сега нека изпълним същия скрипт чрез WSMAN (отдалечено):

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

И ето резултата:

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

Страхотно, Errorlevel изчезна някъде, но трябва да вземем стойността от скрипта! Нека опитаме следния дизайн:

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

Това е още по-интересно. Съобщението в Output е изчезнало някъде:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Сега, като лирично отклонение, ще отбележа, че ако във функция на Powershell напишете Write-Output или просто израз, без да го присвоите на която и да е променлива (и това имплицитно предполага изход към изходния канал), тогава дори когато се изпълнява локално, нищо няма да се покаже на екрана! Това е следствие от архитектурата на конвейера на powershell - всяка функция има свой собствен конвейер за изход, за него се създава масив и всичко, което влиза в него, се счита за резултат от изпълнението на функцията, операторът Return добавя върнатата стойност към същото pipeline като последен елемент и прехвърля управлението на извикващата функция. За да илюстрираме, нека изпълним локално следния скрипт:

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

И ето го резултата:

Main: ParameterValue

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

Основната функция (тялото на скрипта) също има свой собствен изходен канал и ако изпълним първия скрипт от CMD, пренасочвайки изхода към файл,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

тогава ще видим на екрана

ERRORLEVEL=1

и във файла

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

ако направим подобно извикване от powershell

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

тогава ще бъде на екрана

Out to host.
ExitCode: 1

и във файла

Out to output.
1

Това се случва, защото CMD стартира powershell, който при липса на други инструкции смесва две нишки (хост и изход) и ги дава на CMD, който изпраща всичко, което е получил във файл, а в случай на стартиране от powershell, тези две нишки съществуват отделно и символните пренасочвания засягат само изхода.

Връщайки се към основната тема, нека си припомним, че .NET обектният модел в powershell съществува напълно в рамките на един компютър (една ОС), когато изпълнявате код отдалечено чрез WSMAN, прехвърлянето на обекти става чрез XML сериализация, което носи много допълнителен интерес към нашите изследвания. Нека продължим нашите експерименти, като изпълним следния код:

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

И това е, което имаме на екрана:

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

Страхотен резултат! Това означава, че при извикване на Invoke-Command се запазва разделянето на тръбопроводите на две нишки (Host и Output), което ни дава надежда за успех. Нека се опитаме да оставим само една стойност в изходния поток, за която ще променим първия скрипт, който стартираме дистанционно:

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

Нека го изпълним така:

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

и... ДА, изглежда като победа!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Нека се опитаме да разберем какво се е случило. Извикахме локално powershell, който от своя страна извика powershell на отдалечения компютър и изпълни нашия скрипт там. Два потока (хост и изход) от отдалечената машина бяха сериализирани и предадени обратно, докато изходният поток, имащ една цифрова стойност в него, беше преобразуван в тип Int32 и като такъв премина към приемащата страна, а приемащата страна го използва като изходен код на извикващия powershell.

И като последна проверка, нека създадем едноетапно задание на SQL сървъра с тип „Операционна система (cmdexec)“ със следния текст:

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

УРА! Задачата е изпълнена с грешка, текст в дневника:

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

Изводи:

  • Избягвайте използването на Write-Output и указването на изрази без присвояване. Имайте предвид, че преместването на този код на друго място в скрипта може да доведе до неочаквани резултати.
  • В скриптове, предназначени не за ръчно стартиране, а за използване във вашите механизми за автоматизация, особено за отдалечени повиквания чрез WINRM, направете ръчно обработване на грешки чрез Try/Catch и се уверете, че при всяко развитие на събития този скрипт изпраща точно една стойност на примитивен тип . Ако искате да получите класическото ниво на грешка, тази стойност трябва да е числова.

Източник: www.habr.com

Добавяне на нов коментар