Runspaces and Jobs in VSTS hosted agent release Azure Powershell script - powershell

I have a VSTS setup with hosted agents and I'm trying to run an Azure Powershell script to resize classic cloud services using a Runspaces async method. This script works fine when I run it on my local machine.
[hashtable]$myServices = #{}
$myServices.Add('serviceA',3)
$myServices.Add('serviceB',1)
$myServices.Add('serviceC',2)
$myServices.Add('serviceD',1)
$myServices.Add('serviceE',3)
$Throttle = 5 #threads
$ScriptBlock = {
Param (
[string]$serviceName,
[int]$instanceCount
)
Try{
$roles = Get-AzureRole -ServiceName $serviceName -Slot Production -ErrorAction Stop
foreach($role in $roles){
Write-Output 'Currently on ' + $role.RoleName
$result = Set-AzureRole -ServiceName $serviceName -Slot Production -RoleName $role.RoleName -Count $instanceCount -ErrorAction Stop
$thisRunResult = New-Object PSObject -Property #{
Service = $serviceName
Role = $role.RoleName
Description = $result.OperationDescription
Status = $result.OperationStatus
}
$RunResult += $thisRunResult
}
Return $RunResult
} Catch {
write-output "An error occurred in the script block."
write-output $_.Exception.Message
}
}
Try{
$ErrorActionPreference = "Stop"
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $Throttle)
$RunspacePool.Open()
$Jobs = #()
} Catch {
write-output "An error occurred while instantiating Runspaces."
write-output $_.Exception.Message
} Finally {
$ErrorActionPreference = "Continue"
}
foreach ($service in $qaServices.GetEnumerator()){
Write-Output 'Currently on ' + $service.Key
Try{
$ErrorActionPreference = "Stop"
$Job = [powershell]::Create().AddScript($ScriptBlock).AddArgument($service.Key).AddArgument($service.Value)
write-output "Line after the Job is created gets executed."
$Job.RunspacePool = $RunspacePool
$Jobs += New-Object PSObject -Property #{
RunNum = $service.Key
Pipe = $Job
Result = $Job.BeginInvoke()
}
} Catch {
write-output "An error occurred creating job."
write-output $_.Exception.Message
} Finally {
$ErrorActionPreference = "Continue"
}
}
Write-Host "Waiting.." -NoNewline
Do {
Write-Host "." -NoNewline
Start-Sleep -Seconds 1
} While ( $Jobs.Result.IsCompleted -contains $false)
Write-Host "All jobs completed!"
$Results = #()
ForEach ($Job in $Jobs)
{ $Results += $Job.Pipe.EndInvoke($Job.Result)
}
$Results | Select-Object -Property Service,Role,Description,Status | Sort-Object -Property Service | Out-Host
None of my Try/Catch stuff outputs anything in the log. When I run this locally, I get the resulting output like I expect with a new "." every second until it's complete:
Waiting.........................All jobs completed!
Locally it's a few hundred ...'s because it takes a few minutes to scale things. When run in VSTS, it's about eight dots returned immediately. So it looks like nothing is actually happening (specifically that the Jobs are not initializing), but there's no error to tell me what the problem is. Thanks in advance.

(untested)
try this:
try {
$ErrorActionPreference = "Stop"
# Your code
} catch{
Write-Host $_.Exception
}finally{
$ErrorActionPreference = "Continue"
}
by default the $ErrorActionPreference is set to Continue so by setting it to Stop you are saying that all error are "terminating" thus the exception is catched by try/catch block.
or second option is to use common parameter -ErrorAction Stop
try {
$result = Set-AzureRole -ServiceName $serviceName -Slot Production -RoleName $role.RoleName -Count $instanceCount -ErrorAction Stop
} catch{
Write-Host $_.Exception
}
I had the same issue with AD(active directory) commandlets.

Related

Powershell Error Handling not working in Invoke-command block

