Powershell - Variables and foreach loop - windows

This thread got me started very well, but now I need more help
I am trying to loop through my serverlist.txt file, and pass the results of Get-EventLog to Out-GridView and then on to a .csv file. I have this working, but I have to select all the records in the GridView window then click OK for each server.
So, I have the idea that I want to create a $sys variable outside the loop, go in, append the results to that variable for each server, and then exit the loop and pass $sys over to Grid-view.
My confusion comes regardinf variable declaration, type, appending and placement in the code...
I'm just learning PS now, so this may be a little basic for you :)
this code works...need to add in the variable idea in the right places:
#Drop the existing files
Remove-Item C:\system.csv
# SERVER LIST PROPERTIES
# Get computer list to check disk space. This is just a plain text file with the servers listed out.
$computers = Get-Content "C:\ServerList.txt";
#Declare $sys here ??
# QUERY COMPUTER SYSTEM EVENT LOG
foreach($computer in $computers)
{
if(Test-Connection $computer -Quiet -Count 1)
{
Try {
# $sys =
Get-EventLog -ComputerName $computer -LogName System -EntryType "Error","Warning" -After (Get-Date).Adddays(-7) `
| Select-Object -Property machineName, EntryType, EventID, Source, TimeGenerated, Message `
| Out-GridView -PassThru | Export-Csv C:\System.csv -NoTypeInformation -Append;
}
Catch
{
Write-Verbose "Error $($error[0]) encountered when attempting to get events from $computer"
}
}
else {
Write-Verbose "Failed to connect to $computer"
}
}
# $sys | Out-GridView....etc.
Thanks!
Kevin3NF

Just to close this out, I used suggestions from mutiple comments:
$sys = #() (outside the loop)
$sys += Get-EventLog (inside the loop)
$sys | Export-Csv (after the loop to send to .csv)
I even blogged the whole thing, including all the various iterations of learning I went through:
http://dallasdbas.com/getting-to-know-powershell-from-an-old-dba/
Thanks to all that helped. This gave me a framework I will continue to use on these servers as the needs arise.
Kevin3NF

Related

Logging off from all servers in a domain

