Looking to speed up this PowerShell Function - loops

I'm running the following code to pull data from SCOM 2012 and using an exported spreadsheet from SCCM 2012, output servers which are pending reboot along with their SCCM maintenance window for automated scheduled reboots.
The code takes around 5-8 minutes to run and I was wondering if there was any way to speed up the process. The code running under Begin Loop is the bottle neck.
Function Generate-RebootData{
IF(Get-Command Get-SCOMAlert -ErrorAction SilentlyContinue){}ELSE{Import-Module OperationsManager}
"Get Pend reboot servers from prod"
New-SCOMManagementGroupConnection -ComputerName ProdSrv
$AlertData = get-SCOMAlert -Criteria `
"Severity = 1 AND ResolutionState < 254 AND Name = 'Pending Reboot'" |
Select NetbiosComputerName
"Get Pend reboot servers from cert"
#For cert information
New-SCOMManagementGroupConnection -ComputerName CertSrv
$AlertData += Get-SCOMAlert -Criteria `
"Severity = 1 AND ResolutionState < 254 AND Name = 'Pending Reboot'" |
Select NetbiosComputerName
"Remove duplicates"
$AlertDataNoDupe = $AlertData | Sort NetbiosComputerName -Unique
"Create hash table"
$table = #{}
"Populate hash table"
Import-Csv D:\Scripts\servers2.csv | ForEach-Object {
$table[$_.Computername] = $_.'Collection Name'}
"Create final object"
$result = #{}
"Begin Loop"
$result = $AlertDataNoDupe | ForEach-Object { [PSCustomObject] #{
Server=$_.NetbiosComputerName
MaintenanceWindow=IF($table[$_.NetbiosComputerName]){$table[$_.NetbiosComputerName]}
ELSE{"Not found!"}
PingCheck=IF(Test-Connection -Count 1 $_.NetbiosComputerName -Quiet -EA SilentlyContinue)
{"Alive"}
ELSE{"Dead"}
LastReboot=Try{
$operatingSystem = Get-WmiObject Win32_OperatingSystem -ComputerName `
$_.NetbiosComputerName -ErrorAction Stop
[Management.ManagementDateTimeConverter]::ToDateTime(`
$operatingSystem.LastBootUpTime)}
Catch{"Access Denied!"}
} }
}

You should perform the PingCheck first, and only if that succeeds move on with the Get-WmiObject call - there's no need to contact a machine if you've just determined that it's "dead".
...
$result = $AlertDataNoDupe | ForEach-Object {
# Create hashtable
$Properties = #{
Server = $_.NetbiosComputerName
MaintenanceWindow = if($table[$_.NetbiosComputerName]){
= $_.NetbiosComputerName
} else {
'Not found!'
}
}
# Perform ping check, keep as boolean
$Properties['PingCheck'] = Test-Connection -Count 1 $_.NetbiosComputerName -Quiet -EA SilentlyContinue
$Properties['LastReboot'] = if($Properties['PingCheck'])
{
try
{
# Server seems to be online
$operatingSystem = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_.NetbiosComputerName -ErrorAction Stop
[Management.ManagementDateTimeConverter]::ToDateTime($operatingSystem.LastBootUpTime)
}
catch
{
'Access Denied!'
}
}
else
{
# If server doesn't respond, declare it offline
'Computer offline!'
}
# create the object
New-Object -TypeName psobject -Property $Properties
}

Related

Powershell - Start-ThreadJob Ids Incrementing Very Quickly, Why?