I want to print error message when service is not found or machine is offline
$csv = Import-Csv "C:\Users\user\Desktop\computers.csv" -delimiter ","
foreach ($cs in $csv){
$computers = $cs.DNSHostname
foreach ($computer in $computers){
Try{
Invoke-Command -ComputerName $computer -ScriptBlock {
$ProcessCheck = Get-Process -Name agentid-service -ErrorAction Stop
if ($null -eq $ProcessCheck) {
Write-output "agentid-service IS not running $env:computername"
}
else {
Write-output "agentid-service IS running $env:computername"
}
}
}
catch{
Write-Warning $Error[0]
}
}
}
Instead, when service name is not found, i'm getting error:
Cannot find a process with the name "agentid-service". Verify the process name and call the cmdlet again.
And if connection to computer is not possible then getting:
[vm.domain.local] Connecting to remote server vm.domain.local failed with the following error message : The WinRM client cannot process the request because the server name cannot be resolved.
And script continue execution (as it should).
How can i get "Catch" block to be executed ?
You can do this in one foreach loop.
Try
$computers = (Import-Csv "C:\Users\user\Desktop\computers.csv").DNSHostname
foreach ($computer in $computers) {
if (!(Test-Connection -ComputerName $computer -Count 1 -Quiet)) {
Write-Warning "Computer $computer cannot be reached"
continue # skip this one and proceed with the next computer
}
try{
Invoke-Command -ComputerName $computer -ScriptBlock {
$ProcessCheck = Get-Process -Name 'agentid-service' -ErrorAction Stop
if (!$ProcessCheck.Responding) {
"agentid-service is NOT not running on computer '$env:computername'"
}
else {
"agentid-service is running on computer '$env:computername'"
}
}
}
catch{
Write-Warning $_.Exception.Message
}
}
Thanks to #Theo's answer, managed to fix it:
$csv = Import-Csv "C:\Users\user\Desktop\computers.csv" -delimiter ","
foreach ($cs in $csv){
$error.Clear()
$computers = $cs.DNSHostname
foreach ($computer in $computers){
Try{
Invoke-Command -ComputerName $computer -ScriptBlock {
$ProcessCheck = Get-Process -Name agentid-service -ErrorAction Stop
if ($null -ne $ProcessCheck) {
Write-output "agentid-service IS running $env:computername"
}
} -ErrorAction Stop
}
catch{
if ($null -eq $ProcessCheck){
Write-warning "agentid-service IS not running $env:computername"
}
Write-Warning $Error[0]
}
}
}

Script won't read the service line by line from txt file, it combline them instead

