OctoPosh function works with literal but not with params (string+num) - powershell

I'm calling the cmdlet Get-OctopusEnvironment:
https://octoposh.readthedocs.io/en/latest/cmdlets/get-octopusenvironment/#parameters from OctoPosh. The call works if I enter a literal like ENV1. (This is a sample value, obviously you need to enter a value that exists as a environment on the Octopus server)
However, I'm using a script with params and the $env and $num need to be combined to identify the environment that I wish to target.
I tried:
Calling it in another session
Setting the $env and $num separately and then calling the method
Calling the function with the literal value - which always works.
Param(
[string]$env,
[string]$num
)
Import-Module Octoposh
Set-OctopusConnectionInfo (Call details hidden)
$result = Get-OctopusEnvironment -EnvironmentName $env+$num
$result
Output:
<pre>
Name : ENV1
Id : Environments-1
Machines : {some server}
Deployments : {some octopus projects}
Resource : Octopus.Client.Model.EnvironmentResource
</pre>
UPDATE: this seems to work fine for some reason. And it solves my problem... GetEnvironment basically just does the same as I wanted to. It combines the params $env+$num and returns them.
if($env -eq "DEV" -or $env -eq "INT")
{
Set-OctopusConnectionInfo
$envName = .\GetEnvironment.ps1 $env $num
$result = Get-OctopusEnvironment -EnvironmentName $envName
}

Related

Delay-bind script block does not work when function is exported from module