New to Powershell and Stackoverflow. Here's my first Powershell Script that I'm trying to optimize to the best of my abilities. My goal is to have the code run as efficiently as possible. Any help/suggestions on that front would be much appreciated!
This script shows new 'Established' TCP Connections (Get-NetTCPConnection) and their associated DNS Hostnames (Resolve-DnsName). Each new Connection is compared to an array of previous Connections. If they have the same 'RemoteAddress', the DNS Hostname is copied over to the new Connection and displayed; otherwise it creates a new (Resolve-DnsName) (Start-ThreadedJob), and moves on to the next new Connection. Once a Job is 'Completed' it copies over the 'NameHost' and displays the Connection.
I have hit a roadblock in my understanding. When the code is running, the Job 'Ids' seem to be incrementing very quickly even though no new Jobs where created in between the last Job and the new Job.
To test the script, run it and visit any Site. Watch as the 'Id' increment very quickly. Please note that it will create a Log File in "C:\Temp\Active_Connections.csv"
$logFile = 'C:\Temp\Active_Connections.csv'
if (-not(Test-Path $logFile -PathType Leaf)){
New-Item -ItemType File -Force -Path $logFile | Out-Null
} else {
Clear-Content $logFile
}
$headersAdded = $true
$newConnections = #()
While ($true){
$connections = #(Get-NetTCPConnection)
foreach ($connection in $connections){
if ($connection.State -eq "Established"){
if ($newConnections.InstanceID -notcontains $connection.InstanceID){
if ($newConnections.RemoteAddress -notcontains $connection.RemoteAddress){
if ((Get-Job).Name -notcontains $connection.RemoteAddress){
Start-ThreadJob -Name $connection.RemoteAddress -ScriptBlock {param($remoteAddress) Resolve-DNSName -Name $remoteAddress} -ArgumentList $connection.RemoteAddress >$null
}else{
$job = Get-Job | Where-Object {$_.Name -eq $connection.RemoteAddress}
if ($job.State -eq "Completed"){
Add-Member -InputObject $connection -MemberType NoteProperty -Name "Id" -Value $job.Id -Force
Try {
$receivedJob = $job | Receive-Job -ErrorAction Stop
Add-Member -InputObject $connection -MemberType NoteProperty -Name "NameHost" -Value $receivedJob.NameHost -Force
}catch{
$na = "N/A"
Add-Member -InputObject $connection -MemberType NoteProperty -Name "NameHost" -Value $na -Force
}
#Remove-Job -Id $job.Id
}
}
}else{
foreach ($newConnection in $newConnections){
if ($newConnection.RemoteAddress -eq $connection.RemoteAddress){
Add-Member -InputObject $connection -MemberType NoteProperty -Name "NameHost" -Value $newConnection.NameHost -Force
}
}
}
}
if ($null -ne $connection.NameHost){
if ($headersAdded) {
$formatting = #{n='CreationTime';e={$_.CreationTime.ToString("h:mm:ss tt")}},'Id','LocalAddress','LocalPort','RemoteAddress','RemotePort','NameHost'
$properties = #{Expression="CreationTime";Width=13},#{Expression="Id";Width=4},#{Expression="LocalAddress";Width=15},#{Expression="LocalPort";Width=10;Alignment="Left"},#{Expression="RemoteAddress";Width=15},#{Expression="RemotePort";Width=10;Alignment="Left"},#{Expression="NameHost";Width=100}
($connection | Select-Object $formatting | Format-Table -Property $properties | Out-String).Trim() | Tee-Object -FilePath $logFile -Append
$headersAdded = $false
} else {
($connection | Select-Object $formatting | Format-Table -HideTableHeaders -Property $properties | Out-String).Trim() | Tee-Object -FilePath $logFile -Append
}
$newConnections += $connection
}
}
}
}
Please, let me know what I can do better and if you have any ideas as to why the Job Id's are incrementing so quickly between new Connections.
Appreciate the help,
Chris
I have no explanation for the jumps in job ID values. While it would be good to know the reason, pragmatically speaking, it isn't necessarily a problem.
Your code creates a tight loop which is best avoided.
The following is a PowerShell-idiomatic reformulation of your code that tries to get results as early as possible, while sleeping a fixed amount of time between tries (which you can obviously adjust).
The upshot is that the output objects won't necessarily be ordered chronologically.
The Id property (column) of the output objects reflects the original output order as returned by Get-NetTCPConnection
# NOTE: What is created is NOT a CSV file.
# It is a plain-text file in tabular format FOR THE HUMAN OBSERVER.
$logFile = 'C:\Temp\Active_Connections.csv'
& {
$newConnections = [ordered] #{} # (Ordered) hashtable that stores all new connections.
while ($true) {
# Look for new connections, and start a thread job for each
# in order to resolve the remote adddress to a domain name, if possible.
Get-NetTCPConnection |
Where-Object { $_.State -eq 'Established' -and -not $newConnections.Contains($_.InstanceID) } |
ForEach-Object {
$jb = Start-ThreadJob { Resolve-DNSName -Name ($using:_).RemoteAddress }
$newConnections[$_.InstanceID] =
$_ |
Select-Object CreationTime,
#{
n = 'Id'
e = { $jb.Id }
},
LocalAddress, LocalPort, RemoteAddress, RemotePort,
#{
n = 'NameHost'
e = { $jb }
}
}
# Sleep a little, to avoid a tight loop.
Start-Sleep -Milliseconds 300
# Look for thread jobs that have completed, and output
# the connection-info objects with the job result.
$newConnections.Keys |
ForEach-Object {
if (($obj = $newConnections[$_]) -and ($jb = $obj.NameHost).State -notin 'NotStarted', 'Running') {
# A completed job: get its result.
$result = try { $jb | Receive-Job -ErrorAction Stop } catch { #{ NameHost = 'n/a' } }
$jb | Remove-Job -Force # Remove the completed job.
$obj.NameHost = $result.NameHost # Update the object with the job result.
$obj # Output the updated object.
$newConnections[$_] = $null # No need to hang on to the object in the hasthable.
}
}
}
} |
Format-Table #{ Name = 'CreationTime'; Expression = { $_.CreationTime.ToString('h:mm:ss tt') }; Width = 13 },
#{Expression = "Id"; Width = 4 },
#{Expression = "LocalAddress"; Width = 15 },
#{Expression = "LocalPort"; Width = 10; Alignment = "Left" },
#{Expression = "RemoteAddress"; Width = 15 }, #{Expression = "RemotePort"; Width = 10; Alignment = "Left" },
#{Expression = "NameHost"; Width = 100 } |
Tee-Object -FilePath $logFile