This is the script I have, I'm trying to have it read a list of services from the text but it instead combines them together. For example, I have two services(RTCRGS and TabletInputService), it will read it as
" RTCRGS TabletInputService " instead of one service at a time.
$Serverstxt = "C:\Scripts\Services\Server.txt"
$Servicestxt = "C:\Scripts\Services\Services.txt"
$ServiceList = get-content "$Servicestxt"
$ServerList = get-content "$Serverstxt"
#Initialize variables:
[string]$WaitForIt = ""
[string]$Verb = ""
[string]$Result = "FAILED"
#
foreach($Server in $ServerList){
foreach($Service in $ServiceList){
$svc = (get-service -computername $Server -name $Service)
Write-host "$Service on $Sever is $($svc.status)"
Switch ($svc.status) {
'Stopped' {
Write-host "Starting $Service..."
$Verb = "start"
$WaitForIt = 'Running'
$svc.Start()}
'Running' {
Write-host "Stopping $Service..."
$Verb = "stop"
$WaitForIt = 'Stopped'
$svc.Stop()}
Default {
Write-host "$Service is $($svc.status). Taking no action."}
}
if ($WaitForIt -ne "") {
Try {
# For some reason, we cannot use -ErrorAction after the next statement:
$svc.WaitForStatus($WaitForIt,'00:02:00')
} Catch {
Write-host "After waiting for 2 minutes, $Service failed to $Verb."
}
$svc = (get-service -computername $Server -name $Service)
if ($svc.status -eq $WaitForIt) {$Result = 'SUCCESS'}
Write-host "$Result`: $Service on $Server is $($svc.status)"
}
The foreach is correct, but you are using the entire list, instead of each element throughout your loop.
$SvrNames and $SvcNames are the contents of the files.
$SvrName and $SvcName are the current loop variables.
So you'd use
$svc = (get-service -computername $SvrName -name $SvcName)
and so on, inside the loop.
To make things a bit clearer, rename the variables
$Serverstxt = "C:\Scripts\Services\Server.txt"
$Servicestxt = "C:\Scripts\Services\Services.txt"
$ServiceList = get-content "$Servicestxt"
$ServerList = get-content "$Serverstxt"
foreach($Server in $ServerList){
foreach($Service in $ServiceList){
# . . .
$svc = (get-service -computername $Server -name $Service)
# . . .
}
}

Split-Pipeline parallel jobs script won't finish the last job

So I've been using Split-Pipeline module for some time now, and what bothers me about it, rarely does it really finish. I always get stuck with the last job in the queue hanging and have to either stop the script or kill ISE:
VERBOSE: Split-Pipeline: Jobs = 1; Load = End; Queue = 0
example: 300 servers, running a scan for hotfixes on them using split-pipeline. I'm using pretty standard parameters:
$servers | Split-Pipeline -Verbose -Count 10 { process {#some code for scanning#}}
So 10 jobs each loaded with 30 servers, first lets say 250 servers are scanned really fast, then it slows down a little and when the last job remains only, it never finishes...
Anyone experienced something similar? I've tested the same on several machines and it's always the same so I don't think it's related to the machine running the script.
EDIT: heres the code
$servers = (Get-Clipboard).trim() #server1..100
$KB = (Get-Clipboard).trim() -split ', ' #KB4022714, KB4022717, KB4022718, KB4022722, KB4022727, KB4022883, KB4022884, KB4022887, KB4024402, KB4025339, KB4025342, KB4021903, KB4021923, KB4022008, KB4022010, KB4022013, KB3217845
$servers | Split-Pipeline -Verbose -Variable KB -Count 10 { process {
$hash = [ordered]#{}
try
{
$hash.Hostname = $_
$os = gwmi win32_operatingsystem -ComputerName $_ -ErrorAction Stop
$hash.OS = $os.Caption
$hash.Architecture = $os.OSArchitecture
$today = [Management.ManagementDateTimeConverter]::ToDateTime($os.LocalDateTime)
$hash.LastReboot = [Management.ManagementDateTimeConverter]::ToDateTime($os.LastBootUpTime)
$hash.DaysSinceReboot = ($today - $hash.LastReboot).Days
}
catch
{
$hash.OS = $Error[0]
$hash.Architecture = 'N/A'
$hash.LastReboot = 'N/A'
$hash.DaysSinceReboot = 'N/A'
}
try
{
$hash.PendingReboot = icm -cn $_ {
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA SilentlyContinue) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA SilentlyContinue) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -Ea SilentlyContinue) { return $true }
try
{
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending)
{
return $true
}
}
catch{}
return $false
}
}
catch
{
$hash.PendingReboot = 'N/A'
}
try
{
$hotfix = Get-HotFix -ComputerName $_ -Id $KB -ErrorAction Stop
if ($hotfix)
{
$hash.Hotfix = $hotfix.HotFixID -join ','
}
else
{
$hash.Hotfix = "No Hotfix from the list applied"
}
}
catch
{
$hash.Hotfix = $Error[0]
}
$obj = New-Object -TypeName PSObject -Property $hash
$obj | Export-Csv c:\temp\hotfixes.csv -NoTypeInformation -Append -Force
}
}

Getting output from background jobs