Occasionally I forget to log off from a server or am disconnected through an error and I don't remember the name of the server. And my domain account starts getting periodically locked out, so I have to access logs on DC to find out which server(s) keep locking my account and log off from it/them. So I wanted to write to script in powershell that would log me off from all servers in a domain (with the exception of the server where I run the script on of course) without me needing to search which to log off from. This is what I have:
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
$SessionIDs | ForEach-Object
{
Write-Host "Logging off session [$($_)]..."
logoff $_
}
}
}
foreach ($Server in $Servers)
{
if ($Server -isnot $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock {$ScriptBlock}
}
}
But when I launch the script, nothing happens. The script doesn't return any errors but doesn't log me off from any server, nor does it write any of the messages from Write-Host cmdlet, obviously. I noticed the $SessionIDs variable definition only returns ID of the first session. Usually this shouldn't be a problem, since it's unlikely I will have more than one session on a server, but I'd like to have this insurance. Can anyone tell me what's wrong in the script?
I notice a few things...
"First, I don't think quser | Where-Object {$_ -match $env:USERNAME} will ever return anything. The output of quser will not contain the hostname."
Try this for getting logon sessions:
$Sessions = (query user /server:$Env:ComputerName) -split "\n" -replace '\s\s+', ';' |
ConvertFrom-Csv -Delimiter ';'
Next, when you reference the $Server variable on the remote machine in your script block, it is out of scope. You would need to use $Using:Server in the script block.
Lastly, the -isnot operator doesn't compare value, it compares type. So in your last foreach, the if statement evaluates to "if type string is not type string" and will not run. Try -ne or -notlike instead.
Working with objects is much easier if you can just parse the output of QUser.exe. Given your scenario, here's my take on it:
$servers = (Get-ADComputer -Filter '*').Name.Where{$_ -ne $env:COMPUTERNAME}
foreach ($server in $servers)
{
if (-not ($quser = ((QUser.exe /server:$server) -replace '\s{20,39}',',,' -replace '\s{2,}',',' 2>&1) | Where-Object -FilterScript { $_ -match $env:USERNAME })) {
Continue
}
Write-Verbose -Message "$($quser.Count) session(s) found on $server." -Verbose
($quser.Trim() | ConvertFrom-Csv -Header 'USERNAME','SESSIONNAME','ID','STATE','IDLE TIME','LOGON TIME').foreach{
Write-Verbose -Message "Logging user [$($_.UserName)] off." -Verbose
LogOff.exe $_.ID /server:$server
}
}
Filtering should always happen before hand meaning, filter out your computer name on your first call to Get-ADComputer. Since you're using QUser.exe and LogOff.exe to begin with, I'd recommend the use of it all the way through since LogOff accepts an ID value that QUser outputs.
Next, placing the call to quser inside your if statement does two things in this case.
Filters for all users matching $ENV:UserName
Returns $true if anything is found, and $false if not found.
So, switching the results using -not will turn $false into $true allowing the execution of the code block which will just continue to the next server.
This in turn doesn't bother with the rest of the code and continues onto the next computer if no matching names were found.
The use of $quser inside the if statement is so you can save the results to it if more than one name is found; (..) allows this as it turns the variable assignment into an expression having the output pass through onto the pipeline where it is either empty, or not.
Finally, referencing the $quser variable we can convert the strings into objects piping to ConvertFrom-Csv. Only step left to do is iterate through each row and passing it over to LogOff to perform the actual logoff.
If you've noticed, the headers are manually-specified because it is filtered out by the Where-Object cmdlet. This is a better approach seeing as there could be "more than one" RDP Session, now you're just left with those sessions matching the name which can be saved to $quser, so no extra filtering is needed down the line.
So I modified the script this way and it works, sort of. It logs off account from servers, which is the main goal. There are still some glitches, like the message it sends from the first Write-Host doesn't give server's name, the message from second one gives a different value than it should (it gives [1] value after -split instead of [2] for some reason; but those are not really that important things, even though I will try to make at least the first message right) and $SessionIDs still gives only the first value, but usually you shouldn't have more than one RDP session per server. I've seen more sessions of one user, but that is very rare. But I'd also like to fix this if possible. Nevertheless, the script basically does the most important thing. But if someone has a suggestion how to fix the glitches I mentioned I would be grateful.
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = , ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
Foreach ($SessionID in $SessionIDs)
{
Write-Host "Logging off session $SessionID..."
logoff $SessionID
}
}
}
foreach ($Server in $Servers)
{
if ($Server -ne $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock $ScriptBlock
}
}

Powershell 'Optimize-Volume' Output

