Powershell String Variable Not Happy - powershell

I have the following powershell script:
$BizTalkHosts = "BTSSvc*"
Foreach($svc in Invoke-Command -Computer d-vasbiz01 -ScriptBlock{ get-service -Name $BizTalkHosts})
{
Write-Host $svc.name
}
I want this to return a list of services on the remote computer that begin with "BTSSVC*". Problem is, I won't actually know the service name until runtime, it will be passed into the script as a param.
When I run the above script I get a list of ALL services - not what I want! However, if I provide a string literal to the get-service cmdlet (i.e. get-service -Name "BTSSvc*) it works fine, providing a filtered list.
Can anyone please explain what I'm doing wrong?

There's no need to use Invoke-Command in this case, you can get the services with Get-Service :
Get-Service -Name $BizTalkHosts -ComputerName d-vasbiz01
To be able to do that with Invoke-Command (which is an overkill), you need to create a parameter inside the script block and pass $BizTalkHosts to the script block via the -ArgumentList parameter
$BizTalkHosts = "BTSSvc*"
Foreach($svc in Invoke-Command -Computer d-vasbiz01 -ScriptBlock{ param($name) get-service -Name $name}) -Argument $ArgumentList
{
Write-Host $svc.name
}

Related

Using Invoke-Command with Variables in ScriptBlock

New to PowerShell and learning through writing random scripts using the help info. I've tried the following 3 ways to properly get variables into the ScriptBlock(along with way too many small variations to list) with listed error message wrapped in **:
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $DaysAgo | Out-File $HOME\$location\$filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $Using:$DaysAgo | Out-File $Using:HOME\$Using:location\$Using:filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
Write-Host "Processing..."
$DaysAgo = [datetime]::Now.AddDays(-$days)
$parameters = #{
ScriptBlock = { Param ($Arg1,$Arg2,$Arg3) Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object source -eq DCOM | where TimeGenerated -ge $Arg1 | Out-File "$HOME\$Arg2\$Arg3.txt"}}
JobName = "DCOM"
ArgumentList = ($DaysAgo,$location,$filename)
}
Invoke-Command #parameters
Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for the argument, and then try running the command again.
I'm just looking to have user input how far back they want to view Event Logs, where to save it, and what to name it. I've been able to work my way through everything so far until I hit the Invoke-Command line and haven't been able to get through it. I prefer the one line style of 1 and 2 over the parameters style, however after spending way too much time using the help_Invoke-Command-full and googling I'm throwing in the towel over what I'm sure is a simple error on my syntax.
You can use $args inside the scriptblock, see an example:
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {
Get-EventLog -logname system | Where-Object EntryType -eq Warning |
where TimeGenerated -ge $args[0] |
Out-File $HOME\$location\$filename.txt
} -ArgumentList $DaysAgo
Add the arguments at the end of the Invoke-Command like in the example and use $args[0] for the first argument, $args[1] for the second and so on...
This works for me. The computer is localhost as a test, at an elevated prompt, which you would need for the system log anyway. Level 3 is warning. If it was for the same computer you wouldn't need invoke-command at all.
$location = 'foo'
$filename = 'myfile'
$date = get-date
$daysago = $date.adddays(-1)
invoke-command localhost { param($daysago, $location, $filename)
get-winevent #{logname = 'system'; level = 3; starttime = $daysago} |
out-file $home\$location\$filename.txt } -args $daysago,$location,$filename
In order to use Invoke-Command's -AsJob switch, you must execute code remotely, such as by targeting a different computer with the -ComputerName or -Session arguments.
In the absence of such arguments, your command would run locally, but it fails due to the syntactic restriction described above.
If you want to run a job locally, use Start-Job directly:
$job = Start-Job -Name JobEventLog -ScriptBlock {
Get-EventLog -logname system |
Where-Object EntryType -eq Warning |
Where-Object TimeGenerated -ge $using:DaysAgo |
Out-File $HOME\$using:location\$using:filename.txt
}
Note: Since your background script block references variables from the caller's scope, they must be referenced via the $using: scope (as you've also done in your last Invoke-Command-based attempt). This requirement also applies to script blocks executed remotely, such as via Invoke-Command -ComputerName - see this answer for background information. The alternative is to pass arguments to the script block, via the -ArgumentList (-Args) parameter, though the $using: approach is usually simpler.
Start-Job returns a job-information object (System.Management.Automation.Job), which you can use to monitor the progress of and obtain output from the background job, using the various *-Job cmdlets, notably Wait-Job and Receive-Job - see the about_Jobs conceptual help topic.
Generally, using Invoke-Command for local code execution, while technically supported, is rarely necessary.
For direct, synchronous invocation (not as a job) of a command or script block, use &, the call operator (not needed for single commands, as long as the command name isn't quoted or specified via a variable), or, for execution directly in the caller's scope, ., the dot-sourcing operator (. { ... }).
You have a couple of options. Since you're only running this on your local machine, you can use Start-Job instead of Invoke-Command.
That being said, the problem that you're running into is 2-fold. First, if you're running the Invoke-Command cmdlet, you'll need to specify the ComputerName parameter. Even though it's an optional parameter, you'll need to use it to tell Powershell which parameter set you're using, otherwise it's going to get confused.
Secondly, you'll need to pass the arguments into the scriptblock. This is because Start-Job and Invoke-Command are part of PSRemoting and will actually look for environment variables on the specified computer instead of variables that you've declared in your script.
Here's what worked for me:
Invoke-Command -ComputerName $(hostname) -AsJob -JobName "TestJob" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename
The Invoke-Command option is powerful if you're wanting to get this information from other devices on your network.
And here's the Start-Job version:
Start-Job -Name "TestJob2" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename

Passing parameters to a remote scriptblock

Say i have 2 scripts. main-script.ps1 and base-script.ps1 and base-script being called from main-script.
#base-script.ps1
Param(
[string]$Site,
[string]$Env,
[string]$Package,
[bool]$New_Variable,
[string]$New_Variable2
)
Write-Host "i am in base script"
Write-Host " i am in server " $env:COMPUTERNAME
Write-Host $Site
Write-Host $Env
Write-Host $Package
Write-Host $New_Variable
Write-Host $New_Variable2
#main-script.ps1
Param(
[string]$SiteName,
[string]$Environment,
[string]$PackageName
)
Write-Host "I am in main-script"
$new_var = $true
$new_var2 = "Green"
$deployToBaseBlock = get-command '.\Base-script.ps1' | Select-Object -ExpandProperty ScriptBlock
Invoke-Command S1 -ScriptBlock $deployToBaseBlock -ArgumentList $SiteName,$Environment,$PackageName,$new_var,$new_var2
Write-Host "I am back in main. exiting"
Now as the parameters in base-script.ps1 grows, the arguments being passed in argumentlist is getting long and unmanageable. Is there a better way to do this. I wasnt able to get splatting work on this.
According to the first comment, we need to try something like this:
start-job -scriptblock { & 'c:\ps\bcpCopy.ps1' #args } -ArgumentList $ARGS
but in my case, scriptblock is defined outside.
$deployToBaseBlock = get-command '.\Base-script.ps1' | Select-Object -ExpandProperty ScriptBlock
Invoke-Command S1 -ScriptBlock {$deployToBaseBlock #args} -ArgumentList $arr
$arr is the array of arguments. This doesnt seem to work. Please suggest.
The remote endpoint will need to re-create and re-compile the scriptblock anyway, so you might as well just pass it the raw Base-Script.ps1 file.
Fortunately Invoke-Command already has a parameter for this exact use case:
Invoke-Command -FilePath '.\Base-script.ps1' -ComputerName S1 -ArgumentList $arr

How do you define variables locally then reference them in a remote machine?

Let's say I have 3 functions; A, B and C. Obviously this is super watered down but the logic is there.
A is a function that gathers a name.
function A {
Set-Variable -Scope Script -Name "First" -Value (Read-Host "First name?")
Set-Variable -Scope Script -Name "Last" -Value (Read-Host "Last name?")
}
Function B will use this name to create a O365 user with an msol session. No big deal.
function B {
Connect-MsolService -Credential $creds
"Add user foo"
$Name = "$Script:First $Script:Last"
"Success!"
}
While function C uses the name from A but references it within a PSSession to a remote AD server. This is where my knowledge totally breaks down. I've tried everything I know to reference these local variables within C.
function C {
New-ADUser
$Name = "$Script:First $Script:Last"
}
New-PSSession -computername AD -credential $ADcreds
Invoke-Command -ComputerName AD -ScriptBlock ${function:C} -credential $ADcreds
Remove-PSSession $s0
But they are totally wiped. I've tried to invoke a script block first to kind of define them again using the old data with no luck. My $Script:Name always comes up null in the PSSession.
Am I missing something huge?
The using: variable prefix should work here. If not, you should define a Param section within your function and pass the arguments using the -Argument parameter to the Invoke-Command cmdlet.
$scriptBlockC = {
New-ADUser
$Name = "$using:First $using:Last"
}
Invoke-Command -ComputerName AD -ScriptBlock $scriptBlockC -credential $ADcreds
Note that I removed the function keyword since you are just defining a scriptblock.

PowerShell Invoke-Command Returns Blank Data?

Been trying to solve this for a bit and can't seem to figure it out.
I have the following script:
$Servers = Get-Content -Path "C:\Utilities_PowerShell\ServerList.txt"
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus # Checks for status of IIS services
{
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3)
{
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
}
else
{
Write-Host " No IIS service(s) were found..." -foreground "red"
}
}
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock ${function:IISServiceStatus} -AsJob -ArgumentList $IISServiceName1, $IISServiceName2, $IISServiceName3, $IISarrService, $IISarrServiceCheck | Wait-Job | Receive-Job
Write-Host " "
}
Whenever I run it, all I get is the output of:
Status of IIS service(s) on *PC* :
If I run the function outside of a loop/invoke-command, the results are absolutely perfect. What is wrong with my remote loop?
I've tried putting the variables inside the function, I've tried running invoke-command without the argument list, etc.
Update: 3/17/16
Turns out...if I run my actual script as is, the result of $EndJobs is weird in that it outputs ALL services in one table and then the three IIS services in another table. This would explain why when I run my invoke-command (stopIIS) scriptblock...I had to reboot the whole server because it took all of the services down.
These functions run PERFECTLY when not run via remote/invoke-command.
What the heck...invoke-command is seriously screwing with my stuff!
Anyone have any ideas/tips on how I can run my local script (which works 100%) on a set of servers from a text file without weird issues like this? Is invoke-command the only way?
do you have the same problem if you wrap it all into the script block like this?
$Servers = Get-Content 'C:\Utilities_PowerShell\ServerList.txt'
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock {
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus { # Checks for status of IIS services
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3) {
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
} else {
Write-Host ' No IIS service(s) were found...' -ForegroundColor Red
}
}
IISServiceStatus $IISServiceName1 $IISServiceName2 $IISServiceName3 $IISarrService $IISarrServiceCheck
} -AsJob | Wait-Job | Receive-Job
Write-Host ' '
}
$EndJobs
I'm having a similar issue. I'm using credssp to test 2nd hop auth for an automation for shutting down a production environment cleanly. My script has 3 sections; session setup, the invoke, session teardown. If I run each piece separately, I get output. If I run the whole script, I get blank lines matching the amount of output I get when I run them separately... there's nothing fancy in my invoke (backtick line continuation - I prefer Python's formatting paradigm better than Powershell/C#):
Invoke-Command `
-Session $workingSession `
-ScriptBlock {
get-service *spool* -ComputerName server01
}