I created the following script to reset the password of the local admin account on all machines in a specific host file. This script functions properly and provides a useful output, but it is slow as it only does one machine at a time.
# function to convert a secure string to a standard string
function ConvertTo-String {
param(
[System.Security.SecureString] $secureString
)
$marshal = [System.Runtime.InteropServices.Marshal]
try {
$intPtr = $marshal::SecureStringToBSTR($secureString)
$string = $marshal::PtrToStringAuto($intPtr)
}
finally {
if($intPtr) {
$marshal::ZeroFreeBSTR($intPtr)
}
}
$string
}
$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"
# prompt for password and confirm
do {
$ss1 = Read-Host "Enter new password" -AsSecureString
$ss2 = Read-Host "Enter again to confirm" -AsSecureString
# compare strings - proceed if same - prompt again if different
$ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
Write-Host "Passwords match"
if(-not $ok) {
Write-Host "Passwords do not match"
}
}
until($ok)
# set password variable to string value
$adminPassword = ConvertTo-String $ss1
# setup job to reset password on each client
foreach($client in $clients) {
$status = "OFFLINE"
$isOnline = "OFFLINE"
if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
$isOnline = "ONLINE"
}
# change the password
try {
$localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
$localAdminAccount.SetPassword($adminPassword)
$localAdminAccount.SetInfo()
Write-Verbose "Password change completed successfully"
}
catch {
$status = "FAILED"
Write-Verbose "Failed to change password"
}
# create psobject with system info
$obj = New-Object -TypeName PSObject -Property #{
ComputerName = $client
Online = $isOnline
ChangeStatus = $status
}
$obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append
if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
$stream.writeline("$client -t $status")
}
}
$adminPassword = " "
Write-Verbose "Complete"
Invoke-Item C:\test.txt
To make the script run faster, I set it up to use background jobs so it could run on multiple clients at once. However, now I get no output in my text files. The new script is below. Lines 43 and 79-89 are the changes. What changes do I need to make to this script provide the output it does in the first version? I've tried a number of other ways to get the output than what I currently have on line 89.
# function to convert a secure string to a standard string
function ConvertTo-String {
param(
[System.Security.SecureString] $secureString
)
$marshal = [System.Runtime.InteropServices.Marshal]
try {
$intPtr = $marshal::SecureStringToBSTR($secureString)
$string = $marshal::PtrToStringAuto($intPtr)
}
finally {
if($intPtr) {
$marshal::ZeroFreeBSTR($intPtr)
}
}
$string
}
$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"
# prompt for password and confirm
do {
$ss1 = Read-Host "Enter new password" -AsSecureString
$ss2 = Read-Host "Enter again to confirm" -AsSecureString
# compare strings - proceed if same - prompt again if different
$ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
Write-Host "Passwords match"
if(-not $ok) {
Write-Host "Passwords do not match"
}
}
until($ok)
# set password variable to string value
$adminPassword = ConvertTo-String $ss1
# setup job to reset password on each client
#-----------------------------------------------------------------------------------------
$scriptBlock = {
#-----------------------------------------------------------------------------------------
foreach($client in $clients) {
$status = "OFFLINE"
$isOnline = "OFFLINE"
if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
$isOnline = "ONLINE"
}
# change the password
try {
$localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
$localAdminAccount.SetPassword($adminPassword)
$localAdminAccount.SetInfo()
Write-Verbose "Password change completed successfully"
}
catch {
$status = "FAILED"
Write-Verbose "Failed to change password"
}
# create psobject with system info
$obj = New-Object -TypeName PSObject -Property #{
ComputerName = $client
Online = $isOnline
ChangeStatus = $status
}
$obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append
if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
$stream.writeline("$client -t $status")
}
}
}
#-----------------------------------------------------------------------------------------
Get-Job | Remove-Job -Force
Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset
Get-Job
While(Get-Job -State "Running") {
Start-Sleep -m 10
}
Receive-Job -name AdminPWReset | Out-File C:\test2.txt
#-----------------------------------------------------------------------------------------
$adminPassword = " "
Write-Verbose "Complete"
Invoke-Item C:\test.txt
Invoke-Item C:\test2.txt
You don't receive output, because you moved your entire loop into the scriptblock:
$scriptBlock = {
foreach ($client in $clients) {
...
}
}
but initialized your client list ($clients) outside the scriptblock, so that the variable $clients inside the scriptblock is a different (empty) variable since it's in a different scope. Because of that your job is iterating over an empty list, and is thus not producing any output.
To be able to use the client list inside the scriptblock you'd have to use the using: scope modifier:
$scriptBlock = {
foreach ($client in $using:clients) {
...
}
}
or pass the client list into the scriptblock as an argument:
$scriptBlock = {
foreach ($client in $args[0]) {
...
}
}
Start-Job -ScriptBlock $scriptBlock -ArgumentList $clients
As I can see from your code you're trying to do the latter:
Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset
However, that doesn't work for two reasons:
In the context where you run Start-Job there is no current object, so $_ is empty and nothing is passed into the scriptblock.
The scriptblock neither has a Param() block, nor does it use the automatic variable $args, so even if you passed an argument into the scriptblock it would never be used.
With that said, you don't want to pass $clients in the first place, because even if it worked, it wouldn't speed up anything as the entire client list would still be processed sequentially. What you actually want to do is to process the list in parallel. For that you must put just the tests into the scriptblock and then start one job for each client in a loop:
$scriptBlock = {
Param($client)
$status = "OFFLINE"
$isOnline = "OFFLINE"
if (Test-Connection -Computer $client -Quiet -Count 1 -Delay 1) {
$isOnline = "ONLINE"
}
...
}
foreach ($client in $clients) {
Start-Job -Name AdminPWReset -ScriptBlock $scriptBlock -ArgumentList $client
}
while (Get-Job -State "Running") {
Start-Sleep -Milliseconds 100
}
Receive-Job -Name AdminPWReset | Out-File C:\test2.txt
Remove-Job -Name AdminPWReset
Note that if you have a great number of target hosts you may want to use a job queue, so that you don't have hundreds of jobs running in parallel.