I have written a system maintenance script which executes basic functions that retrieve statistics from a host, writes the output to a new PSObject, then finally combines the results and converts it all to a HTML web page.
I do not seem to be able to write the output of Optimize-Volume to the pipeline, I have to use -verbose - why is this? I would like to check the results of the Optimize-Volume cmdlet by looking for the following text which is generated at the end of the -verbose output, depending on the result:-
'It is recommended that you defragment this volume.'
'You do not need to defragment this volume.'
Here is the function:-
function Get-DefragInfo {
$getwmi = Get-WmiObject -Class Win32_volume -Filter "DriveType = 3" | Where-Object {$_.DriveLetter -cne $null} -ErrorAction SilentlyContinue
$letter = $getwmi.DriveLetter -replace ':'
foreach ($drive in $getwmi)
{
$analysis = Optimize-Volume -DriveLetter $letter -Analyze
if ($analysis -like 'It is recommended that you defragment this volume.')
{
$props =[ordered]#{‘Drive Letter’=$letter
'Defrag Recommended?'='Yes'}
}
elseif ($analysis -like 'You do not need to defragment this volume.')
{
$props =#{‘Drive Letter’=$letter
'Defrag Recommended?'='No'}
}
$obj = New-Object -TypeName PSObject -Property $props
Write-Output $obj
}
}
How do I capture the output I need?
Thanks in advance.
In PowerShell 3.0 and onward, you can use the stream redirection operator > to capture the Verbose ouput to a variable:
# Merge stream 4 (Verbose) into standard Output stream
$analysis = &{Optimize-Volume -DriveLetter $letter -Analyze -Verbose} 4>&1
# Check the "Message" property of the very last VerboseRecord in the output
if($analysis[-1].Message -like "*It is recommended*")
{
# defrag
}
else
{
# don't defrag
}
If we Get-Help Optimize-Volume -full we'll see the cmdlet has no output.
Some searching lead me to this Microsoft Scripting Guys article that pointed out using the following to check if Defrag is needed.
(gwmi -Class win32_volume -Filter "DriveLetter = 'C:'").DefragAnalysis()
Knowing this, we can easily make an IF Statement.
$DefragCheck = (gwmi -Class win32_volume -Filter "DriveLetter = 'C:'").DefragAnalysis() |
Select DefragRecommended
IF($DefragCheck){"Defrag recommended"}ELSE{"Defrag is not needed."}
It's helpful to pipe cmdlets to Get-Member in order to see if there are any options available. In the above example, we can pipe gwmi -Class win32_volume -Filter "DriveLetter = 'C:'" to Get-Member and find the DefragAnalysis method, which we use dotted notation to access (wrap the Get-WmiObject in () then use a . and the method name followed by (), it looks confusing until you try it a couple times!)
Thanks, I went for the verbose redirection option and it seems to be working well. My method is not the cleanest way of doing it I understand, but it works for me.
I like the second option also, I'm going to look at using this once the script is complete and functionality is proofed.
Thanks for your help once again.

Check If a program is installed on multiple computers using Powershell

I'm having issue with a script I've written and would love some help.
Please note I'm very new to powershell.
I've written a script that uses a txt file that contains remote computers on a domain, I appears to be working to some degree but in the event of a machine being offline I get errors which then loop the script.
$machines
$pcname
Name = 'Machine'
Expression = { $_.PsComputerName }
}
ForEach ($System in $Machines)
{
#Pings machine's found in text file
if (!(test-Connection -ComputerName $System -BufferSize 16 -Count 1 -ea 0 -Quiet))
{
Write-Output "$System Offline"
}
Else
{
#Providing the machine is reachable
#Checks installed programs for products that contain Kaspersky in the name
gwmi win32_product -Filter {Name like "%Kaspersky%"} -ComputerName $Machines | Select-Object -Property $pcname,Name,Version
}
}
At present this runs and output's like so:
Machine Name Version
UKTEST01 Kaspersky Security Center Network Agent 10.1.249
UKTEST02 Kaspersky Endpoint Security 10 for Windows 10.2.1.23
But in the event of a machine not being reachable the following error is given:
gwmi : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At C:\Scripts\Powershell\Kaspersky Endpoint Security 10\Script\New folder\Kaspersky Checker working v2.ps1:15 char:9
+ gwmi win32_product -Filter {Name like "%Kaspersky%"} -ComputerName $Mach ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject], COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
And then moves to the next machine in the list, and then repeats from the beginning again.
I'd like for this to simply show as:
UKTEST03 Offline
And stop once the last machine in the txt file is done.
Any help or advise would be greatly appreciated.
This is the perfect time to use a Try/Catch/Finally block. The flow is this : Try the block of code here, if you encounter an error, suppress the message and do what is in the Catch block instead.
I've modified your code a bit, so simply copy this whole code block and drop it in, replacing your Else {scriptblock} in your original code.
Else
{
#Providing the machine is reachable
#Checks installed programs for products that contain Kaspersky in the name
Try {Get-WMIObject -Class win32_product -Filter {Name like "%Kaspersky%"} `
-ComputerName $Machines -ErrorAction STOP |
Select-Object -Property $pcname,Name,Version }
Catch {#If an error, do this instead
Write-Output "$system Offline }
}
}
Your completed answer
I've folded in the change you requested, to keep your script from running on every machine in $machines instead of $system, as you likely intended.
ForEach ($System in $Machines){
#Pings machine's found in text file
if (!(test-Connection -ComputerName $System -BufferSize 16 -Count 1 -ea 0 -Quiet))
{
Write-Output "$System Offline"
}
Else
{
#Providing the machine is reachable
#Checks installed programs for products that contain Kaspersky in the name
Try {Get-WMIObject -Class win32_product -Filter {Name like "%Kaspersky%"} `
-ComputerName $System -ErrorAction STOP |
Select-Object -Property $pcname,Name,Version }
Catch {#If an error, do this instead
Write-Output "$system Offline "}
#EndofElse
}
#EndofForEach
}
You could try this:
$machines=... # your machines' names
foreach ($machine in $machines)
{
trap{"$machine`: not reachable or not running WsMan";continue}
if(test-wsman -ComputerName $machine -ea stop){
gcim -Class CIM_Product -Filter 'Name like "%Kaspersky%"' |
select pscomputername,name,version
}
}
I'm using gcim because gwmi is deprecated.
Correction: the correct name is Kaspersky; I corrected it.

