im stuck on how to output my foreach loop into a csv or excel file. this script just get all computers off a local network and then test to see if that computer has a certain KBPatch. The script works just like I said I need help trying to make it output to a csv file. any tips/help is appreciated
Code Below
$strCategory = "computer"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$ObjSearcher.SearchRoot = $objDomain
$objSearcher.filter = ("(objectCategory=$strCategory)")
$colProplist = "name"
foreach($i in $colProplist)
{
$objSearcher.PropertiesToLoad.Add($i)
}
#Finds all operating systems and computer names
$colResults = $objSearcher.FindAll()
foreach($objResult in $colResults)
{
$objComputer = $objResult.Properties;
$names = $objComputer.name
# Define Hotfix to check
$CheckKBS = #(“patch#" , "patch#")
#Query the computers for the HotFix
foreach($name in $names)
{
foreach ($CheckKB in $CheckKBS) {
$HotFixQuery = Get-HotFix -ComputerName $name | Where-Object {$_.HotFixId -eq $CheckKB} | Select-Object -First 1;
if($HotFixQuery -eq $null)
{
Write-Host “Hotfix $CheckKB is not installed on $name”;
}
else
{
Write-Host “Hotfix $CheckKB was installed on $name by ” $($HotFixQuery.InstalledBy);
}
}}
}
To do this cleanly I usually use a custom object.
Before your foreach, instantiate an empty array:
$records = #()
and make an object template:
$tmpRecord = [PSCustomObject]#{
serverName = ''
missingKB = ''
}
Inside the foreach, clone the record obj:
$record = $tmpRecord.psobject.copy()
put your data into the record:
$record.serverName = $name
$record.missingKB = $CheckKb
Put the record into the array:
$records += $record
Then after the foreach, export to csv:
$records | export-csv yourcsv.csv
The need for an object template was confusing to me when I first learned this pattern. You need this because of the combination of scope and how objects are added to arrays (by reference).
If you try to get away with declaring an object inside the loop then that object will be scoped to the lifetime of the foreach loop. You'll then add a reference to that object to your $records array. After the foreach loop completes you will have an array full of references to objects that do not exist.
Related
I found this thread that offers two basic approaches to getting local group members.
This works for me in all versions of powershell, but depends on using the old NET command line utility.
function Get-LocalGroupMembers() {
param ([string]$groupName = $(throw "Need a name") )
$lines = net localgroup $groupName
$found = $false
for ($i = 0; $i -lt $lines.Length; $i++ ) {
if ( $found ) {
if ( -not $lines[$i].StartsWith("The command completed")) {
$lines[$i]
}
} elseif ( $lines[$i] -match "^----" ) {
$found = $true;
}
}
}
This works for me in PowerShell 2.0, but barfs in PS5.0 with Error while invoking GetType. Could not find member.
It only barfs on some groups, including Administrators, which has me thinking it's some sort of security feature, like requiring elevated privileges to REALLY have admin rights in a script.
Function Get-LocalGroupMembers
{
Param(
[string]
$server = "."
)
Try
{
$computer = [ADSI]"WinNT://$( $Server ),computer"
$computer.psbase.children |
where {
$_.psbase.schemaClassName -eq 'group'
} |
ForEach {
$GroupName = $_.Name.ToString()
$group =[ADSI]$_.psbase.Path
$group.psbase.Invoke("Members") |
foreach {
$memberName = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null) -replace "WinNT:", ""
$props = #{
"LocalGroup" = $GroupName
"MemberName" = $memberName
}
$obj = New-Object -TypeName psobject -Property $props
Write-Output $obj
} # foreach members
} # foreach group
}
Catch
{
Throw
}
}
I think I read somewhere that PS5.1 has a native CMDlet finally. But I can't depend on a particular version of PS, I need to support everything from PS2.0 in Win7 up. That said, is there a single version agnostic solution that doesn't depend on a command line utility kludge? Or do I need to have code that uses the old hack or the new CMDlet depending on PS version I am running on?
So, I am having good luck with this solution.
$hostname = (Get-WmiObject -computerName:'.' -class:Win32_ComputerSystem).name
$wmiQuery = Get-WmiObject -computerName:'.' -query:"SELECT * FROM Win32_GroupUser WHERE GroupComponent=`"Win32_Group.Domain='$Hostname',Name='$group'`""
if ($wmiQuery -ne $null) {
:searchLoop foreach ($item in $wmiQuery) {
if (((($item.PartComponent -split "\,")[1] -split "=")[1]).trim('"') -eq $user) {
$localGroup = $true
break :searchLoop
}
}
}
I'm not sure yet if I like that overly complex IF vs some variables, but the functionality is there and working across all versions of PS without resorting to command line kludges, which was the goal.
Note that this just returns true if the user is a member of the group, which is all I need. The other code I posted would provide a list of members, which is the basis of doing a check, and I just hadn't modified it to show the real end goal, since the problem manifested without that.
Instead this :
$memberName = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null) -replace "WinNT:", ""
You can try this :
$memberName = ([ADSI]$_).InvokeGet("Name")
I hope you can help me out. I work with two ArrayLists:
array1 is filled with log.csv (contains header and logon-data, values of column 'pc' are unique). It's defined as ArrayList because I want to add entries.
#pc,name,date,city
#pc-x,name-1,2017-01-01,berlin
#pc-y,name-1,2017-01-02,berlin
#pc-z,name-2,2017-01-02,munich
[System.Collections.ArrayList]$array1 = Import-Csv log.csv
array2 is filled during runtime
$array2=[System.Collections.ArrayList]#()
... ForEach-Object {
$array2.Add([PSCustomObject]#{ pc=$pcname
name=$loginname
date=$logindate }) >$null }
What I want to do:
Update array1.date=array2.date where array1.pc=array2.pc
If no entry found in array1 I want to add it:
$array1.Add([PSCustomObject]#{ pc=$pcname
name=$loginname
date=$logindate
city='[unknown]' }) >$null
Finally array1 is exported again:
$array1 | Export-Csv log.csv -Encoding UTF8 -NoTypeInformation
So the question is: how can I find the entry in array1 and update it? Trying hard for days now ...
try Something like this:
$array1=import-csv "C:\temp\log.csv"
$array2=import-csv "C:\temp\log2.csv"
#modify founded and output not founded
$toadd=$array2 | %{
$current=$_
$founded=$array1 | where pc -eq $current.pc | %{$_.date=$current.date;$_}
if ($founded -eq $null)
{
$current.city='UNKNOW'
$current
}
}
#output of $array1 modified and elements to add
$array1, $toadd
Here is a sample I created that might help. Note: I use List types instead of ArrayList ones. Also, it assumes only one possible matching PC name in the data to be updated. You'll have to alter it to update the file since it merely updates the first List variable. Let me know how it goes.
[PSCustomObject]
{
[string] $pc,
[string] $name,
[string] $date,
[string] $city
}
[System.Collections.Generic.List[PSCustomObject]] $list1 = Import-Csv "C:\SOSamples\log.csv";
[System.Collections.Generic.List[PSCustomObject]] $list2 = Import-Csv "C:\SOSamples\log2.csv";
[PSCustomObject] $record = $null;
[PSCustomObject] $match = $null;
foreach($record in $list2)
{
# NOTE: This only retrieves the FIRST MATCHING item using a CASE-INSENSITIVE comparison
$match = $list1 | Where { $_.pc.ToLower() -eq $record.pc.ToLower() } | Select -First 1;
if($match -eq $null)
{
Write-Host "Not Found!";
$list1.Add($record);
}
else
{
Write-Host "Found!";
$match.date = $record.date;
}
}
Write-Host "--------------------------------------------------------------------"
foreach($record in $list1)
{
Write-Host $record
}
I'm writing a script that I'd like to be able to easily move between IIS servers to analyze logs, but these servers store the logs in different places. Some on C:/ some on D:/ some in W3SVC1, some in W3SVC3. I'd like to be able to have powershell look this information up itself rather than having to manually edit this on each server. (Yeah, I'm a lazy sysadmin. #automateallthethings.)
Is this information available to PowerShell if I maybe pass the domain to it or something?
I found this to work for me since I want to know all of the sites log directory.
Import-Module WebAdministration
foreach($WebSite in $(get-website))
{
$logFile="$($Website.logFile.directory)\w3svc$($website.id)".replace("%SystemDrive%",$env:SystemDrive)
Write-host "$($WebSite.name) [$logfile]"
}
Import-Module WebAdministration
$sitename = "mysite.com"
$site = Get-Item IIS:\Sites\$sitename
$id = $site.id
$logdir = $site.logfile.directory + "\w3svc" + $id
Thanks for Chris Harris for putting the website ID idea in my head. I was able to search around better after that and it led me to the WebAdministration module and examples of its use.
Nice... I updated your script a little bit to Ask IIS for the log file location.
param($website = 'yourSite')
Import-Module WebAdministration
$site = Get-Item IIS:\Sites\$website
$id = $site.id
$logdir = $site.logfile.directory + "\w3svc" + $id
$time = (Get-Date -Format "HH:mm:ss"(Get-Date).addminutes(-30))
# Location of IIS LogFile
$File = "$logdir\u_ex$((get-date).ToString("yyMMdd")).log"
# Get-Content gets the file, pipe to Where-Object and skip the first 3 lines.
$Log = Get-Content $File | where {$_ -notLike "#[D,S-V]*" }
# Replace unwanted text in the line containing the columns.
$Columns = (($Log[0].TrimEnd()) -replace "#Fields: ", "" -replace "-","" -replace "\(","" -replace "\)","").Split(" ")
# Count available Columns, used later
$Count = $Columns.Length
# Strip out the other rows that contain the header (happens on iisreset)
$Rows = $Log | where {$_ -like "*500 0 0*"}
# Create an instance of a System.Data.DataTable
#Set-Variable -Name IISLog -Scope Global
$IISLog = New-Object System.Data.DataTable "IISLog"
# Loop through each Column, create a new column through Data.DataColumn and add it to the DataTable
foreach ($Column in $Columns) {
$NewColumn = New-Object System.Data.DataColumn $Column, ([string])
$IISLog.Columns.Add($NewColumn)
}
# Loop Through each Row and add the Rows.
foreach ($Row in $Rows) {
$Row = $Row.Split(" ")
$AddRow = $IISLog.newrow()
for($i=0;$i -lt $Count; $i++) {
$ColumnName = $Columns[$i]
$AddRow.$ColumnName = $Row[$i]
}
$IISLog.Rows.Add($AddRow)
}
$IISLog | select #{n="DateTime"; e={Get-Date ("$($_.date) $($_.time)")}},csuristem,scstatus | ? { $_.DateTime -ge $time }
Fairly new to PowerShell and wondering if someone could provide a hand with the following. Basically I have a .txt document with a number of records separated by a special character ($)(e.g)
ID
Date Entered
Name
Title
Address
Phone
$
ID
Date Entered
Name
Title
Address
Phone
$
I want to split each item between the $ into individual "records" so that I can loop through each record. So for example I could check each record above and return the record ID for all records that didn't have a phone number entered.
Thanks in advance
If each record contains a fixed number of properties you can take this approach. It loops through creating custom objects while skipping the dollar sign line.
$d = Get-Content -Path C:\path\to\text\file.txt
for ($i = 0; $i -lt $d.Length; $i+=7) {
New-Object -TypeName PsObject -Property #{
'ID' = $d[$i]
'Date Entered' = $d[$i+1]
'Name' = $d[$i+2]
'Title' = $d[$i+3]
'Address' = $d[$i+4]
'Phone' = $d[$i+5]
}
}
I once had the same requirement... this is how I did it
$loglocation = "C:\test\dump.txt"
$reportlocation = "C:\test\dump.csv"
$linedelimiter = ":"
$blockdelimiter = "FileSize"
$file = Get-Content $loglocation
$report = #()
$block = #{}
foreach ($line in $file)
{
if($line.contains($linedelimiter))
{
$key = $line.substring(0,$line.indexof($linedelimiter)).trimend()
$value = $line.substring($line.indexof($linedelimiter)+1).trimstart()
$block.Add($key,$value)
if ($block.keys -contains $blockdelimiter)
{
$obj = new-object psobject -property $block
$report += $obj
$block = #{}
}
}
}
$report
$report | Export-Csv $reportlocation -NoTypeInformation
So you cycle through each line, define key and value and add the object to a hashtable. Once the keys contains the blockdelimiter a new object gets written to an array and the hashtable gets cleared.
In my case the linedelimiter was a colon and the blockdelimiter was a valid record so you will have to make some changes. Let me know if this approach suits your needs and you can't find what to do.
P.S. By default only the noteproperties of the first object in the array will be shown so you will have to pipe the array to Select-Object and add all properties needed.
Grts.
I have the following powershell script which binds to an active directory OU and lists the computers. It seems to work fine except that it ouputs an extra 0 - I'm not sure why. Can anyone help?
$strCategory = "computer"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP:// OU=Computers,OU=datacenter,DC=ourdomain,DC=local")
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher($objDomain)
$objSearcher.Filter = ("(objectCategory=$strCategory)")
$colProplist = "name"
foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)}
$colResults = $objSearcher.FindAll()
foreach ($objResult in $colResults)
{
$objComputer = $objResult.Properties;
$objComputer.name
}
Output:
0
Server1
Server2
Server3
You need to capture (or ignore) the output of the PropertiesToLoad.Add method, otherwise you'll get a value for each property in $colPropList.
foreach ($i in $colPropList){[void]$objSearcher.PropertiesToLoad.Add($i)}
You can simplify and shorten your script and load a bunch of properties in one call without using a foreach loop. Another benefit of the AddRange method is that it doesn't output the length of requested properties, so there's no need to capture anything.
$strCategory = "computer"
$colProplist = "name","distinguishedname"
$searcher = [adsisearcher]"(objectCategory=$strCategory)"
$searcher.PropertiesToLoad.AddRange($colProplist)
$searcher.FindAll() | Foreach-Object {$_.Properties}
I suspect your foreach loop is outputting the result when calling PropertiesToLoad.Add.
Try piping to out-null, like so:
foreach ($i in $colPropList){
$objSearcher.PropertiesToLoad.Add($i) | out-null
}