Error Handling Issue with Try/Catch in Powershell

I have been working on the following example from one of Don Jones' powershell books as part of my personal development and am having some serious trouble getting the try/catch construct to work as it should. As you can see, when the catch block executes, it sets a variable called $everything_ok to $false - which should trigger the else block in the following code. Which it does - the logfile is appended as per my expectations.
However it does not stop the script from ALSO executing the code in the if block and spewing out 'The RPC Server is unavailable' errors when it tries to query the made-up machine 'NOTONLINE' (Exception type is System.Runtime.InteropServices.COMException).
What makes this even stranger is that I went through the script with breakpoints, checking the contents of the $everything_ok variable along the way, and it never contained the wrong value at any point. So why on earth is the if block still executing for 'NOTONLINE' when the condition I have specified ( if ($everything_ok = $true) ) has not been met?
Am I doing something wrong here?
function get-systeminfo {
<#
.SYNOPSIS
Retrieves Key Information on 1-10 Computers
#>
[cmdletbinding()]
param (
[parameter(mandatory=$true,valuefrompipeline=$true,valuefrompipelinebypropertyname=$true,helpmessage="computer name or ip address")]
[validatecount(1,10)]
[validatenotnullorempty()]
[alias('hostname')]
[string[]]$computername,
[string]$errorlog = "C:\retry.txt",
[switch]$logerrors
)
BEGIN {
write-verbose "Error log will be $errorlog"
}
PROCESS {
foreach ($computer in $computername) {
try {$everything_ok = $true
gwmi win32_operatingsystem -computername $computer -ea stop
} catch {
$everything_ok = $false
write-verbose "$computer not Contactable"
}
if ($everything_ok = $true) {
write-verbose "Querying $computer"
$os = gwmi win32_operatingsystem -computername $computer
$cs = gwmi win32_computersystem -computername $computer
$bios = gwmi win32_bios -computername $computer
$props = #{'ComputerName' = $cs.__SERVER;
'OSVersion' = $os.version;
'SPVersion' = $os.servicepackmajorversion;
'BiosSerial' = $bios.serialnumber;
'Manufacturer' = $cs.manufacturer;
'Model' = $cs.model}
write-verbose "WMI Queries Complete"
$obj = new-object -type psobject -property $props
write-output $obj
}
elseif ($everything_ok = $false) {
if ($logerrors) {
"$computer $_" | out-file $errorlog -append
}
}
}
}
END {}
}
get-systeminfo -host localhost, NOTONLINE -verbose -logerrors
The equals sign in Powershell is used as the assignment operation. -eq is used to test for equality. So your if statement is assigning $true to $everything_ok, which then tests true.