how to execute multiple Invoke-Sqlcmd in one transaction?

I would like to perform a bunch of invoke-sqlcmd in one sql transaction. Here's what I'm doing:
try{
$scope = New-Object -TypeName System.Transactions.TransactionScope
GetFiles $SqlFilesDirectory
$scope.Complete()
}
catch{
$_.exception.message
}
finally{
$scope.Dispose()
}
Here's how GetFiles is defined:
#
# Get SQL Files recursively
#
function GetFiles($path = $pwd)
{
$subFolders = Get-ChildItem -Path $path -Directory | Select-Object FullName,Name | Sort-Object -Property Name
$sqlFiles = Get-ChildItem -Path $path -Filter *.sql | Select-Object FullName,Name | Sort-Object -Property Name
foreach ($file in $sqlFiles)
{
Write-Host "file: " $file.Name
Invoke-Sqlcmd -ServerInstance $ServerInstance -Database $DBName -Username $SvcAdminAccount -Password $SvcAdminPassword -InputFile $file.FullName -QueryTimeout 65535
}
foreach ($folder in $subFolders)
{
Write-Host "`nGetting files for subfolder: " $folder.Name
GetFiles $folder.FullName
}
}
How do we perform a series of invoke-sqlcmd in one transaction?
Here's the output:
The behavior that I want is that ALL
changes are rolled back if a single sql script fails.

PowerShell Get Windows OS Version Fast and Do Different Things