Powershell - Running .ps1 doesn't work, but running it through the console itself does

Before I start, please note I am a beginner at Powershell, so some questions I ask may seem very obvious and stupid to the more experienced.
I have a problem with my script. If I copy paste it into Powershell itself, it works with no problems. However putting it in a .ps1 file, and making it execute with Powershell doesn't work. Can anyone tell me why, and what I can do to make it work using a .ps1? Here's the code:
$Group = import-csv -path C:\Output\Gruppe.csv
$DomainUsers = import-csv -path C:\Output\DomainUsers.csv
$ErrorActionPreference = "SilentlyContinue"
Get-ADGroupMember –identity Test –recursive | select "samaccountname" | Export-csv –path C:\Output\Gruppe.csv -NoTypeInformation
Get-ADUser –Filter * -SearchBase ”ou=Domain Users,dc=sfol,dc=local” | select "samaccountname" | Export-csv –path C:\Output\DomainUsers.csv –NoTypeInformation
Compare-Object $Group $DomainUsers -property samaccountname -IncludeEqual | where-object {$_.SideIndicator -eq "=="} | select "samaccountname" | Export-csv C:\Output\Difference.csv –NoTypeInformation
(Get-Content C:\Output\Difference.csv) | % {$_ -replace '"', ""} | out-file -FilePath C:\Output\Difference.csv -Force -Encoding ascii
$File = "C:\Output\Difference.csv"
$Time = Get-Date
ForEach ($User in (Get-Content $File))
{ Try {
Remove-ADGroupMember -Identity "Test" -Member $User -Confirm:$false -ErrorAction Stop
Add-Content c:\Output\Gruppelog.log -Value "$Time - $User slettet fra gruppen"
}
Catch {
Add-Content c:\Output\Gruppelog.log -Value "$Time - $User medlem kunne ikke blive slettet fra gruppen pga: $($Error[0])"
}
}
I also have another problem I noticed as I am writing this question. What this script does is to print out a userlist from an OU and a group. Then it compares the OU to the group using the two files it printed out, and prints out a new userlist containing only the users that exists in both the OU and the group. Then it uses the new userlist to remove users from the group (so that there is no users that exist in both the OU and group).
This script works well the first time I run it, but if I proceed with re-adding the users to the group, running the script again, sometimes it will only remove some of the users. If I do ctrl+c and CLS after running the script, it works fine. As mentioned, I am a beginner at this, so I'd just like to know why it doesn't work 100% the second time without ctrl+c or cls. Sorry if I am bad at explaining, and I don't expect you to help me with this since it's not a part of the question. But I'd appreciate it if you could.
Kind regards, Shadow
Problem was not having imported the module ActiveDirectory. I thought that when you imported the module once, it would stay for future use in Powershell. This isn't so, so what I did to fix the problem was adding Import-Module ActiveDirectory to the beginning of the code. I still haven't figured out my bonus question, so if anyone can help with that, it'd be great.

