Author Posts

June 19, 2017 at 8:16 pm

One can pass function definitions as parameters to a ScriptBlock using the method described in this post (https://powershell.org/forums/topic/pass-function-definitions-to-scriptblock/), but this method only saves space in the ScriptBlock; it doesn't prevent the functions from being defined twice.

I tried implementing actual lambda functions using this syntax:

$TestFunction = {
param($server)

write-host $server

}

$TestFunction.invoke($server)

This works for this simple example (the server name is returned), but implemented as follows in a larger script, the functions do not execute (i.e., running ScriptName.ps1 -ConfigFile Chef.config does not error out, but kicks back to the command prompt without doing anything).

Any ideas why the functions wouldn't execute in this script?

Any help is appreciated.


$BootStrap = {

	param($Server,$Username,$Password)

	$NodeList = @(knife node list)        

	if($NodeList -notcontains $server)

	{


		try {
			
			#knife bootstrap windows winrm $server --winrm-user $username --winrm-password $password --node-name $server --y
            Invoke-WebRequest https://devops-api.prd.costargroup.com/api/chef/bootstrap?server_name=$server
            Invoke-Command $server { chef-client }
			Write-Host `n"$server" -ForegroundColor Cyan -NoNewline; Write-Host " Bootstrap SUCCESS" -ForegroundColor Green
			[boolean]$BootstrapSuccess = "true"
			return [boolean]$BootstrapSuccess

		}

		catch {

			$ErrorMessage = $_.Exception.Message
			Write-Host "The error message was $ErrorMessage"
			[boolean]$BootstrapSuccess = "false"
			return [boolean]$BootstrapSuccess
			

		}      

	}

	else{ Write-Host "$Server already exists in Chef, processing next server..." -ForegroundColor Yellow }

}
$RunChefClient = {
param($server)

Invoke-Command $server {chef-client}

}
$TestFunction = {
param($server)

write-host $server

}
$SetEnvironment = {

	param($Server,$Environment)


	if(!([string]::IsNullOrWhiteSpace($Environment)))

	{

		try{

			knife node environment_set $server $environment 2> $null
			Write-Host `n"$server" -ForegroundColor Cyan -NoNewline; Write-Host " Environment set SUCCESS" -ForegroundColor Green

		}

		catch

		{

			$ErrorMessage = $_.Exception.Message
			Write-Host "The error message was $ErrorMessage"
			Throw "Environment set failed, stopping script..."
			Exit

		}

	}

	else {Write-Host "No valid environment to set, exiting..."; exit}

}
$SetTags = {

	param($Server,$Tags)

	foreach ($tag in $Tags) {



		try{

			knife tag create $Server $Tag 2> $null
			write-host $Server -ForegroundColor Cyan -NoNewline; write-host " $($Tag) "-ForegroundColor Yellow -NoNewline; write-host "Tag Applied Successfully" -ForegroundColor Green
			

		}

		catch{

			$ErrorMessage = $_.Exception.Message
			Write-Host "The error message was $ErrorMessage"
			Throw "Tag application failed, stopping script..."
			Exit
		}

	}

}
$SetRoles = {

	param($Server,$Roles)

	foreach ($role in $Roles) {   

		try{

			knife node run_list add $Server role[$($role)] 2> $null
			write-host $Server -ForegroundColor Cyan -NoNewline; write-host " $($role) "-ForegroundColor Yellow -NoNewline; write-host "Role Applied Successfully" -ForegroundColor Green			

		}

		catch{

			$ErrorMessage = $_.Exception.Message
			Write-Host "The error message was $ErrorMessage"
			Throw "Role application failed, stopping script..."
			Exit

		}

	}

}
$DisplayJobStatus = {

	$activity = 'Bootstrapping servers in $Servers and setting Chef environment...'

#Get and display job status
	$RunningJobs = get-job | ? { $_.state -eq "running" }

	$InitialRunningJobsCount = $RunningJobs.count

	$CurrentRunningJobsCount = $RunningJobs.count


	while($CurrentRunningJobsCount -gt 0) {

		$PercentComplete=[math]::Round((($InitialRunningJobsCount-$CurrentRunningJobsCount)/$InitialRunningJobsCount * 100),2)

		write-progress -activity "$activity" -status "Progress: $PercentComplete%" -percentcomplete (($InitialRunningJobsCount-$CurrentRunningJobsCount)/$InitialRunningJobsCount*100)

		$Jobs | Receive-Job

		$CurrentRunningJobsCount = (get-job | ? { $_.state -eq "running" }).Count
		

	}



}
	
#Create bootstrap job for each server, pass lambda functions to Scriptblock for execution.
	
if(($Username -ne $null) -and ($Password -ne $null))
{
ForEach($HashTable in $Config)


	{

		$Server = $HashTable.Server
		$Roles = $HashTable.Roles
		$Tags = $HashTable.Tags
		$Environment = $HashTable.Environment

	

		$ScriptBlock = {

			param ($Server,$Roles,$Tags,$Environment,$Username,$Password,$BootStrap,$RunChefClient,$SetEnvironment,$SetTags,$SetRoles)

            if(($BootStrap.invoke($Server,$Username,$Password)))

			{

				$SetEnvironment.invoke($Server,$Environment)

				$SetTags.invoke($Server,$Tags)

				$SetRoles.invoke($Server,$Roles)

				$RunChefClient.invoke($Server)

			}


			else {Write-Host "Bootstrap failed, processing next server..."; return}
			

		} 

		$Jobs += Start-Job -ScriptBlock $ScriptBlock -ArgumentList @($Server,$Roles,$Tags,$Environment,$Username,$Password,$BootStrap,$RunChefClient,$SetEnvironment,$SetTags,$SetRoles,$DisplayJobStatus)
	}


	#$DisplayJobStatus.invoke()
}

else {Write-Host "Username or password is missing, exiting..." -ForegroundColor Red; exit}

}