Is there a faster way to get a specific registry value from a list of servers? I'm selecting a text file of computers with different flavors of windows and getting the OS product name. I'm finding that it's taking a couple seconds per computer to retrieve.
Current script:
Clear-Host
# Prompt for file containing list of target
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$myDialog = New-Object System.Windows.Forms.OpenFileDialog
$myDialog.Title = "Select File of Target Systems"
$myDialog.InitialDirectory = $PSScriptRoot
$myDialog.Filter = "TXT (*.txt) | *.txt"
$result = $myDialog.ShowDialog()
If ($result -eq "OK") {
$Computers = Get-Content $myDialog.FileName
}
Else {
Write-Host "`nCancelled by User`n"
}
$Array = #()
# Loop Through Computers
ForEach ($Computer in $Computers) {
Write-Warning "Processing $Computer"
# Get Registry Values
Try {
$OSVersion = Invoke-Command -ComputerName $Computer -ScriptBlock { (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName }
# Create a custom object
$ComplexObject = New-Object PSCustomObject
$ComplexObject | Add-Member -MemberType NoteProperty -Name "Server name" -Value $Computer
$ComplexObject | Add-Member -MemberType NoteProperty -Name "OS Version" -Value $OSVersion
# Add custom object to our array
$Array += $ComplexObject
}
Catch {
$_.Exception.Message
Break
}
}
# Results
If ($Array) {
# Display results in new window
$Array | Out-GridView -Title "OS Version Results"
# Display results in PS console
$Array
}
My end goal later on in the script is to do different things based on the OS version so I want to separate them into independent lists:
If (We have Win2008 servers) {
"Do This"
}
If (We have Win2012R2 servers) {
"Do This"
}
Clear-Host
# Prompt for file containing list of target
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$myDialog = [System.Windows.Forms.OpenFileDialog]::new()
$myDialog.Title = "Select File of Target Systems"
$myDialog.InitialDirectory = $PSScriptRoot
$myDialog.Filter = "TXT (*.txt) | *.txt"
$result = $myDialog.ShowDialog()
If ($result -eq "OK") {
$Computers = Get-Content $myDialog.FileName
}
Else {
Write-Host "`nCancelled by User`n"
}
# Get Registry Values
$Array = Try {
Invoke-Command -ComputerName $Computers -ScriptBlock {
(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName
} -ErrorAction stop | Select-Object #{n="Server Name";e={$_.pscomputername}},
#{n="OS Version";e={$_}}
}
Catch {
write-warning $_.Exception.Message
break
}
# Results
If ($Array) {
# Display results in new window
$Array | Out-GridView -Title "OS Version Results"
# Display results in PS console
$Array
}
You can use Get-AdComputer like:
Get-ADComputer -Filter {(OperatingSystem -like "*windows*server*") -and (Enabled -eq "True")} -Properties OperatingSystem | Select -ExpandProperty OperatingSystem | ForEach {
If($_ -match "Windows Server 2008.*"){
# Server 2008
}
If($_ -match "Windows Server 2012.*"){
# Server 2012
}
# Add more like 2016,2019
}

Ping a list of host names and output the results to a csv in powershell

I have a large list of hostnames I need to ping to see if they are up or down. I'm not really that great at scripting but I managed to figure this much out:
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
Write-Host "$name is up" -ForegroundColor Green
}
else{
Write-Host "$name is down" -ForegroundColor Red
}
}
This gets me what I need but i now need to write out these results to a csv file and i have no idea how to do that.
Please Help!
You can use the following code instead (I simply altered the write-host calls to CSV formatting) and execute it with "PowerShell.exe script.ps > output.csv"
Note that you must execute it from the folder that contains hnames.txt, or simply change the "hnames.txt" to a full path.
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
Write-Host "$name,up"
}
else{
Write-Host "$name,down"
}
}
P.S. You can also use the Out-File Cmdlet to create the CSV file
I am a complete newbie to Powershell, so I took this on as a learning task, as I needed a quick and simple way to check a list of PC's for up/down status. These tweaks were needed to get it to output cleanly to the screen and to a txt file
$Output= #()
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
$Output+= "$name,up"
Write-Host "$Name,up"
}
else{
$Output+= "$name,down"
Write-Host "$Name,down"
}
}
$Output | Out-file "C:\support\result.csv"
$Output= #()
$names = Get-Content ".\input\Servers.txt"
foreach ($name in $names){
if (Test-Connection -Delay 15 -ComputerName $name -Count 1 -ErrorAction SilentlyContinue -quiet){
$Output+= "$name,up"
Write-Host "$Name,up" -ForegroundColor Green
}
else{
$Output+= "$name,down"
Write-Host "$Name,down" -ForegroundColor Red
}
}
$Output | Out-file ".\output\result.csv"
This is a tad cleaner, and includes the original foreground options but, BTW, the 'delay' switch seems to be ignored -PB
I would do it this way. Using a list of computers and -asjob works very well. The Responsetime property (confusingly the header is "Time(ms)") will be non-null if the host is up.
$names = Get-content hnames.txt
test-connection $names -asjob -count 1 | receive-job -wait -auto
Source Destination IPV4Address IPV6Address Bytes Time(ms)
------ ----------- ----------- ----------- ----- --------
COMP001 yahoo.com 74.6.231.21 32 39
COMP001 microsoft.com 40.113.200.201 32
Lately I do it this way. It requires threadjobs installed in powershell 5.1. Or just use get-port. I stick it in a mymod\mymod.psm1 module file somewhere in $env:psmodulepath. I can check a classroom in under 10 seconds.
function get-pport { # multi-threaded
param($list)
$list |
% { $_ | start-threadjob { get-port $input } -throttlelimit 20 } |
receive-job -wait -auto
}
function Get-Port {
Param (
[parameter(ValueFromPipeline)]
[string[]]$Hostname='yahoo.com'
)
begin {
$ports = 22,5988,3389,5985
$ping = New-Object System.Net.Networkinformation.ping
$Timeout = 200 # ms
}
process {
$hostname | foreach {
$openPorts = #()
foreach ($port in $ports) {
$client = New-Object System.Net.Sockets.TcpClient
$beginConnect = $client.BeginConnect($_,$port,$null,$null)
Start-Sleep -Milli $TimeOut
if($client.Connected) { $openPorts += $port }
$client.Close()
}
$result = $Ping.Send($_, $timeout)
if (! $result) { write-error "hostname $_ not found" }
$pingstatus = ($result.status -eq 'Success')
New-Object -typename PSObject -Property #{
HostName = $_
Port = $openPorts
Ping = $pingstatus
} | select hostname,port,ping
} # end foreach
} # end process
}
Example:
$avid = cat afile.txt
pport $avid
HostName Port Ping
-------- ---- ----
A006 {3389, 5985} True
A011 {3389, 5985} True
A015 {3389} True