How to capture the Return Value of a ScriptBlock invoked with Powershell's Invoke-Command

My question is very similar to this one, except I'm trying to capture the return code of a ScriptBlock using Invoke-Command (so I can't use the -FilePath option). Here's my code:
Invoke-Command -computername $server {\\fileserver\script.cmd $args} -ArgumentList $args
exit $LASTEXITCODE
The problem is that Invoke-Command doesn't capture the return code of script.cmd, so I have no way of knowing if it failed or not. I need to be able to know if script.cmd failed.
I tried using a New-PSSession as well (which lets me see script.cmd's return code on the remote server) but I can't find any way to pass it back to my calling Powershell script to actually DO anything about the failure.
$remotesession = new-pssession -computername localhost
invoke-command -ScriptBlock { cmd /c exit 2} -Session $remotesession
$remotelastexitcode = invoke-command -ScriptBlock { $lastexitcode} -Session $remotesession
$remotelastexitcode # will return 2 in this example
Create a new session using new-pssession
Invoke your scripblock in this session
Fetch the lastexitcode from this session
$script = {
# Call exe and combine all output streams so nothing is missed
$output = ping badhostname *>&1
# Save lastexitcode right after call to exe completes
$exitCode = $LASTEXITCODE
# Return the output and the exitcode using a hashtable
New-Object -TypeName PSCustomObject -Property #{Host=$env:computername; Output=$output; ExitCode=$exitCode}
}
# Capture the results from the remote computers
$results = Invoke-Command -ComputerName host1, host2 -ScriptBlock $script
$results | select Host, Output, ExitCode | Format-List
Host : HOST1
Output : Ping request could not find host badhostname. Please check the name and try again
ExitCode : 1
Host : HOST2
Output : Ping request could not find host badhostname. Please check the name and try again.
ExitCode : 1
I have been using another method lately to solve this problem. The various outputs that come from the script running on the remote computer are an array.
$result = Invoke-Command -ComputerName SERVER01 -ScriptBlock {
ping BADHOSTNAME
$lastexitcode
}
exit $result | Select-Object -Last 1
The $result variable will contain an array of the ping output message and the $lastexitcode. If the exit code from the remote script is output last then it can be fetched from the complete result without parsing.
To get the rest of the output before the exit code it's just:
$result | Select-Object -First $(result.Count-1)
#jon Z's answer is good, but this is simpler:
$remotelastexitcode = invoke-command -computername localhost -ScriptBlock {
cmd /c exit 2; $lastexitcode}
Of course if your command produces output you'll have to suppress it or parse it to get the exit code, in which case #jon Z's answer may be better.
It is better to use return instead of exit.
For example:
$result = Invoke-Command -ComputerName SERVER01 -ScriptBlock {
return "SERVER01"
}
$result