Unable to pass hashtable of parameters to another script using Invoke-Command - powershell

I've built a script for dynamically changing an HTML template and sending it as an email, it uses a hashtable for the items to be changed due to a fluctuating number of items it may need to change (seemed a better option than an array)
The emailing and replacing of variables etc is all working correctly, where I'm having issues is when I try to call it from another script and pass in a hashtable with the replacements and to/fromsubject, I'm using Splatting as it seemed the best option, I have tried just passing in the hashtable as is, and tried start-job and Invoke-Expression
powershell version: 5.1
This is the code I'm using to call the emailing script
$TemplateReplacements = #{}
$TemplateReplacements.Add("EmailTo","firstname.lastname#company.com.au")
$TemplateReplacements.Add("EmailFrom","ISnotifications#company.com.au")
$TemplateReplacements.Add("EmailSubject","Test Test Test")
$TemplateReplacements.Add("EmailTemplate","\ZeroLicenses.html")
$TemplateReplacements.Add("Heading","Heading replaced from main script")
$TemplateReplacements.Add("1","Variable1 repalced from main script")
$TemplateReplacements.Add("2","Variable2 replaced from main script")
$TemplateReplacements.Add("3","Variable3 replaced from main script")
$TemplateReplacements.Add("4","Variable4 replaced from main script")
$ScriptToRun = "C:\Users\User1\Desktop\Projects\Powershell\EmailVarReplace-Test\Test3.ps1"
$params = #{
Replacements = $TemplateReplacements
}
Invoke-Command $ScriptToRun #Params
#start-job $ScriptToRun #params
In the email script thats been called I have this at the top hich I believe is all I need for it to accept the parameter?
[CmdletBinding()]
param ($Replacements)
I've been stumped on this for ages and just keep getting the error
Invoke-Command : A parameter cannot be found that matches parameter name 'Replacements'.
At C:\Users\User1\Desktop\Projects\Powershell\EmailTest.ps1:19 char:29
+ Invoke-Command $ScriptToRun #Params
+ ~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Command], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.InvokeCommandCommand
What I'm expecting to happen is it passes the hashtable into the script so it can get the variables for where to send the email to, what address from, subject plus the replacements of variables in the email
I'm building the email script as a standalone module that later just needs templates created adn scripts calling it with the replacements made
Any help would be very appreciated
Also ignore the hard-coded path etc as it's still a testing script :p

Don't use Invoke-Command to invoke your script - Invoke-Command is virtually only ever useful in the context of remoting, i.e. if you use with the -ComputerName parameter to run a script of script block on one or more remote computers.
Instead, use &, the call operator, to invoke your script, then splatting should work as intended:
& $ScriptToRun #Params
As for what you tried:
If you try splatting via Invoke-Command, the splatting is applied to its parameters, not to the parameters of the script you're invoking.
Therefore, a -Replacements parameter is looked for among Invoke-Command's parameters rather than your script's - and there is no such parameter, which explains the error you got.

Related

Powershell - rerun the script with different credentials from itself

I have currently had to take a huge leap from my unix scripting to the MS side of things and found myself overwhelmed with PowerShell.
My situation is as follows:
I have a script script.ps1 which can be only run under specific windows account. In order to facilitate the use, it was decided that if user runs the script from a different account, it will pop up a query for credentials and restart itself from within (similarly to recursion), but importantly - maintaining the input parameters.
I have found out, that the Invoke-Command is probably what I am looking for, but I cannot seem to be able to build the PS query for this.
my code snippet looks like
if(!([System.Environment]::UserName -eq $user)){
$Credential = Get-Credential -credential INTRANET\$user
Invoke-Command -FilePath $script -Credential $Credential -ArgumentList $arguments
}
where $user contains the desired user, $script contains filepath to the script.ps1 and $arguments contain command line arguments that were passed to the script as a String, i. e. -order 66 -location UAT
but currently I get an error
Parameter set cannot be resolved using the specified named parameters.
...
FullyQualifiedErrorId : AmbiguousParameterSet
I tried shuffling the parameters around, I tried using Start-Process instead of Invoke-Command, but everything resulted in same or similar errors.
Also, because I am really new to the powershell, please do not hesitate to offer different solution, if it is viable. I do not know the capabilities of the language well.
Lastly, please note that the starting point is always powershell prompt running with non-elevated user account. Unfortunately, the option to start up powershell under a different account in the first place is not available to us.
The problem probably is that you specify the parameters stored in the variable $arguments as string in the regular format like you said: -order 66 -location UAT
The parameter -ArgumentList works differently, its an array used for array splatting. So you can't pass the values by the parameter name. You have to pass the values by parameter order, e.g.:
$Arguments = #(66,'uat')
Invoke-Command -FilePath $script -Credential $Credential -ArgumentList $Arguments
See Parameter Argumentlist.
See Array Splatting.
The value 66 is passed to the first parameter, the value uat to the 2nd... So you must know the order of the parameters and insert the related values into the array at the right position.
To control the position of the parameters, the param specification in the other script should at least have:
param (
[parameter(Position=1)]
[int]$order,
[parameter(Position=2)]
[string]$location
)