I have following function:
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject,
[Object] $ScriptBlock
)
process {
$value = Invoke-Command -ScriptBlock $ScriptBlock
Write-Host "Script: $value"
}
}
When I define this function directly in script and pipe input into it I get following result which is expected:
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }
# Outputs: "Script: Test"
But when I define this function inside module and export it with Export-ModuleMember -Function PipeScript then pipeline variable $_ inside script block is always null:
Import-Module PipeModule
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }
# Outputs: "Script: "
Full repro is available at: https://github.com/lpatalas/DelayBindScriptBlock
Can someone explain this behaviour?
Tip of the hat to PetSerAl for all his help.
Here's a simple solution, but note that it runs the script block directly in the caller's scope, i.e. it effectively "dot-sources", which allows modification of the caller's variables.
By contrast, your use of Invoke-Command runs the script block in a child scope of the caller's scope - if that is truly the intent, see the variant solution below.
"Dot-sourcing" the script block is also what standard cmdlets such as Where-Object and ForEach-Object do.
# Define the function in an (in-memory) module.
# An in-memory module is automatically imported.
$null = New-Module {
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject
,
[scriptblock] $ScriptBlock
)
process {
# Use ForEach-Object to create the automatic $_ variable
# in the script block's origin scope.
$value = ForEach-Object -Process $ScriptBlock -InputObject $InputObject
# Output the value
"Script: $value"
}
}
}
# Test the function:
$var = 42; #{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 43 - the script block ran in the caller's scope.
The above outputs string Script: Test and 43 afterwards, proving that the input object was seen as $_ and that dot-sourcing worked ($var was successfully incremented in the caller's scope).
Here's a variant, via the PowerShell SDK, that runs the script block in a child scope of the caller's scope.
This can be helpful if you don't want the execution of the script block to accidentally modify the caller's variables.
It is the same behavior you get with the engine-level delay-bind script-block and calculated-property features - though it's unclear whether that behavior was chosen intentionally.
$null = New-Module {
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject
,
[scriptblock] $ScriptBlock
)
process {
# Use ScriptBlock.InvokeContext() to inject a $_ variable
# into the child scope that the script block runs in:
# Creating a custom version of what is normally an *automatic* variable
# seems hacky, but the docs do state:
# "The list of variables may include the special variables
# $input, $_ and $this." - see https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext
$value = $ScriptBlock.InvokeWithContext(
$null, # extra functions to define (none here)
[psvariable]::new('_', $InputObject) # actual parameter type is List<PSVariable>
)
# Output the value
"Script: $value"
}
}
}
# Test the function:
$var = 42
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 42 - unaltered, because the script block ran in a child scope.
The above outputs string Script: Test, followed by 42, proving that the script block saw the input object as $_ and that variable $var - although seen in the script block, was not modified, due to running in a child scope.
The ScriptBlock.InvokeWithContext() method is documented here.
As for why your attempt didn't work:
Generally, script blocks are bound to the scope and scope domain in which they are created (except if they're created expressly as unbound script blocks, with [scriptblock]::Create('...')).
A scope outside of a module is part of the default scope domain. Every module has its own scope domain, and except for the global scope, which all scopes across all scope domains see, scopes in different scope domains do not see one another.
Your script block is created in the default scope domain, and when the module-defined function invokes it, the $_ is looked for in the scope of origin, i.e., in the (non-module) caller scope, where it isn't defined, because the automatic $_ variable is created by PowerShell on demand in the local scope, which is in the enclosing module's scope domain.
By using .InvokeWithContext(), the script block runs in a child scope of the caller's scope (as would be the case with .Invoke() and Invoke-Command by default), into which the above code injects a custom $_ variable so that the script block can reference it.
Providing better SDK support for these scenarios is being discussed in GitHub issue #3581.

VSTS Release - 'runtime' replacement of Environment Variable

I have made a release in vsts with some environment variables.
One of those environment variables is as follows:
#Array
[ { "name":"password", "value":"thisismypassword" }, { ... } ]
However, I get an output parameter from one of the release tasks which returns the password. So I thought to make a 'tag' and replace it when the output parameter has returned:
[ { "name":"password", "value":"<Password>" } ]
When my output parameter has returned I can create an powershell task to replace the 'tag' with the real password. However to replace, it should be either an string or an valid powershell array. If I directly use the environment variable, it breaks on the first ':' with an error message (because it is not a legit powershell command/format);
#This breaks
$var = $(environment_variable)
Hence I thought to convert it to a String, replace it, Convert it back to json object and set it back on the environment variable:
$Setting = ConvertFrom-Json -InputObject '$(environment_variable)'
$Setting = $Setting -replace "<Password>", "$(Output_Password)"
#Tried both below
$Setting_JSON - ConvertTo-Json -InputObject $Setting
$Setting_JNSON = [Newtonsoft.Json.JsonConvert]::SerializeObject($Setting, [Newtonsoft.Json.Formatting]::None)
Write-Host "##vso[task.setvariable variable=$(environment_variable)]$Setting_JSON"
However these produce a json string which is of a different format and the step which uses this variable does not understand;
#Output
["#{name=Password;value=thisisapasswordvalue}"]
#Expected (and needed) Output
[ { "name":"password", "value":"thisisapasswordvalue" } ]
#This breaks
$var = $(environment_variable)
For this, you can use this:
$var = $Env:variablename
This works at my side:
$Setting = $env:Var1
$Setting = $Setting -replace "<Password>", "NewValue"

PowerShell Runspaces, outputs and variables

I'm trying to understand Runspaces in PowerShell. I know about the PoshRSJob-Module, but I'd like to create my Runspace Jobs by myself.
This is my code, mostly taken out from this blog:
$Computer = "somename"
[runspacefactory]::CreateRunspacePool() > $null
$SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
1..2 | % {
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript({
param(
$Computer
)
$Computer
}) > $null
$PowerShell.AddParameter($Computer)
$Invoke = $PowerShell.BeginInvoke()
while (!($Invoke.IsCompleted)) {sleep -Milliseconds 2}
$Data = $PowerShell.EndInvoke($Invoke)
Write-Host $Data -f Red
}
Three question are in my mind:
Will I be able to return a Value in a Variable and use it for further use inside the script after finishing the Job?
Why is my $Data Variable empty?
In the Script I have so far, what is creating the following output? If I $null the invocation like this $Invoke = $PowerShell.BeginInvoke() > $null
, the script doesn't work properly anymore and still creates this output
Commands : System.Management.Automation.PSCommand
Streams : System.Management.Automation.PSDataStreams
InstanceId : 3b91cfda-028e-4cec-9b6d-55bded5d9d3c
InvocationStateInfo : System.Management.Automation.PSInvocationStateInfo
IsNested : False
HadErrors : False
Runspace :
RunspacePool : System.Management.Automation.Runspaces.RunspacePool
IsRunspaceOwner : False
HistoryString :
I don't understand your first question.
For the second question, I think it's because you're using $PowerShell.AddParameter($Computer).
Try $PowerShell.AddArgument($Computer) instead. AddArgument is for adding a value that gets implicitly (positionally) bound to a parameter. AddParameter is for adding a named parameter. The overload of AddParameter that takes just a string is for [Switch] parameters.
For your third question, I think it's $RunspacePool.Open() that's giving you that output.
When trying to determine these things, look for lines, especially with method calls, that have no left-hand assignment; so things you aren't assigning to a variable, as that's generally how these values get put into the output stream.

Safe way to convert string literal without using Invoke-Expression [duplicate]

Imagine the following code:
# Script Start
$WelcomeMessage = "Hello $UserName, today is $($Date.DayOfWeek)"
..
..
# 100 lines of other functions and what not...
..
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage
}
This is a very basic example, but what it tries to show is a script where there is a $WelcomeMessage that the person running the script can set at the top of the script and controls how/what the message displayed is.
First thing's first: why do something like this? Well, if you're passing your script around to multiple people, they might want different messages. Maybe they don't like $($Date.DayOfWeek) and want to get the full date. Maybe they don't want to show the username, whatever.
Second, why put it at the top of the script? Simplicity. If you have 1000 lines in your script and messages like these spread all over the script, it makes it a nightmare for people to find and change these messages. We already do that for static messages, in the form of localized strings and stuff, so this is nothing new, except for the variable parts in it.
So, now to the issue. If you run that code and invoke Greet-User (assuming the functions/cmdlets for retrieving username and date actually exist and return something proper...) Greet-User will always return Hello , today is.
This is because the string is expanded when you declare it, at the top of the script, when neither $UserName nor $Date objects have a value.
A potential workaround would be to create the strings with single quotes, and use Invoke-Expression to expand them. But because of the spaces, that gets a bit messy. I.e.:
$WelcomeMessage = 'Hello $env:USERNAME'
Invoke-Expression $WelcomeMessage
This throws an error because of the space, to get it to work properly it would have to be declared as such:
$WelcomeMessage = 'Hello $env:USERNAME'
$InvokeExpression = "`"$WelcomeMessage`""
Messy...
Also, there's another problem in the form of code injection. Since we're allowing the user to write their own welcome message with no bounds specified, what's to prevent them from putting in something like...
$WelcomeMessage 'Hello $([void] (Remove-Item C:\Windows -Force -Recurse))'
(Yes, I know this will not delete everything but it is an example)
Granted this is a script and if they can modify that string they can also modify everything else on the script, but whereas the example I gave was someone maliciously taking advantage of the nature of the script, it can also happen that someone accidentally puts something in the string that ends up having unwanted consequences.
So... there's got to be a better way without the use of Invoke-Expression, I just can't quite thing of one so help would be appreciated :)
Embedding variables into strings is not the only way to create dynamic text, the way I would do it is like this:
$WelcomeMessage = 'Hello {0}, today is {1}'
# 100 lines of other functions and what not...
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage -f $Username, $Date
}
The canonical way to delay evaluation of expressions/variables in strings is to define them as single-quoted strings and use $ExecutionContext.InvokeCommand.ExpandString() later on.
Demonstration:
PS C:\> $s = '$env:COMPUTERNAME'
PS C:\> $s
$env:COMPUTERNAME
PS C:\> $ExecutionContext.InvokeCommand.ExpandString($s)
FOO
Applied to your sample code:
$WelcomeMessage = 'Hello $UserName, today is $($Date.DayOfWeek)'
...
...
...
function Greet-User {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$ExecutionContext.InvokeCommand.ExpandString($WelcomeMessage)
}
Have you considered using a lambda expression; i.e. instead of defining the variable as a string value define it as a function, then invoke that function passing the relevant parameters at runtime.
$WelcomeMessage = {param($UserName,$Date);"Hello $UserName, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage.invoke($username,$date)
}
cls
Greet-User
Update
If you only wish to allow variable replacement the below code would do the trick; but this fails to do more advanced functions (e.g. .DayOfWeek)
$WelcomeMessage = 'Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))'
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$temp = $WelcomeMessage
get-variable | ?{#('$','?','^') -notcontains $_.Name} | sort name -Descending | %{
$temp = $temp -replace ("\`${0}" -f $_.name),$_.value
}
$temp
}
cls
Greet-User
Update
To avoid code injection this makes use of -whatif; that will only help where the injected code supports the whatif functionality, but hopefully better than nothing...
Also the code now doesn't require parameters to be declared; but just takes those variables which are available at the time of execution.
$WelcomeMessage = {"Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Greet-User {
[cmdletbinding(SupportsShouldProcess=$True)]
param()
begin {$original = $WhatIfPreference; $WhatIfPreference = $true;}
process {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
& $WelcomeMessage
}
end {$WhatIfPreference = $original;}
}
cls
Greet-User

PowerShell DSC - how to pass configuration parameters to ScriptResources?

I'm having a lot of trouble trying to get a PowerShell Desired State Configuration script working to configure an in-house application. The root of the problem is that I can't seem to pass my configuration data down into a ScriptResource (at least not with the way I'm trying to do it).
My script is supposed to create a config folder for our in-house application, and then write some settings into a file:
configuration MyApp {
param (
[string[]] $ComputerName = $env:ComputerName
)
node $ComputerName {
File ConfigurationFolder {
Type = "Directory"
DestinationPath = $Node.ConfigFolder
Ensure = "Present"
}
Script ConfigurationFile {
SetScript = {
write-verbose "running ConfigurationFile.SetScript";
write-verbose "folder = $($Node.ConfigFolder)";
write-verbose "filename = $($Node.ConfigFile)";
[System.IO.File]::WriteAllText($Node.ConfigFile, "enabled=" + $Node.Enabled);
}
TestScript = {
write-verbose "running ConfigurationFile.TestScript";
write-verbose "folder = $($Node.ConfigFolder)";
write-verbose "filename = $($Node.ConfigFile)";
return (Test-Path $Node.ConfigFile);
}
GetScript = { #{Configured = (Test-Path $Node.ConfigFile)} }
DependsOn = "[File]ConfigurationFolder"
}
}
}
For reference, my configuration data looks like this:
$config = #{
AllNodes = #(
#{
NodeName = "*"
ConfigFolder = "C:\myapp\config"
ConfigFile = "C:\myapp\config\config.txt"
}
#{
NodeName = "ServerA"
Enabled = "true"
}
#{
NodeName = "ServerB"
Enabled = "false"
}
)
}
And I'm applying DSC with the following:
$mof = MyApp -ConfigurationData $config;
Start-DscConfiguration MyApp –Wait –Verbose;
When I apply this configuration it happily creates the folder, but fails to do anything with the config file. Looking at the output below, it's obvious that it's because the $Node variable is null inside the scope of ConfigurationFile / TestScript, but I've got no idea how to reference it from within that block.
LCM: [ Start Resource ] [[Script]ConfigurationFile]
LCM: [ Start Test ] [[Script]ConfigurationFile]
[[Script]ConfigurationFile] running ConfigurationFile.TestScript
[[Script]ConfigurationFile] node is null = True
[[Script]ConfigurationFile] folder =
[[Script]ConfigurationFile] filename =
LCM: [ End Test ] [[Script]ConfigurationFile] in 0.4850 seconds.
I've burnt off an entire day searching online for this specific problem, but all the examples of variables, parameters and configuration data all use File and Registry resources or other non-script resources, which I've already got working in the "ConfigurationFolder" block in my script. The thing I'm stuck on is how to reference the configuration data from within a Script resource like my "ConfigurationFile".
I've drawn a complete blank so any help would be greatly appreciated. If all else fails I may have to create a separate "configuration" script per server and hard-code the values, which I really don't want to do if at all possible.
Cheers,
Mike
Change this: $Node.ConfigFolder to $using:Node.ConfigFolder.
If you have a variable called $Foo and you want it to be passed to a script DSC resource, then use $using:Foo
Based on David's answer, I've written a utility function which converts my script block to a string and then performs a very naive search and replace to expand out references to the configuration data as follows.
function Format-DscScriptBlock()
{
param(
[parameter(Mandatory=$true)]
[System.Collections.Hashtable] $node,
[parameter(Mandatory=$true)]
[System.Management.Automation.ScriptBlock] $scriptBlock
)
$result = $scriptBlock.ToString();
foreach( $key in $node.Keys )
{
$result = $result.Replace("`$Node.$key", $node[$key]);
}
return $result;
}
My SetScript then becomes:
SetScript = Format-DscScriptBlock -Node $Node -ScriptBlock {
write-verbose "running ConfigurationFile.SetScript";
write-verbose "folder = $Node.ConfigFolder";
write-verbose "filename = $Node.ConfigFile)";
[System.IO.File]::WriteAllText("$Node.ConfigFile", "enabled=" + $Node.Enabled);
}
You have to be mindful of quotes and escapes in your configuration data because Format-DscScriptBlock only performs literal substitution, but this was good enough for my purposes.
A quite elegant way to solve this problem is to work with the regular {0} placeholders. By applying the -f operator the placeholders can be replaced with their actual values.
The only downside with this method is that you cannot use the curly braces { } for anything other than placeholders (i.e. say a hashtable or a for-loop), because the -f operator requires the braces to contain an integer.
Your code then looks like this:
SetScript = ({
Set-ItemProperty "IIS:\AppPools\{0}" "managedRuntimeVersion" "v4.0"
Set-ItemProperty "IIS:\AppPools\{0}" "managedPipelineMode" 1 # 0 = Integrated, 1 = Classic
} -f #($ApplicationPoolName))
Also, a good way to find out if you're doing it right is by simply viewing the generated .mof file with a text editor; if you look at the generated TestScript / GetScript / SetScript members, you'll see that the code fragment really is a string. The $placeholder values should already have been replaced there.
ConfigurationData only exists at the time the MOF files are compiled, not at runtime when the DSC engine applies your scripts. The SetScript, GetScript, and TestScript attributes of the Script resource are actually strings, not script blocks.
It's possible to generate those script strings (with all of the required data from your ConfigurationData already expanded), but you have to be careful to use escapes, subexpressions and quotation marks correctly.
I posted a brief example of this over on the original TechNet thread at http://social.technet.microsoft.com/Forums/en-US/2eb97d67-f1fb-4857-8840-de9c4cb9cae0/dsc-configuration-data-for-script-resources?forum=winserverpowershell