I have a requirement to report the local groups and members from a specific list of servers. I have the following script that I have pieced together from other scripts. When run the script it writes the name of the server it is querying and the server's local group names and the members of those groups. I would like to output the text to a file, but where ever I add the | Out-File command I get an error "An empty pipe element is not allowed". My secondary concern with this script is, will the method I've chosen the report the server being queried work when outputting to a file. Will you please help correct this newbies script errors please?
$server=Get-Content "C:\Powershell\Local Groups\Test.txt"
Foreach ($server in $server)
{
$computer = [ADSI]"WinNT://$server,computer"
"
"
write-host "==========================="
write-host "Server: $server"
write-host "==========================="
"
"
$computer.psbase.children | where { $_.psbase.schemaClassName -eq 'group' } | foreach {
write-host $_.name
write-host "------"
$group =[ADSI]$_.psbase.Path
$group.psbase.Invoke("Members") | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
write-host **
write-host
}
}
Thanks,
Kevin
You say that you are using Out-File and getting that error. You don't show_where_ in your code that is being called from.
Given the code you have my best guess is that you were trying something like this
Foreach ($server in $server){
# All the code in this block
} | Out-File c:\pathto.txt
I wish I had a technical reference for this interpretation but alas I have not found one (Think it has to do with older PowerShell versions). In my experience there is not standard output passed from that construct. As an aside ($server in $server) is misleading even if it works. Might I suggest this small change an let me know if that works.
$servers=Get-Content "C:\Powershell\Local Groups\Test.txt"
$servers | ForEach-Object{
$server = $_
# Rest of code inside block stays the same
} | Out-File c:\pathto.txt
If that is not your speed then I would also consider building an empty array outside the block and populate is for each loop pass.
# Declare empty array to hold results
$results = #()
Foreach ($server in $server){
# Code before this line
$results += $group.psbase.Invoke("Members") | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
# Code after this line
}
$results | Set-Content c:\pathto.txt
Worthy Note
You are mixing Console output with standard output. depending on what you want to do with the script you will not get the same output you expect. If you want the lines like write-host "Server: $server" to be in the output file then you need to use Write-Output
Related
I am updating mass info about users. The script is getting data from a file, comparing with the current data in ARS and changing if necessary.
Unfortunately for two parameters - "st" and "postOfficeBox" - it is updating data all the time altho the data is the same in the file and in AD.
first one is empty, the second one is not
I have checked directly -
PS> $user.$parameters.postofficebox -eq $userQuery.$parameters.postofficebox
True
How can I handle this? It is not an error, but it is annoying and not efficient updating the same data all the time.
#Internal Accounts
$Parameters = #("SamAccountName", "co", "company", "department", "departmentNumber","physicalDeliveryOfficeName","streetAddress","l","st","postalCode","employeeType","manager", "division", "title", "edsvaEmployedByCountry", "extensionAttribute4", "EmployeeID", "postOfficeBox")
#import of users
$users = Import-csv -Path C:\ps\krbatch.csv -Delimiter "," -Encoding UTF8
Connect-QADService -Proxy
#Headers compliance
$fileHeaders = $users[0].psobject.Properties | foreach { $_.Name }
$c = Compare-Object -ReferenceObject $fileHeaders -DifferenceObject $Parameters -PassThru
if ($c -ne $null) {Write-Host "headers do not fit"
break}
#Check if account is enabled
foreach ($user in $users) {
$checkEnable = Get-ADUser $user.SamAccountName | select enabled
if (-not $checkEnable.enabled) {
Write-Host $user.SamAccountName -ForegroundColor Red
}
}
#Main loop
$result = #()
foreach ($user in $users) {
$userQuery = Get-QADUser $user.sAMaccountName -IncludedProperties $Parameters | select $Parameters
Write-Host "...updating $($user.samaccountname)..." -ForegroundColor white
foreach ($param in $Parameters) {
if ($user.$param -eq $userQuery.$param) {
Write-Host "$($user.samaccountname) has correct $param" -ForegroundColor Yellow
}
else {
try {
Write-Host "Updating $param for $($user.samaccountname)" -ForegroundColor Green
Set-QADUser -Identity $user.SamAccountName -ObjectAttributes #{$param=$user.$param} -ErrorVariable ProcessError -ErrorAction SilentlyContinue | Out-Null
If ($ProcessError) {
Write-Host "cannot update $param for $($user.samaccountname) $($error[0])" -ForegroundColor Red
$problem = #{}
$problem.samaccountname = $($user.samaccountname)
$problem.param = $param
$problem.value = $($user.$param)
$problem.error = $($error[0])
$result +=[pscustomobject]$problem
}
}
catch { Write-Host "fail, check if the user account is enabled?" -ForegroundColor Red}
}
}
}
$result | Select samaccountname, param, value, error | Export-Csv -Path c:\ps\krfail.csv -NoTypeInformation -Encoding UTF8 -Append
And also any suggestions to my code, where I can make it better will be appreciated.
Similar to what Mathias R. Jessen was suggesting, the way you are testing the comparison doesn't look right. As debugging approaches either add the suggested Write-Host command or a break point such that you can test at run time.
Withstanding the comparison aspect of the question there's a loosely defined advisory request that I'll try to address.
Why are you you using QAD instead of the native AD module. QAD is awesome and still outshines the native tools in a few areas. But, (without a deep investigation) it looks like you can get by with the native tools here.
I'd point out there's an instance capability in AD cmdlets that allows for incremental updates even without comparison... ie you can run the Set-ADUser cmdlet and it will only write the attributes if they different.
Check out the help file for Set-ADUser
It would be inappropriate and time consuming for me to rewrite this. I'd suggest you check out those concepts for a rev 2.0 ... However, I can offer some advice bounded by the current approach.
The way the code is structured it'll run Set-QADUser for each attribute that needs updating rather than setting all the attributes at once on a per/user basis. Instead you could collect all the changes and apply in a single run of Set-QADUser per each user. That would be faster and likely have more compact logging etc...
When you're checking if the account is enabled you aren't doing anything other than Write-Host. If you wanted to skip that user, maybe move that logic into the main loop and add a Continue statement. That would also save you from looping twice.
Avoid using +=, you can use an [ArrayList] instead. Performance & scalability issues with += are well documented, so you can Google for more info. [ArrayList] might look something like:
$result = [Collections.ArrayList]#()
# ...
[Void]$result.Add( [PSCustomObject]$problem )
I'm also not sure how the catch block is supposed to fire if you've set -ErrorAction SilentlyContinue. You can probably remove If($ProcessError)... and and move population of $Result to the Catch{} block.
I have a Powershell script that seems to be functioning fine except for the fact that part of my output is skipped over and I can't figure out why. Here is the script being run:
#Server list provided for the script.
$ServerList = Get-Content $env:USERPROFILE\Documents\Servers.txt
#Counter for first If loop.
$Counter = 0
#Counter for second If loop.
$Counter2 = 0
#ForEach loop going through the server list, picking out OS, Drives, and CPU info and dumping it into an array.
ForEach($Server in ($ServerList))
{
"Collecting server information on $Server, please wait..."
"Collecting Operating System..."
$OS = gwmi Win32_OperatingSystem -ComputerName $Server | select Caption
"Collecting Storage..."
$Drives = gwmi Win32_LogicalDisk -ComputerName $Server | Format-Table DeviceId, #{n="Size in GB";e={[math]::Round($_.Size/1GB,2)}},#{n="Free Space in GB";e={[math]::Round($_.FreeSpace/1GB,2)}}
"Collecting CPU..."
$CPU = gwmi Win32_Processor -ComputerName $Server | select Name, Manufacturer
$ServerInfo = #($OS,$Drives,$CPU)
#$ServerInfo
#Do loop that posts the info stored in the array and ups the first counter. Runs while counter is equal to 0.
Do
{
"All done. Here's all the info we got on $($Server):"
$ServerInfo
$Counter++
}While ($Counter -eq 0)
#If loop that checks if the Counter has been bumped by the Do loop. Sets Counter to 0 and increases Counter2 by 1.
If ($Counter -eq 1)
{
$Counter = 0
$Counter2++
}
#If loop that checks if Coutner2 is equal to a certain number. This is the hard stop to the loop.
If ($Counter2 -eq 2)
{
"Max number of runs met. Stopping."
break
}
}
I know the script is messy and needs a lot of work, but for some reason after the second pass of the ForEach loop, the OS variable is completely skipped over, and doesn't show up in the console. After the first run, it leaves it out entirely and just posts the Drive and CPU information. I thought maybe it was something weird with the Do and If loops, so I commented them out to test but it's the same result.
I've tried posting the variables OS, Drives, and CPU as the loop runs to make sure it's actually saving something to the variable, and it is, and I'm also calling the variables themselves after the loop breaks to see if something weird is happening in the loop/script. I found out that calling the OS variable and the ServerInfo array after the loop finishes causes it to output with the OS information.
Any idea as to why that is?
I think the reason for the strange output is in the way you are using the counters.
If I understand the question correctly, you want (console) output for each server in the Servers.txt file, BUT maximized to a certain number. There could be over one hundred servers in the text file, but you want to limit the output to just a certain number of servers and then break out of the main ForEach loop.
Not only that, but you want to capture and later combine the different pieces of information you obtained using the various Get-WmiObject calls in a way that PowerShell uses to format them.
The trick there is to use | Out-String.
Below is my version of the script.
$ServerList = Get-Content $env:USERPROFILE\Documents\Servers.txt
$Counter = 0 # the running counter; gets incremented for each server
$MaxCount = 2 # the maximum number of servers you want to process
#ForEach loop going through the server list, picking out OS, Drives, and CPU info and dumping it into an array.
ForEach($Server in $ServerList) {
Write-Host "Collecting server information on $Server, please wait..."
Write-Host "Collecting Operating System..." -ForegroundColor Yellow
$OS = (Get-WmiObject Win32_OperatingSystem -ComputerName $Server |
Select-Object Caption |
Out-String).Trim()
Write-Host "Collecting Storage..." -ForegroundColor Yellow
$Drives = (Get-WmiObject Win32_LogicalDisk -ComputerName $Server |
Select-Object DeviceId,
#{n="Size in GB";e={[math]::Round($_.Size/1GB,2)}},
#{n="Free Space in GB";e={[math]::Round($_.FreeSpace/1GB,2)}} |
Out-String).Trim()
Write-Host "Collecting CPU..." -ForegroundColor Yellow
$CPU = (Get-WmiObject Win32_Processor -ComputerName $Server |
Select-Object Name, Manufacturer |
Out-String).Trim()
Write-Host
# join these strings together with newlines so the output will be readable
$ServerInfo = #($OS,$Drives,$CPU) -join "`r`n`r`n"
# output the serverInfo for this server.
$ServerInfo
Write-Host
# instead of the -join above which looks more like the original code you gave,
# you could also do:
# $ServerInfo = "{0}`r`n`r`n{1}`r`n`r`n{2}`r`n" -f $OS, $Drives, $CPU
# or even:
# Write-Host $OS
# Write-Host
# Write-Host $Drives
# Write-Host
# Write-Host $CPU
# Write-Host
# if $Counter has reached the maximum number of servers to process, break out of the ForEach loop
If (++$Counter -ge $MaxCount) {
Write-Host "Max number of runs ($MaxCount) met. Stopping." -ForegroundColor Green
return
}
Write-Host
}
I have the following..
$output = #()
$servers =Get-Content "C:\Windows\System32\List3.txt"
foreach ($server in $servers)
{
trap [Exception] {continue}
Import-Module PSRemoteRegistry
$key="SOFTWARE\Microsoft\'Microsoft Antimalware'\'Signature Updates'"
$regkey=Get-RegBinary -ComputerName $server -Key $Key -Value SignatuesLastUpdated
#$regkey=(Get-Item HKLM:\SOFTWARE\Microsoft\'Microsoft Antimalware'\'Signature Updates').getValue('SignaturesLastUpdated')
#$regkey=[datetime]::ParseExact("01/02/03", "dd/MM/yy", $null) | Export-csv -path c:\temp\avinfo.csv -append
#$regkey
}
$output | Select $server , $Regkey | Export-Csv c:\temp\avinfo.csv -NoTypeInformation
I think it's pretty close but doesn't work as needed - can anyone tell me what I am doing wrong here - been reading a lot and managed to get this far, just need the help to finalise.
Thanks
Ok... so there is alot that needed to be changed to get this to work. I will update the answer frequently after this is posted.
$servers = Get-Content "C:\Windows\System32\List3.txt"
$key="SOFTWARE\Microsoft\Microsoft Antimalware\Signature Updates"
$servers | ForEach-Object{
$server = $_
Try{
Get-RegBinary -ComputerName $server -Key $Key -Value SignatuesLastUpdated -ErrorAction Stop
} Catch [exception]{
[pscustomobject]#{
ComputerName = $server
Data = "Unable to retrieve data"
}
}
} | Select ComputerName,#{Label=$value;Expression={If(!($_.Data -is [string])){[System.Text.Encoding]::Ascii.GetBytes($_.data)}Else{$_.Data}}} | Export-Csv c:\temp\avinfo.csv -NoTypeInformation
What the above code will do is more in line with your intentions. Take the list and for each item get the key data from that server. If there is an issue getting that data then we output a custom object stating that so we can tell in the output if there was an issue. The part that is up in the air is how you want to export the binary data to file. As it stands it should create a space delimited string of the bytes.
The issues that you did have that should be highlighted are
No need to import the module for every server. Moved that call out of the loop
You have declared the variable $output but do not populate it during your loop process. This is important for the foreach construct. You were, in the end, sending and empty array to you csv. My answer does not need it as it just uses standard output.
As #Meatspace pointed out you had a typo here: SignatuesLastUpdated
Get-RegBinary does not by default create terminating errors which are needed by try/catch blocks. Added -ErrorAction Stop. Don't think your code trap [Exception] {continue} would have caught anything.
The single quotes you have in your $key might have prevented the path from being parsed. You were trying to escape spaces and just need to enclose the whole string in a set of quotes to achieve that.
While Select can use variables they are there, in a basic form, to select property names. In short what you had was wrong.
I spent days trying to implement a parallel jobs and queues system, but... I tried but I can't make it. Here is the code without implementing nothing, and CSV example from where looks.
I'm sure this post can help other users in their projects.
Each user have his pc, so the CSV file look like:
pc1,user1
pc2,user2
pc800,user800
CODE:
#Source File:
$inputCSV = '~\desktop\report.csv'
$csv = import-csv $inputCSV -Header PCName, User
echo $csv #debug
#Output File:
$report = "~\desktop\output.csv"
#---------------------------------------------------------------
#Define search:
$findSize = 40GB
Write-Host "Lonking for $findSize GB sized Outlook files"
#count issues:
$issues = 0
#---------------------------------------------------------------
foreach($item in $csv){
if (Test-Connection -Quiet -count 1 -computer $($item.PCname)){
$w7path = "\\$($item.PCname)\c$\users\$($item.User)\appdata\Local\microsoft\outlook"
$xpPath = "\\$($item.PCname)\c$\Documents and Settings\$($item.User)\Local Settings\Application Data\Microsoft\Outlook"
if(Test-Path $W7path){
if(Get-ChildItem $w7path -Recurse -force -Include *.ost -ErrorAction "SilentlyContinue" | Where-Object {$_.Length -gt $findSize}){
$newLine = "{0},{1},{2}" -f $($item.PCname),$($item.User),$w7path
$newLine | add-content $report
$issues ++
Write-Host "Issue detected" #debug
}
}
elseif(Test-Path $xpPath){
if(Get-ChildItem $w7path -Recurse -force -Include *.ost -ErrorAction "SilentlyContinue" | Where-Object {$_.Length -gt $findSize}){
$newLine = "{0},{1},{2}" -f $($item.PCname),$($item.User),$xpPath
$newLine | add-content $report
$issues ++
Write-Host "Issue detected" #debug
}
}
else{
write-host "Error! - bad path"
}
}
else{
write-host "Error! - no ping"
}
}
Write-Host "All done! detected $issues issues"
Parallel data processing in PowerShell is not quite simple, especially with
queueing. Try to use some existing tools which have this already done.
You may take look at the module
SplitPipeline. The cmdlet
Split-Pipeline is designed for parallel input data processing and supports
queueing of input (see the parameter Load). For example, for 4 parallel
pipelines with 10 input items each at a time the code will look like this:
$csv | Split-Pipeline -Count 4 -Load 10, 10 {process{
<operate on input item $_>
}} | Out-File $outputReport
All you have to do is to implement the code <operate on input item $_>.
Parallel processing and queueing is done by this command.
UPDATE for the updated question code. Here is the prototype code with some
remarks. They are important. Doing work in parallel is not the same as
directly, there are some rules to follow.
$csv | Split-Pipeline -Count 4 -Load 10, 10 -Variable findSize {process{
# Tips
# - Operate on input object $_, i.e $_.PCname and $_.User
# - Use imported variable $findSize
# - Do not use Write-Host, use (for now) Write-Warning
# - Do not count issues (for now). This is possible but make it working
# without this at first.
# - Do not write data to a file, from several parallel pipelines this
# is not so trivial, just output data, they will be piped further to
# the log file
...
}} | Set-Content $report
# output from all jobs is joined and written to the report file
UPDATE: How to write progress information
SplitPipeline handled pretty well a 800 targets csv, amazing. Is there anyway
to let the user know if the script is alive...? Scan a big csv can take about
20 mins. Something like "in progress 25%","50%","75%"...
There are several options. The simplest is just to invoke Split-Pipeline with
the switch -Verbose. So you will get verbose messages about the progress and
see that the script is alive.
Another simple option is to write and watch verbose messages from the jobs,
e.g. Write-Verbose ... -Verbose which will write messages even if
Split-Pipeline is invoked without Verbose.
And another option is to use proper progress messages with Write-Progress.
See the scripts:
Test-ProgressJobs.ps1
Test-ProgressTotal.ps1
Test-ProgressTotal.ps1 also shows how to use a collector updated from jobs
concurrently. You can use the similar technique for counting issues (the
original question code does this). When all is done show the total number of
issues to a user.
I am new in powershell world , I got some project on powershell for inventory reconciling .
I am not sure how to proceed on this , I tried some basic steps and I am able to export
users/group/group membership.
Following are the requirements :
Now What I have achieved - Thanks to Govnah
AD query:
Get-QADUser -searchRoot $OuDomain -SizeLimit 0 |
Select-Object dn, sAMAccountName, #{Name="Groups";Expression={(Get-QADMemberOf $_ | Select-Object -expandProperty Name) -join ";"}} |
Sort-Object SamAccountName |
export-csv $FilePath
I have now two csv files likes AD_users.csv and Oracle_users.csv
I want to compare both files and redirect the difference like
AD users does not exist in Oracle
Oracle User does not exist in AD
Sample data
AD_users.csv
u388848993
K847447388
u994888484
Oracle_users.csv
k095848889
u388848993
I can query oracle database , AD query is also fine the only concern is that I am not able to compare the output.
I did it something like this in a script I wrote:
[System.Collections.ArrayList]$adlist = Get-Content c:\users\sverker\desktop\ad.csv |Sort-Object
[System.Collections.ArrayList]$oraclelist = Get-Content c:\users\sverker\desktop\oracle.csv |Sort-Object
$Matching_numbers = #()
ForEach ($number in $adlist)
{
if ($oraclelist.Contains($number))
{
$Matching_numbers += $number
}
}
ForEach ($number in $Matching_numbers)
{
$adlist.Remove($number)
$oraclelist.Remove($number)
}
now $Matching_numbers now contains the matching numbers
and $adlist contains only numbers from AD
and $oraclelist only numbers from Oracle
you can then loop through the list and display values:
Write-Host "Matches:"
ForEach ($value in $Matching_numbers)
{
$Message += $value + [Environment]::NewLine
}
Write-Host $Message
Write-Host "AD only:"
ForEach ($value in $adlist)
{
$MessageAd += $value + [Environment]::NewLine
}
Write-Host $MessageAd
Write-Host "Oracle only:"
ForEach ($value in $oraclelist)
{
$MessageOracle += $value + [Environment]::NewLine
}
Write-Host $MessageOracle
or simply by writing
$Matching_numbers
will output the list to console
You can output the $Message variables to a file or so..
No doubt, there is a nicer way to do it, but this worked for me for a certain type of file.