Resolving Variables within Scriptblocks for Jobs

I have a job setup to run/call a script that outputs a file. The code works fine when run on its own. I have verified the job is not being blocked and completes successfully. However, no file is generated when the script is called from the job. I'm even doing something very similar elsewhere without an issue, in fact it is from another similar set up that I pulled this code to begin with. Anyway, here is what I have:
When run as job, no file output:
$McShieldCheckerJob = Start-Job -ScriptBlock {
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile $McShieldCheckOutputFileName -GroupByMachine "N" -AsJob "Y"
} -Name McShieldCheckerJob
When run not as a job, file outputs as expected.
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile $McShieldCheckOutputFileName -GroupByMachine "N" -AsJob "Y"
Stumped as to why this won't work exactly the same as a job vs. not as a job. Again, I have verified via Get-Job and Receive-job that the job is not being blocked and is completing successfully.
EDIT:
It seems the variable $McShieldCheckOutputFileName cannot be resolved within the scriptblock for the job. If I use a string literal [for $McShieldCheckOutputFileName] I do not have the issue. (I was able to determine after a bit more troubleshooting that the lack of an output file when running as a job was due to a null value for my output file name.) So, I can probably work around this easily enough, but still this seems curious that a variable cannot be resolved within the scriptblock? Must be a scope thing...
Figured it out... here is how it is done:
$McShieldCheckerJob = Start-Job -ScriptBlock {
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile "$($args[0])" -GroupByMachine "N"
} -Name McShieldCheckerJob -ArgumentList $McShieldCheckOutputFileName
As we can see, we need to specify an -Argumentlist to the script block, then reference said arguments within the scriptblock via the $args variable.
Reference (see example 10)

The start-process cmdlet of powershell will fail with -PassThru argument when trying to launch Edge browser

In my test script by powershell, I'd start an Edge browser and get it's main process's ID. Simply like this:
$edge = Start-Process microsoft-edge: www.ted.com -PassThru
but instead of able to get the process id from $edge.Id, I've got error message like this:
Start-Process : This command cannot be run completely because the system cannot find all the information required.
At line:1 char:7
+ $edge=Start-Process microsoft-edge: www.ted.com -PassThru
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Start-Process], InvalidOperationException
+ FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.StartProcessCommand
Is there any simple and straightforward way to really doing this without enumerate and check the process list?
Thanks!
I try to test your script and I am getting a similar error as yours.
Based on my search result, passthru is not one of the common parameters, and it does not exist everywhere.
Reference:
Use the PowerShell Passthru Parameter and Get Back Objects
I think it is not available for the MS Edge browser and that's why the script gives an error.
I try to search for an alternative command for PassThru but I did not get any helpful information about it.
In this situation, it is better to enumerate and check the process list

How to start a new PowerShell session from another PS session, and run a CmdLet with splatted Parameters