Main

June 20, 2017 at 3:37 pm

I think you've just aimed yourself in a coding direction that PowerShell's creators didn't really intent or anticipate. I'm personally not clear on why you wouldn't just define all of those things as functions and just call them as such. I'm also not clear on why Write-Host is in such use. My lack of clarity on these things makes me suspect I'm missing something fundamental about your question or environment, so I'm hesitant to weigh in... I'm also not clear on what doesn't work. Like, is there an error message? Does it simply do nothing?

I suspect that the problem is one of scope, though. If I'm following this correctly, your'e starting a job which runs $Scriptblock. However, $Scriptblock does not contain the other elements you've referred to, like $SetTags. The job is sorta-kinda a new process, and it's certainly a new scope; it doesn't "inherit" the scope of its caller. Everything it knows about the world is in $scriptblock, which doesn't contain the definitions for anything. I _do_ see that you're passing those in as arguments, but because this is kind of edge-of-the-reservation for PowerShell in terms of coding patterns, I can't be sure that what you're passing in has retained its integrity as an executable block (for example).

Don't get me wrong, I totally get how this is a common approach in some other languages. It's just not something you see much of in PowerShell.

June 21, 2017 at 6:25 am

the problem definitely somewhere in your code or environment
minimal example works ok
I do not like at all 'exit' everywhere in your code

1.ps1

param($testparam1, $testparam2)
$test1 = {param($test1) write-host "test1: $test1" }
$test2 = {param($test2) write-host "test2: $test2" }
$main = { param($sb1, $test1, $sb2, $test2 ) $sb1.Invoke($test1); $sb2.Invoke($test2) }
& $main $test1 'internaltestparam1' $test2 'internaltestparam2'
$main.Invoke($test1,$testparam1,$test2,$testparam2)
D:\>powershell -noprofile -file d:\1.ps1 p1 p2
test1: internaltestparam1
test2: internaltestparam2
test1: p1
test2: p2

ans sligtly modified example for remote execution.
modified because powershell send scriptblock to remote server as string

$main = { param($sb1, $test1, $sb2, $test2 ) [scriptblock]::create($sb1).Invoke($test1); [scriptblock]::create($sb2).Invoke($test2) }
Invoke-Command -ComputerName test-serv -ScriptBlock $main -ArgumentList $test1, 'pp1', $test2, 'pp2'
test1: pp1
test2: pp2