Uruchamianie skryptu .ps1 z PowerShell z parametrami i poświadczeniami i uzyskiwanie danych wyjściowych za pomocą zmiennej

10

Witaj Stack Community :)

Mam prosty cel. Chciałbym uruchomić skrypt PowerShell z innego skryptu PowerShell, ale są 3 warunki:

  1. Muszę przekazać poświadczenia (wykonanie łączy się z bazą danych, która ma określonego użytkownika)
  2. Musi wziąć kilka parametrów
  3. Chciałbym przekazać dane wyjściowe do zmiennej

Istnieje podobne pytanie Link . Ale odpowiedzią jest użycie plików jako sposobu komunikacji między 2 skryptami PS. Chciałbym tylko uniknąć konfliktów dostępu. @Update: Skrypt główny uruchomi kilka innych skryptów. więc rozwiązanie z plikami może być trudne, jeśli wykonanie zostanie wykonane przez wielu użytkowników jednocześnie.

Script1.ps1 to skrypt, który powinien mieć ciąg wyjściowy. (Żeby było jasne, to fikcyjny skrypt, prawdziwy ma 150 wierszy, więc chciałem tylko dać przykład)

param(  
[String]$DeviceName
)
#Some code that needs special credentials
$a = "Device is: " + $DeviceName
$a

Plik ExecuteScripts.ps1 powinien wywoływać ten z tymi 3 warunkami wymienionymi powyżej

Próbowałem wielu rozwiązań. Ten na przykład:

$arguments = "C:\..\script1.ps1" + " -ClientName" + $DeviceName
$output = Start-Process powershell -ArgumentList $arguments -Credential $credentials
$output 

Nie otrzymuję z tego żadnych danych wyjściowych i nie mogę po prostu wywołać skryptu

&C:\..\script1.ps1 -ClientName PCPC

Ponieważ nie mogę przekazać -Credentialparametru do niego ..

Z góry dziękuję!

Dmytro
źródło
Jeśli chodzi tylko o konflikty dostępu: utworzenie unikalnych nazw plików dla każdego wywołania rozwiązałoby problem, prawda?
mklement0
1
@ mklement0 jeśli to jedyny sposób, postawiłbym stos z tym rozwiązaniem. Po prostu generowanie losowych nazw plików, sprawdzanie, czy taki plik istnieje ... Będę wykonywać od 6 do 10 skryptów z mojego kodu Java i będzie potrzebował od 6 do 10 plików za każdym razem, gdy używam lub ktoś inny korzysta z mojej aplikacji. Więc chodzi również o wydajność
Dmytro

Odpowiedzi:

2

Uwaga:

  • Poniższe rozwiązanie działa z dowolnym programem zewnętrznym i niezmiennie przechwytuje dane wyjściowe jako tekst .

  • Aby wywołać inną instancję programu PowerShell i przechwycić dane wyjściowe jako obiekty bogate (z ograniczeniami), zapoznaj się z wariantem rozwiązania w dolnej części lub rozważ pomocną odpowiedź Mathiasa R. Jessena , która korzysta z zestawu SDK programu PowerShell .

Oto dowód koncepcji oparty na bezpośrednim użyciu typów System.Diagnostics.Processi System.Diagnostics.ProcessStartInfo.NET do przechwytywania danych wyjściowych procesu w pamięci (jak podano w pytaniu, Start-Processnie jest opcją, ponieważ obsługuje tylko przechwytywanie danych wyjściowych w plikach , jak pokazano w tej odpowiedzi ) :

Uwaga:

  • Ze względu na to, że działa jako inny użytkownik, jest obsługiwany tylko w systemie Windows (od .NET Core 3.1), ale w obu tam wersjach PowerShell.

  • Ze względu na potrzeby działać jako inny użytkownik i konieczności wyjścia przechwytywania, .WindowStylenie mogą być wykorzystane do uruchomienia polecenia ukryty (ponieważ używając .WindowStylewymaga .UseShellExecutesię $true, co jest niezgodne z tymi wymaganiami); Jednakże, ponieważ cała produkcja jest zrobione , ustawienie .CreateNoNewWindowsię $trueskutecznie prowadzi ukryte wykonania.

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # For demo purposes, use a simple `cmd.exe` command that echoes the username. 
  # See the bottom section for a call to `powershell.exe`.
  FileName = 'cmd.exe'
  Arguments = '/c echo %USERNAME%'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # ([email protected]), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output.
