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
Related
I am trying to run a script that searches/downloads/installs windows updates on remote computers using WinRM. I am running this script as a domain user with Admin access. However, I get an ACCESS Denied error.
Now, I have the script copied over to the remote servers but I am unable to view output to see whether the script is running or not.
OUTPUT I want to see:
# Continue running on other servers on error
$ErrorActionPreference = "Continue"
# Server list
$servers = Get-Content "C:\Users\admin\Desktop\vm-nonprod.txt"
# Logs
$log = "C:\Users\admin\Desktop\log-nonprod.txt"
# Path to script on server list
$scriptpath = "C:\Patch.ps1"
$results = #()
foreach ($server in $servers) {
try {
$Credential = Import-CliXml -Path "C:\Users\admin\Desktop\admin.Cred"
#New-PSSession -ComputerName $server -Credential $Credential
Invoke-Command -ComputerName $server -Credential $Credential -ScriptBlock {$scriptpath} -ArgumentList "Y" | Out-File -FilePath C:\Users\admin\Desktop\WinPatch.txt
#Invoke-Command -ComputerName $server -Credential hhq\admin -FilePath "C:\Users\admin\Documents\Patch.ps1"
#Copy-Item -Path C:\Users\admin\Documents\Patch.ps1 -Destination 'C:\' -ToSession (New-PSSession –ComputerName $server -Credential $Credential)
}
catch {
Write-Output ("Error running script on remote host: " + $server)
}
}
$results | Export-Csv -NoTypeInformation $log
There's a few issues here.
Does the script exist on the server?
Sounds like yes, you have Patch.ps1 in C:\ on each $server
The scriptblock does not run the script - just prints the variable.
To run it, change {$scriptpath} to {. $scriptpath} or {& $scriptpath}
The variable $scriptpath is not in the scriptblock scope - you will have to pass it in the -ArgumentList
Change: {$scriptpath} -ArgumentList "Y"
____To: {param($p); . $p} -ArgumentList $scriptpath
The argument "Y" is being passed to the scriptbock, not the script. The scriptblock is not looking for it, so this value is being lost.
Assume you want it to be passed to the script - this needs to be done in the scriptblock:
{$scriptpath "Y"}
I would recommend getting rid of Out-File until you are happy with the output in the console.
Putting it all together:
-ScriptBlock {$scriptpath} -ArgumentList "Y" | Out-File -FilePath C:\Users\admin\Desktop\WinPatch.txt
-ScriptBlock {param($p); . $p "Y"} -ArgumentList $scriptpath
I believe you have the wrong Invoke-Command commented out. The one that is running only has the user name hhq\admin in the credential parameter. It might be failing due to that because it would be prompting for the password during run-time.
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
I'm trying to write a Powershell function to create an OrganizationalUnit. I'm a PS newbie but I've pieced together:
function makeOU ($cn, $path)
{
$sb = [scriptblock]::Create(
"New-ADOrganizationalUnit $cn -path `"$path`"
-ProtectedFromAccidentalDeletion 0"
)
Invoke-Command -ComputerName $server -Credential $Credential `
-ScriptBlock $sb
}
But when I invoke this later in the script, I get a message that -ProtectedFromAccidentalDeletion is an unknown cmdlet. If I make the command one line
"New-ADOrganizationalUnit $cn -path `"$path`" -ProtectedFromAccidentalDeletion 0"
it works.
As I see it, at the end of
"New-ADOrganizationalUnit $cn -path `"$path`"
there is an open parenthesis and an open quote so PS should be looking for more input. Ending that line with a back tick didn't help. Nor did converting the argument to Create() to the form #" ... "#. (This is different from Why I am getting PowerShell Parsing Error? in that I don't have any back ticks, certainly none with spaces after them.)
If I'm making an newbie error here and there's a better way to pass the function parameters to Invoke-Command, I'm open to rewriting but failing that, how can break the string passed to Create() onto multiple lines?
Chris Nelson: Your "if you really want to" makes it sound like I'm doing something
grossly anti-idomatic to PS. I'm perfectly willing to believe that so
I wonder how you'd write it.
Thing is, backticks are frowned upon in Posh community:
READ-02 Avoid backticks
In general, the community feels you should avoid using those backticks
as "line continuation characters" when possible. They're hard to read,
easy to miss, and easy to mistype. Also, if you add an extra
whitespace after the backtick in the above example, then the command
won't work. The resulting error is hard to correlate to the actual
problem, making debugging the issue harder.
Maximum Line Length
The preferred way to avoid long lines is to use splatting (see
About_Splatting) and PowerShell's implied line continuation inside
parentheses, brackets, and braces -- these should always be used in
preference to the backtick for line continuation when applicable, even
for strings
Since you've asked, how I'd write it, here is some examples:
"Structured"
function makeOU ($cn, $path)
{
$Template = 'New-ADOrganizationalUnit {0} -Path "{1}" -ProtectedFromAccidentalDeletion 0'
$ScriptBlock = [scriptblock]::Create(($Template -f $cn, $path))
Invoke-Command -ComputerName $server -Credential $Credential -ScriptBlock $ScriptBlock
}
"Splatted"
function makeOU ($cn, $path)
{
$Template = 'New-ADOrganizationalUnit {0} -Path "{1}" -ProtectedFromAccidentalDeletion 0'
$ScriptBlock = $Template -f $cn, $path
$Splat = #{
ComputerName = $server
Credential = $Credential
ScriptBlock = [scriptblock]::Create($ScriptBlock)
}
Invoke-Command #Splat
}
"Oneliner"
function makeOU ($cn, $path)
{
Invoke-Command -ComputerName $server -Credential $Credential -ScriptBlock (
[scriptblock]::Create(
('New-ADOrganizationalUnit {0} -Path "{1}" -ProtectedFromAccidentalDeletion 0' -f $cn, $path)
)
)
}
# Or, using parentheses:
function makeOU ($cn, $path)
{
Invoke-Command -ComputerName (
$server
) -Credential (
$Credential
) -ScriptBlock (
[scriptblock]::Create(
('New-ADOrganizationalUnit {0} -Path "{1}" -ProtectedFromAccidentalDeletion 0' -f $cn, $path)
)
)
}
Well, if you really want to format it that way, try this:
function makeOU ($cn, $path)
{
$sb = [scriptblock]::Create(
"New-ADOrganizationalUnit $cn -path (
`"$path`"
) -ProtectedFromAccidentalDeletion 0"
)
Invoke-Command -ComputerName $server -Credential $Credential `
-ScriptBlock $sb
}
If you know how-to escape inner " double quotes, and you know how-to spread a command on multiple lines using a Grave Accent as an escape character:
Invoke-Command -ComputerName $server -Credential $Credential `
-ScriptBlock $sb
then you know how-to do the last in a script block as well: just double the Grave Accent character:
$sb = [scriptblock]::Create(
"New-ADOrganizationalUnit $cn -path `"$path`" ``
-ProtectedFromAccidentalDeletion 0"
)
I am trying to create a folder remotely but the parameter that I type in command line doesn't pass in script block. Below code will only create directory webapp.
$computerName = "s3apdev0074"
Invoke-Command -ComputerName $computerName -ScriptBlock { param([string]$appname) md d:\webapp\$appname}
Please try this:
$ComputerName = 's3apdev0074'
$AppName = Read-Host "Please enter the application name"
Invoke-Command -ComputerName $ComputerName -ArgumentList $AppName -ScriptBlock {
Param (
[String]$AppName
)
# DOS:
md d:\webapp\$AppName
# PowerShell: New-Item -Path "d:\webapp\$AppName" -ItemType Directory
}
It seem you are only missing the -ArgumentList parameter to pass in your local variable.
More info here:
Get-Help Invoke-Command -Parameter ArgumentList
I'm trying to use invoke-command to find a specific process using this code
Invoke-Command -ComputerName $selected_server.ServerName -ArgumentList $selected_server.ProcessId -ScriptBlock {Get-Process -Name "winlogon" | where{$_.Id -like $args[0]} }
This command doesn't work, but if I use the numeric value contained in
$selected_server.ProcessId that is 8900, instead of using $args[0], it works.
I also tried to execute this command to verify if variables are read correctly and it seems so
Invoke-Command -ComputerName $selected_server.ServerName -ArgumentList $selected_server.ProcessId -ScriptBlock {$args[0]; $args[0].gettype().fullname}
> 8900
> System.Int32
Am I missing something?
Don't know why but this works ( maybe $args in foreach-object scriptblock is out of scope):
Invoke-Command -ComputerName $selected_server.ServerName `
-ArgumentList $selected_server.ProcessId -ScriptBlock `
{param ($x) Get-Process -Name "winlogon" | where{$_.Id -like $x} }
C.B's answer is good & works anywhere you have remoting available (v2.0 & higher), but there is another (easier) way if you're using PowerShell 3.0 - the Using scope modifier. See about_Remote_Variables
Invoke-Command -ComputerName $selected_server.ServerName -ArgumentList $selected_server.ProcessId -ScriptBlock {Get-Process -Name "winlogon" | where{$_.Id -like $Using:selected_server.ProcessId} }