Execute a remote generic Powershell script with generic parameters - powershell

I need to write a Powershell script (let's call it "the controller script") that is able to call a generic remote Powershell script passing generic parameters.
The controller script accepts as parameters the hostname, the credentials, the remote script path and the remote script's parameters as a hashtable.
The remote script, instead, may be any script which accepts any string parameter.
Using the hashtable parameter for the controller script is useful in that I can pass a dynamic dictionary of parameters (that depends on the controller call) while making PS do the work of "transform" the dictionary to a list of string parameters like -Param1 Value1 -Param2 Value2.
I got some ideas from this answer and this is what I did (the "controller" script):
Param(
[string] $ComputerName,
[string] $Username,
[string] $Password,
[string] $ScriptPath,
[string] $Parameters
)
$EncPassword = ConvertTo-SecureString $Password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential($Username,$EncPassword)
$ScriptBlock = [Scriptblock]::Create(".$ScriptPath $(&{$args} #Parameters)")
Invoke-Command -ComputerName $ComputerName -Credential $cred -Scriptblock $ScriptBlock
Then I execute it via the PS prompt this way:
.\controller.ps1 -ComputerName MACHINE_NAME -Username USERNAME -Password PASSWORD -ScriptPath "D:\TestScript.ps1" -Parameters #{AParameter = "asd"}
The execution fails with this error:
The term '.D:\TestScript.ps1' is not recognized as the name of a
cmdlet, function, script file, or operable program. Check the spelling
of the name, or if a path was included, verify that the path is
correct and try again.
So it seems that the Scriptblock refers to a local script (on the controller's machine), not to the remote machine where the target script resides.
Is there any way to let me execute a remote PS script using the hashtable parameter, which is the desired flexibility requirement?
UPDATE 1
I added a whitespace between the dot and the $ScriptPath variable in the ScriptBlock definition but the error is the same (without the dot).
$ScriptBlock = [Scriptblock]::Create(". $ScriptPath $(&{$args} #Parameters)")
The term 'D:\TestScript.ps1' is not recognized as the name of a
cmdlet, function, script file, or operable program. Check the spelling
of the name, or if a path was included, verify that the path is
correct and try again.
UPDATE 2
I've found a way to call the remote script without the parameters.
Param(
[string] $ComputerName,
[string] $Username,
[string] $Password,
[string] $ScriptPath,
[hashtable] $Parameters
)
$EncPassword = ConvertTo-SecureString $Password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential($Username,$EncPassword )
Invoke-Command -ComputerName $computerName -Credential $cred -ScriptBlock {Invoke-Expression $args[0]} -ArgumentList $ScriptPath
I get the remote script output without parameters. Now the left thing to do is splatting the hashtable $Parameters remotely when calling the script at the remote path $ScriptPath. Do you have any idea? I made some trials but nothing worked.

I finally found the solution
controller.ps1
Param(
[string] $ComputerName,
[string] $Username,
[string] $Password,
[string] $ScriptPath,
[hashtable] $Parameters
)
$EncPassword = ConvertTo-SecureString $Password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential($Username,$EncPassword )
Invoke-Command -ComputerName $computerName -Credential $cred -ScriptBlock {
$params = $Using:Parameters
Invoke-Expression "$Using:ScriptPath #params"
}
As you can see here we use the $Using variable in the ScriptBlock to retrieve the outside variables ($ScriptPath and $Parameters) and then we call the remote script splatting the parameters hashtable.

I'd recommend using the FilePath parameter in Invoke-Command rather than the scriptblock. That way PSRemoting does all the heavy lifting.
Invoke-Command -ComputerName $ComputerName -Credential $cred -FilePath $ScriptPath -ArgumentList $Parameters,$args
Otherwise you can use Sessions to copy the file and run the file.
$Session = New-PSSession -ComputerName $Computer -Credential $credential
Copy-Item -Path $ScriptPath -Destination $Dest -ToSession $Session
Invoke-Command -Session $Session -ScriptBlock $ScriptBlock
Edit: This may be more of what you were looking for:
The first issue seems to be that you don't have the correct path of the script or your user account doesn't have access to that path. Thats the error you are seeing. I've tested on my systems a few different ways and dot-sourcing should work.
The previous method expands the variables too early to use splatting. This is how to get splatting to work:
Invoke-command -ScriptBlock {$a = $args[0]; & D:\full\path\to\testscript.ps1 #a $args[1]} -ArgumentList $Parameters,$additionalArgs
Be sure to get a hash table instead of string in the params
[HashTable] $Parameters

Related

Passing Powershell variables to batch files [duplicate]

This question already has answers here:
How do I pass variables with the Invoke-Command cmdlet?
(3 answers)
Closed 3 years ago.
I have some problem passing the PowerShell variables to Invoke-Command (running CMD /C). The problem is, that the $script_location parameter is empty.
My script creates a batch file on an UNC path with an unique name (20190423T1152100840Z.bat) with some commands in it. At the end, I would like to run this generated batch file on a server.
$datum = Get-Date -Format FileDateTimeUniversal
$script_location = "\\uncpath\batchfile_$datum.bat"
$Username = 'user'
$Password = 'pass'
$pass = ConvertTo-SecureString -AsPlainText $Password -Force
$Cred = New-Object System.Management.Automation.PSCredential -ArgumentList $Username,$pass
Add-Content $script_location " preferences_manager -u=user -p=pass -target=$env:username"
Invoke-Command -ComputerName "SERVERNAME" -Credential $cred -ScriptBlock {
Invoke-Expression -Command:"cmd /c $script_location"
}
The expected result should be that the Invoke-Command runs the $script_location batch file.
Currently the result is that that the $script_location variable is empty.
Any clue on how to do that?
The easiest way to pass a variable into a ScriptBlock is by prefixing its name with the using: scope.
Invoke-Command -ComputerName "SERVERNAME" -Credential $cred -ScriptBlock {
Invoke-Expression -Command:"cmd /c $using:script_location"
}
You can also pass it as an argument:
Invoke-Command -ComputerName "SERVERNAME" -Credential $cred -ScriptBlock {
Param($ScriptLocation)
Invoke-Expression -Command:"cmd /c $ScriptLocation"
} -ArgumentList $script_location

Passing Credentials In PowerShell multi line scriptblock

I have passed credentials before using a credential parameter in my Scriptblock and passing the value via an argument. I expect the size of my Scriptblock to grow so I am using a here string to keep it clean then I convert the string into a Scriptblock. How do I add a credential parameter and argument to my example below. I know the $credential value I use to get my remote session below has the necessary priveleges to get the file I want as I have tested it on the remote machine. So if possible I would like to pass this same credential.
$user = 'MyDomain\username'
$password = ConvertTo-SecureString 'mypassword' -asplaintext -force
$credential = New-Object -typename System.Management.Automation.PSCredential -ArgumentList $user, $password
try {
$s = New-PSSession -ComputerName MyRemoteComputer -Credential $credential
$remoteCommand = #"
New-PSDrive -Name 'P' -PSProvider 'FileSystem' -Root '\\main-server\Folders\DevOps\Projetcs\Juniper'
Get-Item -Path P:\V1.6\InstallFiles\Install.bat
"#
$scriptBlock = [Scriptblock]::Create($remoteCommand)
$status = Invoke-Command -Session $s -ScriptBlock $scriptBlock
Write-Host $status
Exit-PSSession -Session $s
}
catch {
#TODO Add exception handling
}

Power Shell - Copy-Item UNC Path, Cannot Find Path... Does not Exist [duplicate]

I need to install an application in several remote servers in quiet mode. I have created a script (Installer.ps1) like below using Powershell v3.0:
param(
[String] $ServerNameFilePath = $(throw "Provide the path of text file which contains the server names"),
[String] $InstallerFolderPath = $(throw "Provide the Installer Folder Path. This should be a network location"),
[String] $UserName = $(throw "Provide the User Name"),
[String] $Password= $(throw "Provide the Password")
)
Function InstallApp
{
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$mycreds = New-Object System.Management.Automation.PSCredential ($UserName, $secpasswd)
$ScrBlock = {param($InstallerFolderPath) $ExePath = Join-Path $InstallerFolderPath "ServerReleaseManager.exe"; & $ExePath /q;}
Invoke-Command -ComputerName (Get-Content Servers.txt) -Credential $mycreds $ScrBlock -ArgumentList $InstallerFolderPath
}
InstallApp -ServerNameFilePath $ServerNameFilePath -InstallerFolderPath $InstallerFolderPath -UserName $UserName -Password $Password
Then I call the script like below (Installer folder path can have white spaces and the executable ServerReleaseManager.exe accepts argument):
.\Installer.ps1 -ServerNameFilePath Servers.txt -InstallerFolderPath "\\TestServer01\Public\Stable Applications\Server Release Manager Update 2\2.7" -UserName "Domain\User" -Password "Test123"
I am getting below CommandNotFoundException always:
The term '\\TestServer01\Public\Stable Applications\Server Release Manager Update 2\2.7\ServerReleaseManager.exe' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
I have tried other options like using -FilePath with Invoke-Command but same error. I am really blocked here. Can you please let me know why this error has shown? How to resolve the error? Or are there any better ways to deal with this. Thanks for your help.
This sounds like a double-hop authentication issue. Once you're remoted into the server, you can't access a file share on a third server because you can't pass your kerberos-based authentication to it.
You could try copying from the share to the remote server, first (this has to be done on the computer executing the script), and then in the scriptblock refer to the (now local) path.
You could set up CredSSP which isn't a great idea for this purpose.
Basically, you need to avoid connecting to one machine, then connecting to another through that connection.
Code that implements the workaround I'm describing:
param(
[String] $ServerNameFilePath = $(throw "Provide the path of text file which contains the server names"),
[String] $InstallerFolderPath = $(throw "Provide the Installer Folder Path. This should be a network location"),
[String] $UserName = $(throw "Provide the User Name"),
[String] $Password= $(throw "Provide the Password")
)
Function InstallApp
{
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$mycreds = New-Object System.Management.Automation.PSCredential ($UserName, $secpasswd)
$ScrBlock = {param($InstallerFolderPath) $ExePath = Join-Path $InstallerFolderPath "ServerReleaseManager.exe"; & $ExePath /q;}
Get-Content Servers.txt | ForEach-Item {
$remoteDest = "\\$_\c`$\some\temp\folder"
$localDest = "C:\some\temp\folder" | Join-Path -ChildPath ($InstallerFolderPath | Split-Path -Leaf)
try {
Copy-Item -Path $InstallerFolderPath -Destination $dest -Force
Invoke-Command -ComputerName $_ -Credential $mycreds $ScrBlock -ArgumentList $localDest
finally {
Remove-Item $remoteDest -Force -ErrorAction Ignore
}
}
}
InstallApp -ServerNameFilePath $ServerNameFilePath -InstallerFolderPath $InstallerFolderPath -UserName $UserName -Password $Password
Notes
This is untested.
As mentioned by Swonkie, you should set your parameters as mandatory if that's what you're looking to achieve (not addressed in my code).
You shouldn't pass separate plain text user name and password parameters and then convert them to a credential object. Instead pass a single [PSCredential] parameter. You can use a default value that prompts, like [PSCredential] $Cred = (Get-Credential). (this is not addressed in my code either).
Desired state configuration can be used to install software on target machines. I assume this can work around the double hop issue.
http://technet.microsoft.com/de-de/library/dn282132.aspx
http://technet.microsoft.com/de-de/library/dn282129.aspx
By the way - dont throw errors for missing mandatory arguments. Let PowerShell handle that - it's much more user friendly:
param(
[parameter(Mandatory=$true)] [string] $ServerNameFilePath,
[parameter(Mandatory=$true)] [string] $InstallerFolderPath,
[parameter(Mandatory=$true)] [string] $UserName,
[parameter(Mandatory=$true)] [string] $Password
)
Here I created a new PSsession to each server in the list and used the invoke command to target that server's session. I've tested it in my environment and it successfully installs my exe application with a /q switch on my remote servers.
This method however does not tell if you the command ran successfully on the remote side, you would have to logon to the server or do a test-path to the expected location of the installed files for validation. Also, PSsessions are held open until the console that launched the command is closed. If a PSsession ends before the install completes, the install will fail.
Function InstallApp {
param(
[parameter(Mandatory=$true)] [String] $ServerNameFilePath,
[parameter(Mandatory=$true)] [String] $InstallerFilePath,
[parameter(Mandatory=$true)] [String] $CommandArgument,
[parameter(Mandatory=$true)] [String] $UserName,
[parameter(Mandatory=$true)] [String] $Password
)
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$mycreds = New-Object System.Management.Automation.PSCredential ($UserName, $secpasswd)
Get-Content $ServerNameFilePath | ForEach-Object {
$remoteSession = new-PSSession $_ -Credential $mycreds
Invoke-command -Session $remoteSession -Scriptblock {& ($args[0]) #($args[1])} -ArgumentList $InstallerFilePath,$CommandArgument
}
}
InstallApp -ServerNameFilePath $ServerNameFilePath -InstallerFilePath $InstallerFilePath -CommandArgument $CommandArgument -UserName $UserName -Password $Password

Importing Scriptblock from file

I've got a working Powershell script and I'd like to have the scriptblock pulled in from an external file.
Working:
$scriptblock = { ... }
invoke-command -ComputerName $server -ScriptBlock $Scriptblock -ArgumentList $server,$team -Credential $credential -asjob -JobName Dashboard_$server -SessionOption (New-PSSessionOption -NoMachineProfile)
Output of "get-job -id | receive-job" is fine
Not working:
# Generate scriptblock from file
$file = Get-Content E:\Dashboard\Windows\winrm_scriptblock.txt
$Scriptblock = $executioncontext.invokecommand.NewScriptBlock($file)
invoke-command -ComputerName $server -ScriptBlock $Scriptblock -ArgumentList $server,$team -Credential $credential -asjob -JobName Dashboard_$server -SessionOption (New-PSSessionOption -NoMachineProfile)
Output of "get-job -id | receive-job" is empty
The contents of winrm_scriptblock.txt is exactly what is included between the braces in the scriptblock variable defined in the working version.
Any assistance is appreciated.
I know you already have answers, but another way to get a scriptblock from a script file is to use the get-command cmdlet:
$sb=get-command C:\temp\add-numbers.ps1 | select -ExpandProperty ScriptBlock
$sb is now the scriptblock for the script.
Very related to the answer from How do I pass a scriptblock as one of the parameters in start-job
If you stored the string "Get-ChildItem C:\temp" in the file "E:\Dashboard\Windows\winrm_scriptblock.txt" then this code should output the contents of the folder "C:\temp" on your local machine.
Invoke-Command -ScriptBlock ([scriptblock]::Create((Get-Content "E:\Dashboard\Windows\winrm_scriptblock.txt")))
Parameters
As far as passing parameters goes Pass arguments to a scriptblock in powershell covers that answer as well. As Keith Hill states: a scriptblock is just an anonymous function
Consider the following file contents
param(
$number
)
$number..2 | ForEach-Object{
Write-Host "$_ lines of code in the file."
}
And the command
Invoke-Command -ScriptBlock ([scriptblock]::Create((Get-Content "E:\Dashboard\Windows\winrm_scriptblock.txt"))) -ArgumentList "99"
Would give you the annoying output of
99 lines of code in the file.
98 lines of code in the file.
97 lines of code in the file.
....
Any reason not to just use the -FilePath parameter of Invoke-Command?
you must extract {} from E:\Dashboard\Windows\winrm_scriptblock.txt

Issues with Invoke-Command while installing softwares in remote server

I need to install an application in several remote servers in quiet mode. I have created a script (Installer.ps1) like below using Powershell v3.0:
param(
[String] $ServerNameFilePath = $(throw "Provide the path of text file which contains the server names"),
[String] $InstallerFolderPath = $(throw "Provide the Installer Folder Path. This should be a network location"),
[String] $UserName = $(throw "Provide the User Name"),
[String] $Password= $(throw "Provide the Password")
)
Function InstallApp
{
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$mycreds = New-Object System.Management.Automation.PSCredential ($UserName, $secpasswd)
$ScrBlock = {param($InstallerFolderPath) $ExePath = Join-Path $InstallerFolderPath "ServerReleaseManager.exe"; & $ExePath /q;}
Invoke-Command -ComputerName (Get-Content Servers.txt) -Credential $mycreds $ScrBlock -ArgumentList $InstallerFolderPath
}
InstallApp -ServerNameFilePath $ServerNameFilePath -InstallerFolderPath $InstallerFolderPath -UserName $UserName -Password $Password
Then I call the script like below (Installer folder path can have white spaces and the executable ServerReleaseManager.exe accepts argument):
.\Installer.ps1 -ServerNameFilePath Servers.txt -InstallerFolderPath "\\TestServer01\Public\Stable Applications\Server Release Manager Update 2\2.7" -UserName "Domain\User" -Password "Test123"
I am getting below CommandNotFoundException always:
The term '\\TestServer01\Public\Stable Applications\Server Release Manager Update 2\2.7\ServerReleaseManager.exe' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
I have tried other options like using -FilePath with Invoke-Command but same error. I am really blocked here. Can you please let me know why this error has shown? How to resolve the error? Or are there any better ways to deal with this. Thanks for your help.
This sounds like a double-hop authentication issue. Once you're remoted into the server, you can't access a file share on a third server because you can't pass your kerberos-based authentication to it.
You could try copying from the share to the remote server, first (this has to be done on the computer executing the script), and then in the scriptblock refer to the (now local) path.
You could set up CredSSP which isn't a great idea for this purpose.
Basically, you need to avoid connecting to one machine, then connecting to another through that connection.
Code that implements the workaround I'm describing:
param(
[String] $ServerNameFilePath = $(throw "Provide the path of text file which contains the server names"),
[String] $InstallerFolderPath = $(throw "Provide the Installer Folder Path. This should be a network location"),
[String] $UserName = $(throw "Provide the User Name"),
[String] $Password= $(throw "Provide the Password")
)
Function InstallApp
{
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$mycreds = New-Object System.Management.Automation.PSCredential ($UserName, $secpasswd)
$ScrBlock = {param($InstallerFolderPath) $ExePath = Join-Path $InstallerFolderPath "ServerReleaseManager.exe"; & $ExePath /q;}
Get-Content Servers.txt | ForEach-Item {
$remoteDest = "\\$_\c`$\some\temp\folder"
$localDest = "C:\some\temp\folder" | Join-Path -ChildPath ($InstallerFolderPath | Split-Path -Leaf)
try {
Copy-Item -Path $InstallerFolderPath -Destination $dest -Force
Invoke-Command -ComputerName $_ -Credential $mycreds $ScrBlock -ArgumentList $localDest
finally {
Remove-Item $remoteDest -Force -ErrorAction Ignore
}
}
}
InstallApp -ServerNameFilePath $ServerNameFilePath -InstallerFolderPath $InstallerFolderPath -UserName $UserName -Password $Password
Notes
This is untested.
As mentioned by Swonkie, you should set your parameters as mandatory if that's what you're looking to achieve (not addressed in my code).
You shouldn't pass separate plain text user name and password parameters and then convert them to a credential object. Instead pass a single [PSCredential] parameter. You can use a default value that prompts, like [PSCredential] $Cred = (Get-Credential). (this is not addressed in my code either).
Desired state configuration can be used to install software on target machines. I assume this can work around the double hop issue.
http://technet.microsoft.com/de-de/library/dn282132.aspx
http://technet.microsoft.com/de-de/library/dn282129.aspx
By the way - dont throw errors for missing mandatory arguments. Let PowerShell handle that - it's much more user friendly:
param(
[parameter(Mandatory=$true)] [string] $ServerNameFilePath,
[parameter(Mandatory=$true)] [string] $InstallerFolderPath,
[parameter(Mandatory=$true)] [string] $UserName,
[parameter(Mandatory=$true)] [string] $Password
)
Here I created a new PSsession to each server in the list and used the invoke command to target that server's session. I've tested it in my environment and it successfully installs my exe application with a /q switch on my remote servers.
This method however does not tell if you the command ran successfully on the remote side, you would have to logon to the server or do a test-path to the expected location of the installed files for validation. Also, PSsessions are held open until the console that launched the command is closed. If a PSsession ends before the install completes, the install will fail.
Function InstallApp {
param(
[parameter(Mandatory=$true)] [String] $ServerNameFilePath,
[parameter(Mandatory=$true)] [String] $InstallerFilePath,
[parameter(Mandatory=$true)] [String] $CommandArgument,
[parameter(Mandatory=$true)] [String] $UserName,
[parameter(Mandatory=$true)] [String] $Password
)
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$mycreds = New-Object System.Management.Automation.PSCredential ($UserName, $secpasswd)
Get-Content $ServerNameFilePath | ForEach-Object {
$remoteSession = new-PSSession $_ -Credential $mycreds
Invoke-command -Session $remoteSession -Scriptblock {& ($args[0]) #($args[1])} -ArgumentList $InstallerFilePath,$CommandArgument
}
}
InstallApp -ServerNameFilePath $ServerNameFilePath -InstallerFilePath $InstallerFilePath -CommandArgument $CommandArgument -UserName $UserName -Password $Password