# By reading to the *end*, this implicitly waits for (near) termination
# of the process.
# Do NOT use $ps.WaitForExit() first, as that can result in a deadlock.
$stdout = $ps.StandardOutput.ReadToEnd()

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

"Running ``cmd /c echo %USERNAME%`` as user $($cred.UserName) yielded:"
$stdout

Z powyższego wynika coś takiego, co pokazuje, że proces został pomyślnie uruchomiony z podaną tożsamością użytkownika:

Running `cmd /c echo %USERNAME%` as user jdoe yielded:
jdoe

Ponieważ wywołujesz inną instancję programu PowerShell , możesz skorzystać ze zdolności interfejsu PowerShell CLI do reprezentowania danych wyjściowych w formacie CLIXML, co umożliwia deserializację danych wyjściowych w obiekty bogate , aczkolwiek z ograniczoną wiernością typów , jak wyjaśniono w tej pokrewnej odpowiedzi .

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # Invoke the PowerShell CLI with a simple sample command
  # that calls `Get-Date` to output the current date as a [datetime] instance.
  FileName = 'powershell.exe'
  # `-of xml` asks that the output be returned as CLIXML,
  # a serialization format that allows deserialization into
  # rich objects.
  Arguments = '-of xml -noprofile -c Get-Date'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # ([email protected]), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output, in CLIXML format,
# stripping the `#` comment line at the top (`#< CLIXML`)
# which the deserializer doesn't know how to handle.
$stdoutCliXml = $ps.StandardOutput.ReadToEnd() -replace '^#.*\r?\n'

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

# Use PowerShell's deserialization API to 
# "rehydrate" the objects.
$stdoutObjects = [Management.Automation.PSSerializer]::Deserialize($stdoutCliXml)

"Running ``Get-Date`` as user $($cred.UserName) yielded:"
$stdoutObjects
"`nas data type:"
$stdoutObjects.GetType().FullName

Powyższe dane wyjściowe wyglądają mniej więcej tak, jak pokazano, pokazując, że dane wyjściowe [datetime]instance ( System.DateTime) Get-Datezostały zdezrializowane jako takie:

Running `Get-Date` as user jdoe yielded:

Friday, March 27, 2020 6:26:49 PM

as data type:
System.DateTime
mklement0
źródło
5

Start-Processbyłby moim ostatecznym wyborem przy wywoływaniu programu PowerShell z programu PowerShell - szczególnie dlatego, że wszystkie operacje we / wy stają się ciągami, a nie obiektami (bez deserializacji).

Dwie alternatywy:

1. Jeśli użytkownik jest lokalnym administratorem i skonfigurowano PSRemoting

Jeśli zdalna sesja na komputerze lokalnym (niestety ograniczona do lokalnych administratorów) jest opcją, zdecydowanie wybrałbym Invoke-Command:

$strings = Invoke-Command -FilePath C:\...\script1.ps1 -ComputerName localhost -Credential $credential

$strings będzie zawierać wyniki.


2. Jeśli użytkownik nie jest administratorem w systemie docelowym

Możesz napisać własny „tylko lokalny Invoke-Command”, rozbijając obszar roboczy poza procesem poprzez:

  1. Tworzenie PowerShellProcessInstance, pod innym loginem
  2. Tworzenie obszaru roboczego w tym procesie
  3. Wykonaj kod we wspomnianym obszarze roboczym poza procesem

Złożyłem taką funkcję poniżej, zobacz komentarze do przewodnika:

