I'm trying to modify this PowerShell script to allow input of users from a text or CSV file. I've pulled the script directly from Tim Rhymer.
This is the original script:
Import-Module ActiveDirectory
function Get-ADUsersLastLogon() {
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$users = Get-ADUser -Filter *
$time = 0
$exportFilePath = "c:lastLogon.csv"
$columns = "name,username,datetime"
Out-File -FilePath $exportFilePath -Force -InputObject $columns
foreach ($user in $users) {
foreach ($dc in $dcs) {
$hostname = $dc.HostName
$currentUser = Get-ADUser $user.SamAccountName | Get-ADObject -Server $hostname -Properties lastLogon
if ($currentUser.LastLogon -gt $time) {
$time = $currentUser.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
$row = $user.Name + "," + $user.SamAccountName + "," + $dt
Out-File -FilePath $exportFilePath -Append -NoClobber -InputObject $row
$time = 0
}
}
Get-ADUsersLastLogon
I'm thinking I should modify the portion of the script the sets the variable of $user and how the foreach ($user in $users) loop processes each user, but can't figure it out.
As for this...
allow input of users from a text or CSV file.
… that is all that this is...
$users = Get-ADUser -Filter *
… You just replace that with your file
$users = Import-Csv -Path $UncToUserList.csv
Yet, by your question, since this is a really common, basic PowerShell thing, I am assuming you are very new to this and would advise really ramping up on PowerShell using all the freely available learning resources on TechNet MVA, MSChannel9 and YouTube, as well as freely available eBooks, as to avoid much confusion and frustration in your learning cycles as possible.
Also, you should never run any untrusted code, or code you do not fully understand. Use only in a test environment that you can fully recover from before ever considering production runs to avoid things that can cause you to have an RPE error --- 'Resume Producing Event'.
Related
I am beginner in scripting with powershell. My boss asked me to create a script that will get information about the last logon from every user in our domain on every DC. I have created the following script:
Import-Module ActiveDirectory
function Get-ADUsersLastLogon() {
$dcs = Get-ADDomainController -Filter { Name -like "*" }
$users = Get-ADUser -Filter * -Properties LastLogonDate | Where-Object { $_.LastLogonDate -le (Get-Date).AddDays(-30) }
$time = 0
$exportFilePath = "C:\output\aduser.csv"
$columns = "name,username,datetime,domain controller,enabled"
Out-File -filepath $exportFilePath -force -InputObject $columns
foreach ($user in $users) {
$Activeuser = $user.Enabled
foreach ($dc in $dcs) {
$hostname = $dc.HostName
$currentUser = Get-ADUser $user.SamAccountName | Get-ADObject -Server $hostname -Properties lastLogon
if ($currentUser.LastLogon -gt $time) {
$time = $currentUser.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
$row = $user.Name + "," + $user.SamAccountName + "," + $dt + "," + $hostname + "," + $Activeuser
Out-File -filepath $exportFilePath -append -noclobber -InputObject $row
$time = 0
}
}
Get-ADUsersLastLogon
My boss asked me to change the following things in this script:
the output of the domain controller is only our DC in an other country. I want to know which DC the user last logged into.
The running of the script is taking too long. Like half a day. Is it possible to make this faster?
I hope someone can help me with this, I tried a few things, but I didn't work :(
the output of the domain controller is only our DC in an other country. I want to know which DC the user last logged into.
It looks like you've already solved this by querying each DC for each user object.
The running of the script is taking too long. Like half a day. Is it possible to make this faster?
That's because you're querying each DC for each user object :)
One way of speeding it up would be to flip your query logic around - query each DC for all the users but only once, and then update the logon timestamps only when a newer one is encountered.
For that to work, you need to keep track of the highest timestamp seen for each user. I'd suggest using a simple hashtable to index the user accounts by their user name:
$dcs = Get-ADDomainController -Filter { Name -like "*" }
# create hashtable to keep track of latest timestamps per user
$userLastLogonTable = #{}
foreach($dc in $dcs){
# fetch all users from each DC
Get-ADUser -Filter * -Properties LastLogonDate -Server $dc | ForEach-Object {
# Only add new timestamps to table if we either haven't seen the username before, or if the timestamp is newer than the current
if(-not $userLastLogonTable.Contains($_.SAMAccountName) -or $userLastLogonTable[$_.SAMAccountName].LastLogonDate -lt $_.LastLogonDate){
$userLastLogonTable[$_.SAMAccountName] = [pscustomobject]#{
LastLogonDate = $_.LastLogonDate
LogonServer = $dc.Name
}
}
}
}
# Now that we have a complete table of all users and their last logon timestamp,
# we can then easily identify usernames that have no recent logons
$staleUserNames = $userLastLogonTable.PSBase.Keys |Where-Object { $userLastLogonTable[$_].LastLogonDate -le (Get-Date).AddDays(-30) }
$staleUserNames now contain the user names of all user accounts that have not logged in for 30 days of more.
I am writing a script to pull a list of users and then archive that list of users mailboxes and archives into psts
$Lines = Get-ADUser -Filter * -Properties EmailAddress
$Lines | select EmailAddress | Where {$_.EmailAddress -ne $null} | Out-File "C:\T2\EMailAddresses.txt"
(Get-Content "C:\T2\EMailAddresses.txt") | ForEach { $_.Trim() } | Set-Content "C:\T2\EMailAddresses.txt"
$Names = Get-Content "C:\T2\EMailAddresses.txt"
$Emails = $Names.Split("#")[0]
Add-PSSnapIn -Name Microsoft.Exchange, Microsoft.Windows.AD
$SubPath1 = "\\qnap1\archive\exchange_Migration\"
$SubPath2 = "_Primary_mailbox.pst"
$SubPath3 = "_Archive_mailbox.pst"
ForEach ($Email in $Emails) { New-MailboxExportRequest -Mailbox $Email -FilePath "$Subpath1 + $Email.Split("#")[0] + $SubPath2" ; New-MailboxExportRequest -Mailbox $Email.Split("#")[0] -FilePath "$Subpath1 + $Email + $SubPath3" -IsArchive }
Where I am strugglng is getting the variables correct. I want to use the full e-mail address with the export request command but only the name part of the email (not domain) in the path for the export
I think there's a few things going on. When you are unrolling the .Split("#") on the $Names array you are then indexing into it with [0]. That's going to return the first element of the array that resulted from the split. NOT the first element resulting from each split...
But there's not a need for that anyhow. You could simply a few ways, one example:
Add-PSSnapIn -Name Microsoft.Exchange, Microsoft.Windows.AD
$Lines = Get-ADUser -Filter "EmailAddress -like '*'" -Properties EmailAddress
$Lines |
Select-Object -ExpandProperty EmailAddress |
Out-File "C:\T2\EMailAddresses.txt"
(Get-Content "C:\T2\EMailAddresses.txt").Trim() |
Set-Content "C:\T2\EMailAddresses.txt"
$Emails = Get-Content "C:\T2\EMailAddresses.txt"
$SubPath1 = "\\qnap1\archive\exchange_Migration\"
$SubPath2 = "_Primary_mailbox.pst"
$SubPath3 = "_Archive_mailbox.pst"
ForEach ($Email in $Emails)
{
$Alias = $Email.Split("#")[0]
$FilePathPrimary = "$Subpath1" + $Alias + $SubPath2
$FilePathArchive = "$Subpath1" + $Alias + $SubPath3
New-MailboxExportRequest -Mailbox $Email -FilePath $FilePathPrimary
New-MailboxExportRequest -Mailbox $Email -FilePath $FilePathArchive -IsArchive
}
Note: I obviously can't test this.
You can use emails directly, then simply calculate the arguments for your export commands on each loop iteration. This comes out cleaner and easier to read.
Also Notice:
Use of Select-Object -ExpandProperty this will return just the email addresses for the file.
Unrolling the .Trim() method on the results of Get-Content instead of running it through a ForEach-Object loop.
Moving filter criteria left in the command/pipeline by leveraging the -Filter parameter of Get-ADUser, which is a performance best practice. Especially when commands have their own and/or adequate filtering capabilities to do so.
An Aside:
There are a few other things that can be done to simplify. However, the way this is presented thus far, there's actually no need to intermediate the data with files. So I'm going to skip making enhancements in that area. Instead you could take the output from Get-ADUser and use it directly. Again, lot's of ways to do it, but building off what we already have, here's one example:
Add-PSSnapIn -Name Microsoft.Exchange, Microsoft.Windows.AD
$SubPath1 = "\\qnap1\archive\exchange_Migration\"
$SubPath2 = "_Primary_mailbox.pst"
$SubPath3 = "_Archive_mailbox.pst"
$Emails = (Get-ADUser -Filter "EmailAddress -like '*'" -Properties EmailAddress).EMailAddresses
ForEach($Email in $Emails)
{
$Alias = $Email.Split("#")[0]
$FilePathPrimary = "$Subpath1" + $Alias + $SubPath2
$FilePathArchive = "$Subpath1" + $Alias + $SubPath3
New-MailboxExportRequest -Mailbox $Email -FilePath $FilePathPrimary
New-MailboxExportRequest -Mailbox $Email -FilePath $FilePathArchive -IsArchive
}
Warning:
Beyond the scope of the question, but all the code so far assumes the mailbox indeed has an enabled archive. If or when that is not the case the attempt to export the archive will likely throw up a bloody red error. You can handle that by introducing some error handling. Or, you might consider rewriting by using Get-Mailbox from the Exchange tools instead of Get-ADUser
I'm currently trying to query the domain controllers to find the latest logon date for users. I know I can use the lastlogondate but that doesn't get replicated as often as I'd like and I have no control over the that. I tried making a script to query each domain controller and check it against a hashtable of users but this is proving to be really slow to even query one DC. I know I could run background jobs for each DC but still just going all the latest logon times for one domain controller is taking a long time. Any advice?
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$users = Get-ADUser -filter * -SearchBase "OU=users,DC=com"
$outfile = "c:users.csv"
$usertable = #{}
# creating table for all users
foreach($user in $users) {
$userobject = New-Object PSObject
$userobject | Add-Member -MemberType NoteProperty -Name DomainController -Value "0"
$userobject | Add-Member -MemberType NoteProperty -Name LastLogon -Value "0"
$usertable.Add($user.SamAccountName ,$userObject)
} # end foreach
# Method for looping through domain controllers to find last logon
foreach($dc in $dcs) {
$hostname = $dc.HostName
$logons = Get-ADUser -Filter * -SearchBase "OU=users,DC=com" -Property LastLogon -Server $hostname
foreach($u in $logons) {
$sam = $u.SamAccountName
if($u.LastLogon -gt $usertable.$sam.LastLogon) {
$usertable.$sam.LastLogon = $u.LastLogon
$usertable.$sam.DomainController = $dc.HostName
Write-Host "$sam has the latest date"
$usertable.$sam.DomainController
}
} #end inner foreach
} #end outer domain foreach
foreach($h in $usertable.GetEnumerator()) {
$sam = $($h.Name)
$newdate = $($h.Value).LastLogon
$append = "$sam,$newdate"
$append | out-file $outfile -Encoding ascii -Force -Append
}
I think I've figured this out. It's very clear from testing and comparing with my own similar program that the delay is in the following loop:
foreach($u in $logons) {
$sam = $u.SamAccountName
if($u.LastLogon -gt $usertable.$sam.LastLogon) {
$usertable.$sam.LastLogon = $u.LastLogon
$usertable.$sam.DomainController = $dc.HostName
Write-Host "$sam has the latest date"
$usertable.$sam.DomainController
}
} # end inner foreach
Now my program is not exactly the same as yours. One could sanitize this, but for all intents and purposes the below segment serves the same function as the above excerpt:
# Note: the re-use of $DC...
ForEach($DC in $RDCs )
{ # Should you add any measurements here??
Write-Host "Working on $DC : $(Get-Date -Format G) ..."
Get-ADUser -Filter $Filter -SearchBase $SearchBase -Server $DC -Properties $AD_Props |
ForEach-Object{
If ( $Users.Contains( $_.samAccountName ) )
{ # So you don't attemp when it wasn't in the root set.
If( $_.lastlogon -gt $Users[$_.samAccountName].LastLogon )
{
$Users[$_.samAccountName].LastLogon = $_.lastlogon
$Users[$_.samAccountName].DC = $DC
}
}
}
}
You can see that I'm using a ForEach-Object loop instead of the ForEach construct. However, this isn't a sufficient explanation, especially seeing as the latter is often faster.
Note: I timed your loop in several different ways and accounted for code earlier in the script. For example, I'm not creating the objects the same way, but I ruled that out as a factor.
Not able to figure out why your loop would be "dramatically" slower I relucantly rewrote it to:
$logons |
ForEach-Object{
If( $_.LastLogon -gt $usertable[$_.samAccountName].LastLogon )
{
$usertable[$_.samAccountName].LastLogon = $_.LastLogon
$usertable[$_.samAccountName].DomainController = $hostname
However, this didn't seem much faster if at all. So, even more reluctantly I tried it like:
Get-ADUser -Filter * -SearchBase $UsersOU -Property LastLogon -Server $hostname |
ForEach-Object{
If( $_.LastLogon -gt $usertable[$_.samAccountName].LastLogon )
{
$usertable[$_.samAccountName].LastLogon = $_.LastLogon
$usertable[$_.samAccountName].DomainController = $hostname
}
}
Now I started seeing performance similar to my own program. In fact, it seemed a little faster, probably due to not checking the Hash.
Based on experience I can only hypothesize that the objects are live and maintaining some kind of connection back to the source DC. I've seen this type of thing once before in the PowerShell Community Extensions (PSCX) the objects returned from Get-TerminalSession exibited similar behavior. I could be crazy, I'm happy to be corrected and frankly hoping someone has a better explanation.
There are a few other things I can advise here, none as impactful as above. Please also note, I didn't test through to the end, and so can't guarantee there aren't other issues.
I'm trying to Get the Name, Manufacturer, and model of computers so i can distinguish what computers are out of warranty in AD.
I'm trying to do this by getting the computer names and putting there info into the corresponding .csv file but this fails and puts 1 ou to multiple .csv files and then moves to the second ou and does the same thing?
$myMultiArray = #(("OU=Domain Controllers,DC=FABRIKAM,DC=COM"),
("OU=Computers,DC=FABRIKAM,DC=COM"))
$myFileArray = #(("D:\VS-Code\Powershell\AD_Computer_Management\OUs\Domain
Controllers.csv"),("D:\VS-
Code\Powershell\AD_Computer_Management\OUs\Computers.csv"))
foreach ($MultiOU in $myMultiArray) {
Get-ADComputer -Filter * -SearchBase $MultiOU -SearchScope 2 | Select-object Name | Out-File -FilePath "D:\VS-Code\Powershell\AD_Computer_Management\OUs\garbage.csv"
For ($i = 0; $i – $myFileArray.Length - 1; $i++) {
Write-Host $myMultiArray[$i]
[string[]]$cnArray = Get-Content -Path 'D:\VS-Code\Powershell\AD_Computer_Management\OUs\garbage.csv'
Write-Host $OU
if ($i -eq $i) {
foreach($CN in $cnArray){
Get-WmiObject -Class:Win32_ComputerSystem -ComputerName $OU | Format-List -Property Name, Manufacturer, Model | Out-File -FilePath $myFileArray[$1]
}
}
}
}
I've tried multiple variations of different loops and if statements.
I think there are two things:
Out-File -FilePath $myFileArray[$1]
Should be:
Out-File -FilePath $myFileArray[$i]
And also you might need to append:
Out-File -FilePath $myFileArray[$i] -Append
There are a couple of things wrong in your code, like $i – $myFileArray.Length, which should be $i –lt $myFileArray.Length.
Then there is Out-File -FilePath $myFileArray[$1] as Bernard Moeskops already mentioned.
Also your code seems to want to create both the Domain Controllers.csv aswell as the Computers.csv files regardless of the OU you are currently in.
Lastly, you are using Out-File to create the CSV files where for proper CSV output, you should use the Export-Csv cmdlet.
The following code should do what you want:
$myOUArray = "OU=Domain Controllers,DC=FABRIKAM,DC=COM", "OU=Computers,DC=FABRIKAM,DC=COM"
$myFilePath = "D:\VS-Code\Powershell\AD_Computer_Management\OUs" # just the path for the output files is needed
foreach ($OU in $myOUArray) {
# determine the file name from the OU we're in
$fileName = if ($OU -match 'OU=Domain Controllers') { 'Domain Controllers.csv' } else { 'Computers.csv'}
$filePath = Join-Path -Path $myFilePath -ChildPath $fileName
Write-Host "Getting computer info from OU '$OU'"
# get a string array of the computernames found in the OU
$computers = Get-ADComputer -Filter * -SearchBase $OU -SearchScope Subtree | Select-Object -ExpandProperty Name
# loop through this array to get the properties you want for
# each computer and store that as objects in the $result variable
$result = foreach($machine in $computers){
Get-WmiObject -Class:Win32_ComputerSystem -ComputerName $machine | Select-Object -Property Name, Manufacturer, Model
}
Write-Host "Creating file '$filePath'"
# save the CSV file to disk
$result | Export-Csv -Path $filePath -NoTypeInformation -Force
}
I have pasted my code below and pulled out everything that is already working, so I only have the part that isn't working as intended.
I am trying to put the EmployeeID, from a csv, in front of the Description field in AD. I can get that part to work, but the beginning of the If statement where I try to check if the $ID is already in the description fails; it just keeps adding it every time the script runs.
I have tried making both the $ID and $Description type as string with Out-String, and I have left that out, but it's the same result. I have tried -notcontains, -notmatch, and -notlike (which I believe is the correct one to use), but none work. I have even put my variables in a text file to make sure they are pulling the correct information.
I am still learning all of the intricacies of Powershell. Can anyone see what I'm doing wrong?
# Get script Start Time (used to measure run time)
$startDTM = (Get-Date)
#Null out variables
$Users = $Null
$ID = $Null
$Users = Import-Csv .\ImportADUsers\Test-Import-user-data.csv
Import-Module ActiveDirectory
$path = Split-Path -parent ".\ImportADUsers\*.*"
#Create log date
$logdate = Get-Date -Format yyyy-MM-dd-THH.mm.ss
$logfile = $path + "\logs\$logdate.logfile.txt"
# Enumerate the users, one line at a time.
# This assumes the first line is a header line defining the fields.
ForEach ($User In $Users)
{
# Retrieve values from the csv.
$ID = $User.HRRef
# Retrieve the sAMAccountName of the user from AD.
$UserDN = (Get-ADUser -LDAPFilter "(employeeID=$ID)").sAMAccountName
$ID | Out-File $logfile -Append
$IDString = $ID | Out-String
#Retrieve the Description of the user from AD.
$Description = Get-ADUser -Identity $UserDN -Properties description
$Description = $Description.description | Out-String
$Description | Out-File $logfile -Append
# Make sure there is only one user with this employeeID.
If ($UserDN.Count -eq 1)
{
IF ($Description -notlike $IDString) {Set-ADUser -Identity $UserDN
-Description "$($ID) - $($Description)" }
}
Else {"User with ID $ID either not found, or more than one user found."
| Out-File $logfile -Append}
#Log error for users that are not in Active Directory or EmployeeID
#found more than once
}
#Finish
#The lines below calculates how long it takes to run this script
# Get End Time
$endDTM = (Get-Date)
# Echo Time elapsed
"Elapsed Time: $(($endDTM-$startDTM).totalminutes) minutes"
#Append the minutes value to the text file
"Import took $(($endDTM-$startDTM).totalminutes) minutes to complete." |
Out-File $logfile -Append
#SCRIPT ENDS
Your string comparison is incorrect. Below is how to fix it.
The change: -notlike $IDString => -notlike "*$ID*"
ForEach ($User In $Users)
{
# Retrieve values from the csv.
$ID = $User.HRRef
$ID | Out-File $logfile -Append
# Retrieve the SAMAccountName of the user from AD.
$UserDN = (Get-ADUser -LDAPFilter "(employeeID=$ID)").SAMAccountName
#Retrieve the Description of the user from AD.
$Description = (Get-ADUser -Identity $UserDN -Properties description).Description
$Description | Out-File $logfile -Append
# Make sure there is only one user with this employeeID.
If ($UserDN.Count -eq 1 -and $Description -notlike "*$IDString*")
{
Set-ADUser -Identity $UserDN -Description "$ID - $Description"
}
Else
{
"User with ID $ID either not found, or more than one user found." | Out-File $logfile -Append
}
}