Function within a Function - Powershell - powershell

OK I am going to try to explain this as best as I can. What started out as a simple script has turned into a huge mess and now I cannot figure out how to get it working. I have been coming here for answers for some time so maybe you guys can help.
What I am trying to do is a import a list of systems and check to see if they are online. If they are online they go in one list and if not they go in another.
foreach ($server in $servers) {
if (Test-Connection $server -Count 1 -ea 0 -Quiet) {
Write-Host "$server Is Up" -ForegroundColor Green
$server | out-file -Append $liveSystems -ErrorAction SilentlyContinue
} else {
Write-Host "$server Is Down" -ForegroundColor Red
$server | out-file -Append $inactive -ErrorAction SilentlyContinue
}
}
From there I check to see if the application I need installed is on the systems. That is where things start to go off-track. When I run the function to process the $liveSystems file all I get is the last line of the file (or the same system over and over) and not each system as it should be.
function Is-Installed( $program ) {
$x86 = ((Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
$x64 = ((Get-ChildItem "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
}
$program
function process-file1 {
param($filename)
Get-Content $filename -PipelineVariable line | ForEach-Object {
Is-Installed -program "My_Service"
if (Is-Installed -eq "True") {
Write-Host "$server has agent installed" -ForegroundColor Green
$server | Out-File $installed -ErrorAction SilentlyContinue
}
else
{
Write-Host "$server does not have agent installed" -ForegroundColor Red
$server | Out-File -Append $notInstalled -ErrorAction SilentlyContinue
}
}
}
process-file1 -filename $liveSystems
Once I can get the systems to process through the list of installed and not installed I am trying to take the list of installed systems and check which ones have the service running and which ones do not.
$array = #()
foreach($i in (gc $installed)) {
$svc = Get-Service my_service -ComputerName $i -ea "0"
$obj = New-Object psobject -Property #{
Name = $svc.name
Status = $svc.status
Computer = $i
}
$array += $obj
}
$array | Select Computer,Name,Status | Export-Csv -Path $resultsFile -
NoTypeInformation
Last but not least I run through that list of running and not running and attempt to start the service on systems that are not running.
function process-CSVfile2 {
param($filename)
Import-Csv $filename |
ForEach-Object -PipelineVariable object {
if($_.Status -eq "Running") {
Write-Host "Your Service is currently Running on" $_.Computer
}
if($_.Status -eq "Stopped") {
$serviceName = 'my_service'
$service = Get-CimInstance Win32_Service -ComputerName $_.Computer -Filter "Name=$serviceName"
$service.Start()
$service.WaitForStatus("Started",'00:00:30')
Start-Sleep 10
}
}
}
Several of these blocks run separately but when put together they will not run. I can't seem to get past the second block where it just looks at the same line over and over.
In addition there is a piece I have been trying to get working that would install the application on systems that do not have the service installed but that is not working either but I will save that for a different time.
If anyone can help me with this I would really appreciate it. After 3 days of trying to get it running I am at my wits end.

I'd create objects and properties instead of files with computers online etc...
Something like:
$Computers=New-Object -TypeName System.Collections.ArrayList
$Servers = #(Get-Content -path c:\servers.txt)
$Servers = $Servers | ? {$_} | select-object -uniqe |ForEach-Object {$_.TrimEnd()}
$Servers|ForEach-Object {
$tempobj=New-Object -TypeName PSObject
$tempobj | Add-Member -type NoteProperty -name Name -value $_
$tempobj | Add-Member -type NoteProperty -name isOnline -value $FALSE
$tempobj | Add-Member -type NoteProperty -name Installed -value $FALSE
$tempobj | Add-Member -type NoteProperty -name serviceRunning -value $FALSE
[void]$Computers.Add($tempobj)
then You could work on array (no need for additional files)
$Computers|Where-Object {$_.isOnline -eq $TRUE}
etc

Related

How to get details of OS name, .net framework details for multiple servers using powershell?

I am trying to get details of OS Name and .net framework details for multiple servers using PowerShell script below.
$servers = Get-Content -Path "C:\Users\vinay\Desktop\servers.txt"
Foreach ($s in $servers)
{
write-host $s
$s.PSDrive
$s.PSChildName
Add-Content C:\Users\vinay\Desktop\specs.txt "Specs:"
$OS = (Get-WmiObject Win32_OperatingSystem).CSName
Add-Content C:\Users\vinay\Desktop\specs.txt "`nOS:$OS"
$Bit = (Get-WMIObject win32_operatingsystem).name
Add-Content C:\Users\vinay\Desktop\specs.txt "`nOS Bit: $Bit"
$name = (Get-WmiObject Win32_OperatingSystem).OSArchitecture
Add-Content C:\Users\vinay\Desktop\specs.txt "`nServer Name: $name"
$dotnet = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | Get-ItemProperty -Name version -EA 0 | Where { $_.PSChildName -Match '^(?!S)\p{L}'} | Select PSChildName, version
Add-Content C:\Users\vinay\Desktop\specs.txt "`n.NET VERSION $dotnet"
$release = (Get-ItemProperty "HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release
Add-Content C:\Users\vinay\Desktop\specs.txt "`nRelease number: $release"
Add-Content C:\Users\vinay\Desktop\specs.txt "`n----------------------------"
}
But i am getting details of the server in which i am running the script but not for other servers in the text file.
however write-host $s reads all the servers in the text file. Please help where i am doing wrong.
Continuing from my comment, you need to perform your code looping over the servers in your list and have that code run on that server instead of your own machine you are running the script from.
Also, I would have the code output objects instead of trying to add lines to a text file, so that you ca save the results in a structured format like CSV.
Try
$servers = Get-Content -Path "C:\Users\vinay\Desktop\servers.txt"
$specs = foreach ($s in $servers) {
if (Test-Connection -ComputerName $s -Count 1 -Quiet) {
Write-Host "Probing server $s"
# you may need to add parameter -Credential and supply the credentials
# of someone with administrative permissions on the server
Invoke-Command -ComputerName $s -ScriptBlock {
$os = (Get-CimInstance -ClassName Win32_OperatingSystem)
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse |
Get-ItemProperty -Name Version, Release -Erroraction SilentlyContinue |
Where-Object { $_.PSChildName -Match '^(?!S)\p{L}'} |
ForEach-Object {
[PsCustomObject]#{
'ComputerName' = $os.CSName
'Operating System' = $os.Caption
'Architecture' = $os.OSArchitecture
'Net Version' = [version]$_.Version
'Net Framework' = $_.PsChildName
'Net Release' = $_.Release
}
}
}
}
else {
Write-Warning "Computer '$s' cannot be reached.."
}
}
# remove extra properties PowerShell added
$specs = $specs | Select-Object * -ExcludeProperty PS*, RunspaceId
# output on screen
$specs | Format-Table -AutoSize
# output to CSV file you can open in Excel
$specs | Export-Csv -Path 'C:\Users\vinay\Desktop\specs.csv' -UseCulture -NoTypeInformation

How to restart services on remote server in PowerShell

I am trying to do something very simple but have lost my way. I have this script that works almost perfect. It goes into a list of servers checks for 2 services and checks if they are running. Then, it generates a list of servers that are running or stopped. Now, what I need it to do is to restart the service when it finds that it is stopped...
$Computers = Get-Content -path C:\computers.txt
$Computerobject = $null
FOREACH ( $Computer IN $Computers )
{
TRY
{
$Services = $null
$Services = (Get-Service -Name 'CrystalToPDF','Bradware - BoxUploadService' -computername $Computer -ErrorAction Stop | Select-Object Status, Name, DisplayName )
Add-Member -InputObject $Services -NotePropertyName ComputerName -NotePropertyValue $Computer
$ComputerProperties = [ordered] #{ComputerName=$Services.ComputerName; ServiceName=$Services.Name; Status=$Services.Status}
$Computerobject = New-Object PSObject -Property $ComputerProperties
$Computerobject
}
CATCH
{
Write-Host "Both services are not installed on $Computer." -ForegroundColor Red
}
}
Following from my comment.
Like this:
$Computers = Get-Content -path C:\computers.txt
$Computerobject = $null
FOREACH ( $Computer IN $Computers )
{
TRY
{
$Services = $null
$Services = (Get-Service -Name 'CrystalToPDF','Bradware - BoxUploadService' -computername $Computer -ErrorAction Stop | Select-Object Status, Name, DisplayName )
if($Services.Status -eq "Stopped"){ Get-Service -Name 'CrystalToPDF','Bradware - BoxUploadService' -computername $Computer | Start-Service}
Add-Member -InputObject $Services -NotePropertyName ComputerName -NotePropertyValue $Computer
$ComputerProperties = [ordered] #{ComputerName=$Services.ComputerName; ServiceName=$Services.Name; Status=$Services.Status}
$Computerobject = New-Object PSObject -Property $ComputerProperties
$Computerobject
}
CATCH
{
Write-Host "Both services are not installed on $Computer." -ForegroundColor Red
}
}

Powershell - looping through an array

I'm looking to search the C and E drives of all Windows servers in
Active Directory for any existing copies of putty.exe and their version.
The output needs to have the server name, full path to the executable,
and the file version. So far I have the following code (which right now is only using
two servers for testing:
$ComputerName = Get-ADComputer -filter "name -like 'computer01' -or name `
-like 'server01'" | select -ExpandProperty name
$OutputArr = #()
$findFiles = foreach($computer in $computername){
$file = Invoke-Command -computername $computer { Get-ChildItem -Path `
c:\, e:\ -Recurse | where-object{(!$_.psiscontainer -eq $true) -and `
($_.name -like "putty.exe")} | ForEach-Object -process {$_.fullname} }
$output = $OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
Write-Verbose $OutputObj
}
$OutputArr | fl
The above code outputs the following array:
ComputerName : COMPUTER01
FilePath : {C:\Program Files\PuTTY\putty.exe, C:\Program Files (x86)\PuTTY\PUTTY.EXE}
ComputerName : SERVER01
FilePath : {C:\Program Files (x86)\putty\putty.exe, C:\Users\testuser\Desktop\Public Desktop\putty.exe}
This produces the correct data, but now I need to run another snippet of code against each
separate filepath under computername, but am not sure how to accomplish this, as it is
taking the full filepath with multiple entries.
Essentially, I need to separate each ComputerName in the array into multiple lines:
COMPUTER01,C:\Program Files\PuTTY\putty.exe
COMPUTER01,C:\Program Files (x86)\PuTTY\PUTTY.EXE
SERVER01,C:\Program Files (x86)\putty\putty.exe
Etc...
Is an array not the correct way to do it?
If you are working strictly with what you already have stored in $OutputArr, the following will work:
$out = foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
[pscustomobject][ordered]#{ComputerName = $line.ComputerName; FilePath = $fp}
}
}
else {
$line
}
}
$out | ConvertTo-Csv -NoTypeInformation
The foreach loop creates new objects with properties ComputerName and FilePath and stores them in $out as an array of objects.
If you do not care about properties and just want a comma-delimited list, you can use the following:
foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
"{0},{1}" -f $line.ComputerName,$fp
}
}
else {
"{0},{1}" -f $line.ComputerName,$line.FilePath
}
}
This does the same looping as the first solution but instead uses the format operator (-f) to format the output. Piping to ConvertTo-Csv formats the output to be comma-delimited with the properties as headers.
You could move your desired functionality into your code before you even store anything in $OutputArr. I feel like doing all this after all of the other looping to create $OutputArr is just adding inefficiency.
PowerShell can get tricky when doing remote sessions. The below script should be a good starting point for you. Here are some other areas for improvement:
Doing Get-ChildItem -Recurse at the root of a drive will use an inordinate amount of memory and you could cause unintentional page file expansion or even make a server unresponsive due to 100% memory usage. In my snippet below I am using a list of well known paths. If you need to to identify if putty.exe is started on additional machines, your monitoring solution hopefully has process performance data and you can search for putty.exe there.
Speaking of memory management, remote shells have limitations of how much memory they can use. If you run winrm get winrm/config/winrs you will see the upper limit.
If you are going to authenticate to additional resources from within your remote script blocks, you will need to set up authentication that supports double hop scenarios (CredSSP or Kerberos)
$computerNames = #('computer1','computer2')
foreach($computer in $computerNames)
{
<#
First Script Block checks well known paths for putty.exe
#>
$puttyResults = Invoke-Command -ComputerName $computer -ScriptBlock {
$wellKnownPaths = #()
$wellKnownPaths += Join-Path $env:USERPROFILE -ChildPath "Desktop"
$wellKnownPaths += "D:\tools\"
$wellKnownPaths += $env:Path.Split(';')
$puttyPaths = #()
foreach($path in $wellKnownPaths)
{
$puttyPaths += Get-ChildItem $path -Filter "putty.exe" -Recurse
}
if($puttyPaths.Count -gt 0)
{
$resultsArray = #()
foreach($path in $puttyPaths)
{
$resultsArray += [PSCustomObject]#{
ComputerName = $env:COMPUTERNAME
PuttyPath = $path.FullName
}
}
return $resultsArray
}
return $null
}
if($puttyResults -ne $null)
{
foreach($result in $puttyResults)
{
<#
Second script block takes action against putty.exe
#>
$puttyExists = Invoke-Command -ComputerName $computer -ArgumentList #($result.PuttyPath) -ScriptBlock {
Param(
$PuttyPath
)
return (Test-Path $PuttyPath)
}
if($puttyExists)
{
$msg = "Putty exists on '{0}', at '{1}'" -f $result.ComputerName, $result.PuttyPath
Write-Host $msg -ForegroundColor:Yellow
}
}
}
}
I am not sure what exactly you are wanting to do, but this should work for iterating through your custom object. Your Invoke-Command can be simplified also.
$file = Invoke-Command -computername $computer { Get-ChildItem -Path "C:\", "E:\" -Recurse -File -Filter "putty.exe" | Select -Property VersionInfo }
$OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
foreach ($item in $OutputArr)
{
for ($i = 0; $i -lt $item.FilePath.Count; $i++)
{
Write-Output ([string]::Join(', ', $item.ComputerName, $item.FilePath[$i].VersionInfo.FileName, $item.FilePath[$i].VersionInfo.FileVersion))
}
}

Assign local variable within scriptblock

I am trying to assign a local variable from within a scriptblock with no luck. The goal is log a status of each machine for the action taken with the data prior and the data after the change. I am not sure how to assign a local variable from within a script block. Any help is much appreciated.
$csvContents = #() # Create the empty array that will eventually be the CSV file
$Computers = Get-ADComputer -Filter '(OperatingSystem -like "Windows Server*") -and (Name -like "AD*")' | Sort-Object Name
foreach ($Computer in $Computers) {
$row = New-Object PSObject # Create an object to append to the array
$row | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value NotSet
$row | Add-Member -MemberType NoteProperty -Name "PingStatus" -Value NotSet
$row | Add-Member -MemberType NoteProperty -Name "DNSChangeStatus" -Value NotSet
$row | Add-Member -MemberType NoteProperty -Name "BeforeChange" -Value NotSet
$row | Add-Member -MemberType NoteProperty -Name "AfterChange" -Value NotSet
#Write-Host "$($Computer.Name): " -ForegroundColor Yellow
$row.ComputerName = $Computer.Name
$rtn = Test-Connection -CN $Computer.dnshostname -Count 1 -BufferSize 16 -Quiet
if ($rtn -match 'True') {
Write-Host -ForegroundColor Green $Computer.DnsHostname
$row.PingStatus = 'Pingable'
Invoke-Command -ComputerName $Computer.Name -ScriptBlock {
$NewDnsServerSearchOrder = "10.93.108.225","10.93.108.134"
$Adapters = Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object {$_.DHCPEnabled -ne 'True' -and $_.DNSServerSearchOrder -eq "10.93.108.226"}
if ($Adapters -ne $null) {
# Show DNS servers before update
Write-Host "Before: " -ForegroundColor Green
$row.DNSChangeStatus = 'Change Needed'
$Adapters | ForEach-Object {
$_.DNSServerSearchOrder
$row.BeforeChange = $_.DNSServerSearchOrder
}
# Update DNS servers
$Adapters | ForEach-Object {$_.SetDNSServerSearchOrder($NewDnsServerSearchOrder)} | Out-Null
# Show DNS servers after update
$Adapters = Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object {$_.DHCPEnabled -ne 'True' -and $_.DNSServerSearchOrder -ne $null}
Write-Host "After: " -ForegroundColor Green
$Adapters | ForEach-Object {
$_.DNSServerSearchOrder
$row.AfterChange = $_.DNSServerSearchOrder
}
} else {
Write-Host "No DNS change needed " -ForegroundColor Yellow
$row.DNSChangeStatus = 'No DNS Change Needed'
}
}
} else {
Write-Host -ForegroundColor Red $Computer.DnsGostname
Write-Host -ForegroundColor Red "Host not pingable"
$row.PingStatus = 'Not Pingable'
}
$csvContents += $row # append the new data to the array
$row = $null
}
$csvContents | Export-Csv -Path C:\DNSChanges.csv
I can't tell exactly what you want to do so I'll guess it's "pass a variable to scriptblock being invoked on a remote machine". If this is the case, you can either add parameters to the scriptblock or use the $using: qualifier as in:
$using:row.DNSChangeStatus = 'Change Needed'
Note that you can't "return" anything that way so you'd have to actually return the modified object from Invoke-Command.
$row = Invoke-Command -ComputerName $Computer.Name -ScriptBlock {
and add
$row
as the last line of the scriptblock.

hashtable filter / select

I was working tonight to re-write an existing server health check script to store its values in a hashtable, and that part is working fine. However, I want the results to go to a CSV file, and that file only to be populated with servers where I've tagged them as requiring action. Currently those are generating event ID 7011, or failing a ping test by Test-Connection.
Here's the code:
$CheckServer = #{}
$Servers = (Get-Content $Dir\Test.txt)
foreach ($Server in $Servers) {
$CheckServer.EventID7011 = Get-Eventlog -LogName System -ComputerName $Server -Newest 1 |
Where-Object {$_.EventId -eq 7011} | select Message
if ($CheckServer.EventID -ne $Null) {
$CheckServer.Server = "$Server"
$CheckServer.ActionReq = "Yes"
}
$CheckServer.Ping = Test-Connection -ComputerName $Server -Count 1 -Quiet
if (! $CheckServer.Ping) {
$CheckServer.Server = "$Server"
$CheckServer.ActionReq ="Yes"
$CheckServer.Ping = "Offline"
} else {
$CheckServer.Server = "$Server"
$CheckServer.ActionReq = "No"
$CheckServer.Ping = "Online"
}
New-Object -TypeName PSObject -Property $CheckServer |
Export-Csv "ScanResults.csv" -NoTypeInformation -Append
}
I need the correct code at the end, as it stands, the script works fine for collecting/storing the data in the hashtable array $CheckServer, but I'd like to only select those servers that require action. So, if I'm scanning 100 servers, and 2 of them are in a ping fail state, I want only those selected and sent to Export-Csv.
If you want only servers that don't respond to Test-Connection in the output anyway it would be much simpler to just use a Where-Object filter on the server list:
Get-Content "$Dir\Test.txt" |
Where-Object { -not (Test-Connection -Computer $_ -Count 1 -Quiet) } |
Select-Object #{n='Server';e={$_}}, #{n='ActionReq';e={'Yes'}},
#{n='Ping';e={'Offline'}} |
Export-Csv 'ScanResults.csv' -NoType -Append
You need to store the objects into a list before you can filter and export them. See the lines with comments in your code:
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
$Servers = (get-content $Dir\Test.txt)
ForEach ($Server in $Servers) {
$CheckServer.EventID7011 = get-eventlog -LogName System -ComputerName
$Server -newest 1 | where-object {$_.eventID -eq 7011} |select message
If ($CheckServer.EventID -ne $Null) {
$CheckServer.Server="$Server"
$CheckServer.ActionReq = "Yes"}
$CheckServer.Ping = Test-Connection -ComputerName $Server -count 1 -quiet
if (! $CheckServer.Ping) {
$CheckServer.Server="$Server"
$CheckServer.ActionReq ="Yes"
$CheckServer.Ping= "Offline"}
Else {
$CheckServer.Server="$Server"
$CheckServer.ActionReq ="No"
$CheckServer.Ping= "Online"}
# Add the server object to the list
$serverObjects += New-Object -TypeName PSObject -Property $CheckServer
}
}
# now filter it:
$serverObjects | where ActionReq -eq "Yes" | Export-Csv -Path "...."