function Invoke-RunAs
{
    [CmdletBinding()]
    param(
        [Alias('PSPath')]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        ${FilePath},

        [Parameter(Mandatory = $true)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${Credential},

        [Alias('Args')]
        [Parameter(ValueFromRemainingArguments = $true)]
        [System.Object[]]
        ${ArgumentList},

        [Parameter(Position = 1)]
        [System.Collections.IDictionary]
        $NamedArguments
    )

    begin
    {
        # First we set up a separate managed powershell process
        Write-Verbose "Creating PowerShellProcessInstance and runspace"
        $ProcessInstance = [System.Management.Automation.Runspaces.PowerShellProcessInstance]::new($PSVersionTable.PSVersion, $Credential, $null, $false)

        # And then we create a new runspace in said process
        $Runspace = [runspacefactory]::CreateOutOfProcessRunspace($null, $ProcessInstance)
        $Runspace.Open()
        Write-Verbose "Runspace state is $($Runspace.RunspaceStateInfo)"
    }

    process
    {
        foreach($path in $FilePath){
            Write-Verbose "In process block, Path:'$path'"
            try{
                # Add script file to the code we'll be running
                $powershell = [powershell]::Create([initialsessionstate]::CreateDefault2()).AddCommand((Resolve-Path $path).ProviderPath, $true)

                # Add named param args, if any
                if($PSBoundParameters.ContainsKey('NamedArguments')){
                    Write-Verbose "Adding named arguments to script"
                    $powershell = $powershell.AddParameters($NamedArguments)
                }

                # Add argument list values if present
                if($PSBoundParameters.ContainsKey('ArgumentList')){
                    Write-Verbose "Adding unnamed arguments to script"
                    foreach($arg in $ArgumentList){
                        $powershell = $powershell.AddArgument($arg)
                    }
                }

                # Attach to out-of-process runspace
                $powershell.Runspace = $Runspace

                # Invoke, let output bubble up to caller
                $powershell.Invoke()

                if($powershell.HadErrors){
                    foreach($e in $powershell.Streams.Error){
                        Write-Error $e
                    }
                }
            }
            finally{
                # clean up
                if($powershell -is [IDisposable]){
                    $powershell.Dispose()
                }
            }
        }
    }

    end
    {
        foreach($target in $ProcessInstance,$Runspace){
            # clean up
            if($target -is [IDisposable]){
                $target.Dispose()
            }
        }
    }
}

Następnie użyj tak:

$output = Invoke-RunAs -FilePath C:\path\to\script1.ps1 -Credential $targetUser -NamedArguments @{ClientDevice = "ClientName"}
Mathias R. Jessen
źródło
0

rcv.ps1

param(
    $username,
    $password
)

"The user is:  $username"
"My super secret password is:  $password"

wykonanie z innego skryptu:

.\rcv.ps1 'user' 'supersecretpassword'

wynik:

The user is:  user
My super secret password is:  supersecretpassword
thepip3r
źródło
1
Muszę przekazać dane uwierzytelniające tej egzekucji ...
Dmytro
zaktualizowałem odpowiednie porcje.
thepip3r
Wyjaśnienie: celem nie jest tylko przekazanie poświadczeń, ale działanie jako użytkownik zidentyfikowany przez poświadczenia.
mklement0
1
@ mklement0, dziękuję za wyjaśnienie, ponieważ nie były one dla mnie jasne przez różne iteracje zadawanego pytania.
thepip3r
0

Co możesz zrobić, aby przekazać parametr do skryptu ps1.

Pierwszym skryptem może być origin.ps1, w którym piszemy:

& C:\scripts\dest.ps1 Pa$$w0rd parameter_a parameter_n

Skrypt docelowy dest.ps1 może zawierać następujący kod do przechwytywania zmiennych

$var0 = $args[0]
$var1 = $args[1]
$var2 = $args[2]
Write-Host "my args",$var0,",",$var1,",",$var2

I wynik będzie

my args Pa$$w0rd, parameter_a, parameter_n
Andy McRae
źródło
1
Głównym celem jest połączenie wszystkich warunków w 1 wykonanie. Muszę przekazać parametry i poświadczenia!
Dmytro
Co masz na myśli przez „połącz wszystkie warunki w 1 wykonanie”. Nie sądzę, żebyś mógł dodać parametr z symbolem „-” tak jak zrobiłeś. Myślę, że musisz ponownie sformatować ciągi w skrypcie docelowym
Andy McRae
Muszę wykonać plik PS1 z parametrami i przekazać -Credential $credentialsparametr do tego wykonania i uzyskać wynik z niego w zmiennej. PS1. Skrypt, który wykonuję, wyrzuca na końcu pojedynczy ciąg słów. Spójrz tylko, jak to zrobiłem, Start-processale ta funkcja nie generuje danych wyjściowych
Dmytro
Myślę, że PowerShell nie pozwala na przekazanie parametru takiego jak $ arguments = "C: \ .. \ script1.ps1" + "-ClientName" + $ DeviceName. Prawdopodobnie powinieneś pomyśleć o usunięciu „-”
Andy McRae
1
to powiedziało. Start-Process wykonuje skrypt z parametrami i poświadczeniami, ale nie zapisuje tego wyniku w zmiennej. Jeśli próbuję uzyskać dostęp do $outputzmiennej, jest NULL. Innym pomysłem pochodzącym z @ mklement0 jest zapisanie danych wyjściowych w pliku. Ale w moim przypadku spowoduje to ogromną liczbę plików w jednym miejscu. Wszystkie utworzone przez różnych użytkowników z różnymi skryptami
Dmytro