Powershell Arrays - checking to see if services have stopped

I know there is a quicker way of doing this but I just don't know how to approach it. I also need it to work in PowerShell V2 as I still have Windows 2003 servers I need to handle with this.
Basically this would stop services and then check to see if those services were stopped. I have a location to define them earlier in the script. I am using this to deploy code to servers so depending on the code from Development I may need to stop 1 to 4 services and looking to see if there is a way that I can define the services and not have to comment out code below if I only use 2 services as opposed to four.
#Turn off Services
stop-service $Service1 -force
# stop-service $Service2 -force
# stop-service $Service3 -force
# stop-service $Service4 -force
$VerifyServiceStopped1 = Get-Service $Service1 | Where-Object {$_.status -eq "Stopped"} | select -last 1
# $VerifyServiceStopped2 = Get-Service $Service2 | Where-Object {$_.status -eq "Stopped"} | select -last 1
# $VerifyServiceStopped3 = Get-Service $Service3 | Where-Object {$_.status -eq "Stopped"} | select -last 1
# $VerifyServiceStopped4 = Get-Service $Service4 | Where-Object {$_.status -eq "Stopped"} | select -last 1
if ($VerifyServiceStopped1) {Write-Host $Service1' Stop = Pass (0)'} else {Write-Host $Service1' Stop = Fail (1000)'; Exit '1000'}
# if ($VerifyServiceStopped2) {Write-Host $Service2' Stop = Pass (0)'} else {Write-Host $Service2' Stop = Fail (1001)'; Exit '1001'}
# if ($VerifyServiceStopped3) {Write-Host $Service3' Stop = Pass (0)'} else {Write-Host $Service3' Stop = Fail (1002)'; Exit '1002'}
# if ($VerifyServiceStopped4) {Write-Host $Service4' Stop = Pass (0)'} else {Write-Host $Service4' Stop = Fail (1003)'; Exit '1003'}
Any suggestions?
Thanks
Dwight
Something like this, perhaps?
$services = #(
'Service1',
'Service2',
'Service3',
'Service4')
Get-service |
Where { $Services -Contains $_.Name} |
Foreach {
#Stop Service
#Verify Service
#Restart Service
}
I'm not sure how you want to handle this, but since arrays is tagged:
$services = #("ServiceName1","ServiceName2")
#Turn off Services
foreach($service in $services) {
stop-service $Service1 -force
$VerifyServiceStopped1 = Get-Service $Service1 | Where-Object {$_.status -eq "Stopped"} | select -last 1
if ($VerifyServiceStopped1) {
Write-Host $Service1' Stop = Pass (0)'
} else {
Write-Host $Service1' Stop = Fail (1000)'
Exit '1000'
}
}
"so depending on the code from Development I may need to stop 1 to 4 services"
If you cannot define the logic involved in that decision, how do you plan to automate it?
Just wanted to provide the entire code used in case someone else is looking for something similar - thanks to everyone for throwing ideas into the mix it was very helpful.
#Define Services
$Service1 = 'servicename'
$Service2 =
$Service3 =
$Service4 =
$Service5 =
$Service6 =
$Service7 =
$Service8 =
$Service9 =
$services = #(
$Service1,
$service2,
$service3,
$service4,
$Service5,
$Service6,
$Service7,
$Service8,
$Service9)
#Stop Services
Get-service |
Where { $Services -Contains $_.Name} |
Foreach {
$_ | stop-service
}
Set-Service |
Where { $Services -Contains $_.Name} |
Foreach {
$_ | -startuptype "Disabled"
}
#Verify Services
Get-service |
Where { $Services -Contains $_.Name} |
Foreach {
if ((Get-Service $_.Name).Status -eq "stopped") {Write-Host 'Service Start Pass (0)'} else {Write-Host 'Start Fail (1000)';Exit '1000'}
}
#Start Services
Set-Service |
Where { $Services -Contains $_.Name} |
Foreach {
$_ | -startuptype "Automatic"
}
Get-service |
Where { $Services -Contains $_.Name} |
Foreach {
$_ | start-service
}
#Verify Services
Get-service |
Where { $Services -Contains $_.Name} |
Foreach {
if ((Get-Service $_.Name).Status -eq "running") {Write-Host 'Service Start Pass (0)'} else {Write-Host 'Start Fail (2000)';Exit '2000'}
}
This allows me to have someone list any services they need to stop / start to deploy custom code out there - also then I will be moving this to stop executables in order to replace them and other files for verification. Sadly we are handed a collection of exe's and dll's to just hot swap for some deployments of software so this is why this was needed. I wanted a way of defining what I need to move in and out per deployment but didn't want to comment out lines throughout the script that I didn't need (i.e. only had two services so needed to comment out the others throughout the script).
Just access the status directly:
> (Get-Service -Name "Windows Time").Status -eq "Stopped"
$true
or
if ((Get-Service -Name "Windows Time").Status -eq "Stopped")
{
Write-Host "Yep..."
}
or if you want to be really terse, you can use the alias gsv:
> (gsv "Windows Time").Status -eq "Stopped"
$true
You could also make a function:
Function IsStopped ($name)
{
((Get-Service $name).Status -eq "Stopped")
}
if (IsStopped "Windows Time")
{
Write-Host "foo"
}

Resources