Breaking foreach loop in PowerShell v2

I'm having trouble terminating a foreach-object loop in PowerShell v2. For a rough idea of the task I'm trying to accomplish, here's the pseudo-code:
Read lists of host machines from a text file
For each host in the text file get Win32_Product (filtered against an exclusion list),
convert output to html and save.
The reason for the script is that I've amassed a text file listing all applications included on standard client images, and would like to periodically scan hosts from another text file to see if there are any unauthorized, sketchy or otherwise unnecessary applications on the host machines.
The code does work in a rough sense, but the main issue I'm having is that the script will not terminate without manual intervention. I guess the component I'm missing here is to run the loop until some condition exists (ie. first line in the host file is encountered for the second time), then terminates the script. Although this is the method I've envisioned, I am always open to other logic, especially if its more efficient.
Here's the actual code:
Get-Content c:\path\to\testhostlist.txt | Foreach-Object {
Get-WmiObject Win32_Product |
Where-Object { $_.Name -f "'C:\path\to\testauthapplist.txt'" |
ConvertTo-Html name,vendor,version -title $name -body "<H2>Unauthorized Applications.</H2>"}} |
Set-Content c:\path\to\unauthapplisttest.html
I don't see how the first line of the host file (I infer you mean testhostlist.tx) would ever be encountered a second time, since you're only listing it once. This doesn't even seem to be an infinite loop that would need an exit condition. Foreach-Object doesn't repeat indefinitely.
It seems to me that the problem is not that the loop doesn't exit without a condition, it's that the syntax is invalid.
Where-Object filters the pipeline by passing only objects that meet a certain condition, but the scriptblock that follows doesn't perform a boolean test.
In fact, the content of the scriptblock doesn't appear valid in and of itself. -f is the format operator, and takes a format string as the left operand, but $_.Name is not a format string.
I'm going to take a guess here, based on your description, that the idea is to filter the results of Get-WmiObject Win32_Product for objects whose Name property isn't listed in testauthapplist.txt (I take it that's the "exclusion list" you're referring to). If so, this is the correct syntax:
Get-Content c:\path\to\testhostlist.txt | %{
Get-WmiObject Win32_Product | ?{
(Get-Content 'C:\path\to\testauthapplist.txt') -notcontains $_.Name
} | ConvertTo-Html name,vendor,version -title $name -body "<H2>Unauthorized Applications.</H2>"
} | Set-Content c:\path\to\unauthapplisttest.html
(Note that %{} and ?{} are just abbreviations for Foreach-Object and Where-Object, respectively.)
If i understood you correctly you are trying to stop your Script completely? If so did you try Break?
If you only want to skip a loop use continue
$hostlist = Get-Content c:\path\to\testhostlist.txt
$a = #()
Foreach($item in $hostlist)
{
$a += "<style>"
$a += "BODY{background-color:gray;}"
$a += "TABLE{margin: auto;border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
$a += "TH{border-width: 1px;padding: 4px;border-style: solid;border-color: black;background-color:yellow}"
$a += "TD{border-width: 1px;padding: 4px;border-style: solid;border-color: black;background-color:white}"
$a += "h2{color:#fff;}"
$a += "</style>"
Get-WmiObject Win32_Product | select name,vendor,version | sort name | ConvertTo-Html -head $a -body "<Center><H2>Unauthorized Applications.</H2></Center>" | Out-File c:\path\to\$item"-applist.html"
}

Resources