The Goal:
Is to be able to test to see if PowerShell v6 is installed (which is OK), and if it is, then to invoke that shell for certain CmdLets. This will be invoked within a script running in PowerShell v5.1. I cannot shift fully to v6 as there are other dependencies that do not yet work in this environment, however, v6 offers significant optimisations on certain CmdLets that lead to an improvement in operation of over 200 times (specifically, an Invoke-WebRequest where the call will lead to a download of a large file - in v5.1 a 4GB file will take over 1 hour to download, in v6 this will take approximately 30 seconds using the same machines on the same subnet.
Additional Points:
However, I also build up a set of dynamic parameters that are used to splat into the CmdLets parameter list. For example, a built parameter list would look something like:
$SplatParms = #{
Method = "Get"
Uri = $resource
Credential = $Creds
Body = (ConvertTo-Json $data)
ContentType = "application/json"
}
And running the CmdLet normally would work as expected:
Invoke-RestMethod #SplatParms
What has been tried:
Over the past few days I have looked over various posts on this forum and elsewhere. We can create a simple script block the can be call and also works as expected:
$ConsoleCommand = { Invoke-RestMethod #SplatParms }
& $ConsoleCommand
However, attempting to pass the same thing in the Start-Process CmdLet fails, as I guess the parameter hash table is not being evaluated:
Start-Process pwsh -ArgumentList "-NoExit","-Command &{$ConsoleCommand}" -wait
Results in:
Invoke-RestMethod : Cannot validate argument on parameter 'Uri'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:1 char:22
+ &{ Invoke-RestMethod #SplatParms }
Where next?
I guess I somehow have to pass in parameters as arguments so they can then be evaluated and splatted, however, the syntax eludes me. I'm not even sure if Start-Process is the best CmdLet to use, but rather should I look to something else, like Invoke-Command, or something completely different?
It would be awesome to get the result of this CmdLet back into the originating shell, but at the moment, it will simply take something that functions.
Note: In principle, the techniques in this answer can be applied not only to calling from Windows PowerShell to PowerShell Core, but also in the opposite direction, as well as to between instances of the same PowerShell edition, both on Windows and Unix.
You don't need Start-Process; you can invoke pwsh directly, with a script block:
pwsh -c { $SplatParms = $Args[0]; Invoke-RestMethod #SplatParms } -args $SplatParms
Note the need to pass the hashtable as an argument rather than as part of the script block.
Unfortunately, as of Windows PowerShell 5.1 / PowerShell Core 6.0.0, there is a problem with passing [PSCredential] instances this way - see bottom for a workaround.
This will execute synchronously and even print the output from the script block in the console.
The caveat is that capturing such output - either by assigning to a variable or redirecting to a file - can fail if instances of types that aren't available in the calling session are returned.
As a suboptimal workaround, you can use -o Text (-OutputFormat Text) thanks, PetSerAl to capture the output as text, exactly as it would print to the console
(run pwsh -h to see all options).
Output is by default returned in serialized CLIXML format, and the calling PowerShell sessions deserializes that back into objects. If the type of a serialized object is not recognized, an error occurs.
A simple example (execute from Windows PowerShell):
# This FAILS, but you can add `-o text` to capture the output as text.
WinPS> $output = pwsh -c { $PSVersionTable.PSVersion } # !! FAILS
pwsh : Cannot process the XML from the 'Output' stream of 'C:\Program Files\PowerShell\6.0.0\pwsh.exe':
SemanticVersion XML tag is not recognized. Line 1, position 82.
...
This fails, because $PSVersionTable.PSVersion is of type [System.Management.Automation.SemanticVersion] in PowerShell Core, which is a type not available in Windows PowerShell as of v5.1 (in Windows PowerShell, the same property's type is [System.Version]).
Workaround for the inability to pass a [PSCredential] instance:
pwsh -c {
$SplatParms = $Args[0];
$SplatParams.Credential = [pscredential] $SplatParams.Credential;
Invoke-RestMethod #SplatParms
} -args $SplatParms
Calling another PowerShell instance from within PowerShell using a script block involves serialization and deserialization of objects in CLIXML format, as also used in PowerShell remoting.
Generally, there are many .NET types that deserialization cannot faithfully recreate and in such cases creates [PSCustomObject] instances that emulate instances of the original type, with the (normally hidden) .pstypenames property reflecting the original type name prefixed with Deserialized.
As of Windows PowerShell 5.1 / PowerShell Core 6.0.0, this also happens with instances of [pscredential] ([System.Management.Automation.PSCredential]), which prevents their direct use in the target session - see this GitHub issue.
Fortunately, however, simply casting the deserialized object back to [pscredential] seems to work.
Try creating a New-PSSession against 6.0 within your 5.1 session.
After installing powershell core 6.0 and running Enable-PSRemoting, a new PSSessionConfiguration was created for 6.0:
PS > Get-PSSessionConfiguration
Name : microsoft.powershell
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : microsoft.powershell.workflow
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : microsoft.powershell32
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : PowerShell.v6.0.0
PSVersion : 6.0
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
In your parent script, create new session using the 6.0 configuration name PowerShell.v6.0.0 and pass it to any subsequent Invoke-Command you require. Results are returned as objects. Scriptblocks may require local variables passed through -ArgumentList, per mklement0's answer.
$ps6sess = New-PSSession -ComputerName localhost -ConfigurationName 'PowerShell.v6.0.0'
$results = Invoke-Command -Session $ps60sess -ScriptBlock {Param($splatthis) Invoke-WebRequest #splatthis} -ArgumentList $SplatParms
It may also be useful to know that the session persists between Invoke-Command calls. For example, any new variables you create will be accessible in subsequent calls within that session:
PS > Invoke-Command -Session $ps60sess -ScriptBlock {$something = 'zers'}
PS > Invoke-Command -Session $ps60sess -ScriptBlock {write-host $something }
zers
Trouble with PSCredential passing doesn't seem to be a problem with this approach:
$ps6sess = New-PSSession -ComputerName localhost -ConfigurationName 'PowerShell.v6.0.0'
$credential = Get-Credential -UserName 'TestUser'
$IRestArgs = #{
Method='GET'
URI = 'https://httpbin.org'
Credential = $credential
}
$IRestBlock = {Param($splatval) Invoke-RestMethod #splatval}
Invoke-Command -Session $ps6sess -ScriptBlock $IRestBlock -ArgumentList $IRestArgs
# no error
pwsh -c {
Param ($SplatParms)
#$SplatParams.Credential = [pscredential] $SplatParams.Credential;
Invoke-RestMethod #SplatParms
} -args $IRestArgs
# error - pwsh : cannot process argument transformation on
# parameter 'Credential. username
Perhaps at the ps6 session knows it is receiving a block from ps5.1 and knows how to accommodate.
An immediate flash using start-process isn't proper, would have to research for that, regardless, my reaction is to construct an array of params to use not splat them for a try.

PowerShell error Method invocation failed

I have a strange problem where I have existing code that works on Windows 7x86 PowerShell 2.0 but will not work on Windows 10x86 using PowerShell 5.0.
The error states:
Method invocation failed because
[System.Management.Automation.PSRemotingJob] does not contain a method named 'op_Addition'.
At
C:\Build\AmazingCharts\Working\Building\Installer\WiseInstaller\Obfusated
Projects\ObfuscateFiles.ps1:211
char:13 + $Jarray +=
Start-Job -name ObfuscateFiles $ScriptBlock - ...
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation:
(op_Addition:String) [], Runti
meException +
FullyQualifiedErrorId : MethodNotFound
This is stating that the overloaded operator += doesn't seem to be defined but I find it very difficult to believe that the += overloaded addition operator would be deprecated in PowerShell 5.0 for arrays.
Further, the code seems to still execute and produce a correct output.
In my case I use a jobs array to execute Red Gate Smart Assembly to obfuscate build files. Below is a code snippet
$Jarray = #() #initialize jobs array
foreach ($file in $farray_) {
$params = $file,$cwd,$bldLog #parameters that are passed to script block
$nCount = (get-job -state "Running").Count #determines num of running jobs
if (-not $nCount) {$ncount=0} #initialize variable if null
if ($nCount -le $MAX_INSTANCES) {
#The line below is the one that generates the error
$Jarray += Start-Job -name ObfuscateFiles $ScriptBlock -ArgumentList $params
Start-Sleep -s 1 #gives the system time for process to start
} else {
$nCount = (get-job -state "Running").Count
if (-not $nCount) {$nCount=0}
while ($nCount -ge $MAX_INSTANCES) {
Start-Sleep -s 1 #gives time to recognize the job has started
$nCount = (get-job -state "Running").Count
if (-not $nCount) {$nCount=0}
}
$Jarray += Start-Job -name ObfuscateFiles $ScriptBlock -ArgumentList $params
}
}
When this execute it correctly generates all of the obfuscated files and in a timely manner so I know it is building in parallel up to the MAX_INSTANCES value. I even have code (not included above) that verifies that the number of expected obfuscated files is correct. The 'Method invocation failed' error reports back to CruiseControl.NET and my build fails.
Another frustration is that executing the same .ps1 file in PowerGUI completes successfully without any errors.
I have made sure that my array gets initialized. I have tried suggestions to specify that the array be of type [PSObject] but I get the same results.
Does anyone have any ideas what I can check for this?
Thank you.
EDIT:
I tested this in the PowerShell IDE
-- When tested running from a command prompt, I get the errors
-- When tested running the debugger, I do not get errors
I checked the $Jarray type and it is a System.Object[], which is what I would expect for an array.
I checked the $Jarray value during debug and the jobs are being created but the error still persists as if it doesn't understand the += overloaded Addition operator. My only guess is that this is a bogus error masking something else.
I added a try/catch block around the call to $Jarray. I had tried ErrorAction Ignore but that did not work because the call does not know what to do with the error. The try/catch block successfully masks the error; fortunately I have another method to determine if my call is successful. Needless to say this does not 'solve' the issue, it only masks the issue because right now it is not critical.
I will continue to update this area with things I have tested.
Just an idea, but might want to make sure your new machine has a relaxed policy for remoting AND enabling scripts:
Set-ExecutionPolicy Unrestricted &
Enable-PSRemoting