So i have the below script which scans a drive for folders, it then pulls in a csv with folder names and folder owners and then matches them and outputs to HTML.
I am looking for a way to within this use PS to look up the users names in the csv grab their email address from AD and then in the output of the HTML put them as mailto code.
function name($filename, $folderowners, $directory, $output){
$server = hostname
$date = Get-Date -format "dd-MMM-yyyy HH:mm"
$a = "<style>"
$a = $a + "TABLE{border-width: 1px;border-style: solid;border-color:black;}"
$a = $a + "Table{background-color:#ffffff;border-collapse: collapse;}"
$a = $a + "TH{border-width:1px;padding:0px;border-style:solid;border-color:black;}"
$a = $a + "TR{border-width:1px;padding-left:5px;border-style:solid;border-
color:black;}"
$a = $a + "TD{border-width:1px;padding-left:5px;border-style:solid;border-color:black;}"
$a = $a + "body{ font-family:Calibri; font-size:11pt;}"
$a = $a + "</style>"
$c = " <br></br> Content"
$b = Import-Csv $folderowners
$mappings = #{}
$b | % { $mappings.Add($_.FolderName, $_.Owner) }
Get-ChildItem $directory | where {$_.PSIsContainer -eq $True} | select Name,
#{n="Owner";e={$mappings[$_.Name]}} | sort -property Name |
ConvertTo-Html -head $a -PostContent $c |
Out-File $output
}
name "gdrive" "\\server\location\gdrive.csv" "\\server\location$"
"\\server\location\gdrive.html"
Try adding something like this to the select:
#{n="email";e={"mailto:"+((Get-ADUser $mappings[$_.Name] -Properties mail).mail)}
You need to load the ActiveDirectory module before you can use the Get-ADUser cmdlet:
Import-Module ActiveDirectory
On server versions this module can be installed via Server Manager or dism. On client versions you have to install the Remote Server Administration Tools before you can add the module under "Programs and Features".
Edit: I would have expected ConvertTo-Html to automatically create clickable links from mailto:user#example.com URIs, but apparently it doesn't. Since ConvertTo-Html automatically encodes angular brackets as HTML entities and I haven't found a way to prevent that, you also can't just pre-create the property as an HTML snippet. Something like this should work, though:
ConvertTo-Html -head $a -PostContent $c | % {
$_ -replace '(mailto:)([^<]*)', '$2'
} | Out-File $output
Here's how I would do it (avoiding the use of the AD Module, only because it's not on all of my workstations and this works just the same), and assuming you know the user name already:
#Setup Connection to Active Directory
$de = [ADSI]"LDAP://example.org:389/OU=Users,dc=example,dc=org"
$sr = New-Object System.DirectoryServices.DirectorySearcher($de)
After I setup a connection to AD, I set my LDAP search filter. This takes standard ldap query syntax.
#Set Properties of Search
$sr.SearchScope = [System.DirectoryServices.SearchScope]"Subtree"
$sr.Filter = "(&(ObjectClass=user)(samaccountname=$Username))"
I then execute the search.
#Grab user's information from OU. If search returns nothing, they are not a user and the script exits.
$SearchResults = $sr.FindAll()
if($SearchResults.Count -gt 0){
$emailAddr = $SearchResults[0].Properties["mail"]
$mailto = "Contact User"
}
You can of course send the $mailto variable anywhere you want, and change it's html, but hopefully this gets you started.
Related
I have the following code:
$html = New-Object -ComObject "HTMLFile"
$source = Get-Content -Path $FilePath -Raw
try
{
$html.IHTMLDocument2_write($source) 2> $null
}
catch
{
$encoded = [Text.Encoding]::Unicode.GetBytes($source)
$html.write($encoded)
}
$t = $html.getElementsByTagName("table") | Where-Object {
$cells = $_.tBodies[0].rows[0].cells
$cells[0].innerText -eq "Name" -and
$cells[1].innerText -eq "Description" -and
$cells[2].innerText -eq "Default Value" -and
$cells[3].innerText -eq "Release"
}
The code works fine on Windows Powershell 5.1, but on Powershell Core 7 $_.tBodies[0].rows returns null.
So, how does one access the rows of an HTML table in PS 7?
PowerShell (Core), as of 7.3.1, does not come with a built-in HTML parser - and this may never change.
You must rely on a third-party solution, such as the PowerHTML module that wraps the HTML Agility Pack.
The object model works differently than the Internet Explorer-based one available in Windows PowerShell; it is similar to the XML DOM provided by the standard System.Xml.XmlDocument type ([xml])[1]; see the documentation and the sample code below.
# Install the module on demand
If (-not (Get-Module -ErrorAction Ignore -ListAvailable PowerHTML)) {
Write-Verbose "Installing PowerHTML module for the current user..."
Install-Module PowerHTML -ErrorAction Stop
}
Import-Module -ErrorAction Stop PowerHTML
# Create a sample HTML file with a table with 2 columns.
Get-Item $HOME | Select-Object Name, Mode | ConvertTo-Html > sample.html
# Parse the HTML file into an HTML DOM.
$htmlDom = ConvertFrom-Html -Path sample.html
# Find a specific table by its column names, using an XPath
# query to iterate over all tables.
$table = $htmlDom.SelectNodes('//table') | Where-Object {
$headerRow = $_.Element('tr') # or $tbl.Elements('tr')[0]
# Filter by column names
$headerRow.ChildNodes[0].InnerText -eq 'Name' -and
$headerRow.ChildNodes[1].InnerText -eq 'Mode'
}
# Print the table's HTML text.
$table.InnerHtml
# Extract the first data row's first column value.
# Note: #(...) is required around .Elements() for indexing to work.
#($table.Elements('tr'))[1].ChildNodes[0].InnerText
A Windows-only alternative is to use the HTMLFile COM object, as shown in this answer, and as used in your own attempt - I'm unclear on why it didn't work in your specific case.
[1] Notably with respect to supporting XPath queries via the .SelectSingleNode() and .SelectNodes() methods, exposing child nodes via a .ChildNodes collection, and providing .InnerHtml / .OuterHtml / .InnerText properties. Instead of an indexer that supports child element names, methods .Element(<name>) and .Elements(<name>) are provided.
I used the answer above for my solution. I installed PowerHTML.
I wanted to extract the datatable from https://www.dicomlibrary.com/dicom/dicom-tags/ and convert them.
From this:
<tr><td>(0002,0000)</td><td>UL</td><td>File Meta Information Group Length</td><td></td></tr>
To this:
{"00020000", "ULFile Meta Information Group Length"}
$page = Invoke-WebRequest https://www.dicomlibrary.com/dicom/dicom-tags/
$htmldom = ConvertFrom-Html $page
$table = $htmlDom.SelectNodes('//table') | Where-Object {
$headerRow = $_.Element('tr') # or $tbl.Elements('tr')[0]
# Filter by column names
$headerRow.ChildNodes[0].InnerText -eq 'Tag'
}
foreach ($row in $table.SelectNodes('tr'))
{$a = $row.SelectSingleNode('td[1]').innerText.Trim() -replace "`n|`r|\s+", " " -replace "\(",'{"' -replace ",","" -replace "\)",'",'
$c = $row.SelectSingleNode('td[3]').innerText.Trim() -replace "`n|`r|\s+", " "
$b=$row.seletSingleNode('td[2]').innerText.Trim() -replace "`n|`r|\s+", ""; $c = '"'+$b+$c+'"},'
$row = New-Object -TypeName psobject
$row | Add-Member -MemberType NoteProperty -Name Tag -Value $a
$row | Add-Member -MemberType NoteProperty -Name Value -Value $c
[array]$data += $row
}
$data | Out-File c:\scripts\dd.txt
So I have a script that runs at logon to search for PST's on a users machine, then copies them to a holding area waiting for migration.
When the search/copy is complete it outputs to a CSV that looks something like this:
Hostname,User,Path,Size_in_MB,Creation,Last_Access,Copied
COMP1,user1,\\comp1\c$\Test PST.pst,20.58752,08/12/2015,08/12/2015,Client copied
COMP1,user1,\\comp1\c$\outlook\outlook.pst,100,08/12/2015,15,12,2015,In Use
The same logon script has an IF to import the CSV if the copied status is in use and makes further attempts at copying the PST into the holding area. If it's successful it exports the results to the CSV file.
My question is, is there anyway of getting it to either amend the existing CSV changing the copy status? I can get it to add the new line to the end, but not update.
This is my 'try again' script:
# imports line of csv where PST file is found to be in use
$PST_IN_USE = Import-CSV "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" | where { $_.copied -eq "In Use" }
ForEach ( $PST_USE in $PST_IN_USE )
{ $NAME = Get-ItemProperty $PST_IN_USE.Path | select -ExpandProperty Name
$NEW_NAME = $USER + "_" + $PST_IN_USE.Size_in_MB + "_" + $NAME
# attempts to copy the file to the pst staging area then rename it.
TRY { Copy-Item $PST_IN_USE.Path "\\comp4\TEMPPST\PST\$USER" -ErrorAction SilentlyContinue
Rename-Item "\\comp4\TEMPPST\PST\$USER\$NAME" -NewName $NEW_NAME
# edits the existing csv file replacing "In Use" with "Client Copied"
$PST_IN_USE.Copied -replace "In Use","Client Copied"
} # CLOSES TRY
# silences any errors.
CATCH { }
$PST_IN_USE | Export-Csv "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" -NoClobber -NoTypeInformation -Append
} # CLOSES ForEach ( $PST_USE in $PST_IN_USE )
This is the resulting CSV
Hostname,User,Path,Size_in_MB,Creation,Last_Access,Copied
COMP1,user1,\\comp1\c$\Test PST.pst,20.58752,08/12/2015,08/12/2015,Client copied
COMP1,user1,\\comp1\c$\outlook\outlook.pst,100,08/12/2015,15,12,2015,In Use
COMP1,user1,\\comp1\c$\outlook\outlook.pst,100,08/12/2015,15,12,2015,Client copied
It's almost certainly something really simple, but if it is, it's something I've yet to come across in my scripting. I'm mostly working in IF / ELSE land at the moment!
If you want to change the CSV file, you have to write it completely again, not just appending new lines. In your case this means:
# Get the data
$data = Import-Csv ...
# Get the 'In Use' entries
$inUse = $data | where Copied -eq 'In Use'
foreach ($x in $inUse) {
...
$x.Copied = 'Client Copied'
}
# Write the file again
$data | Export-Csv ...
The point here is, you grab all the lines from the CSV, modify those that you process and then write the complete collection back to the file again.
I've cracked it. It's almost certainly a long winded way of doing it, but it works and is relatively clean too.
#imports line of csv where PST file is found to be in use
$PST_IN_USE = Import-CSV "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" | where { $_.copied -eq "In Use" }
$PST_IN_USE | select -ExpandProperty path | foreach {
# name of pst
$NAME = Get-ItemProperty $_ | select -ExpandProperty Name
# size of pst in MB without decimals
$SIZE = Get-ItemProperty $_ | select -ExpandProperty length | foreach { $_ / 1000000 }
# path of pst
$PATH = $_
# new name of pst when copied to the destination
$NEW_NAME = $USER + "_" + $SIZE + "_" + $NAME
TRY { Copy-Item $_ "\\comp4\TEMPPST\PST\$USER" -ErrorAction SilentlyContinue
TRY { Rename-Item "\\comp4\TEMPPST\PST\$USER\$NAME" -NewName $NEW_NAME -ErrorAction SilentlyContinue | Out-Null }
CATCH { $NEW_NAME = "Duplicate exists" }
$COPIED = "Client copied" }
CATCH { $COPIED = "In use" ; $NEW_NAME = " " }
$NEW_FILE = Test-Path "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 4.csv"
IF ( $NEW_FILE -eq $FALSE )
{ "Hostname,User,Path,Size_in_MB,Creation,Last_Access,Copied,New_Name" |
Set-Content "\\lccfp1\TEMPPST\PST\$HOSTNAME - $USER 4.csv" }
"$HOSTNAME,$USER,$PATH,$SIZE,$CREATION,$LASTACCESS,$COPIED,$NEW_NAME" |
Add-Content "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 4.csv"
} # CLOSES FOREACH #
$a = Import-CSV "\\comp4\TEMPPST\PST\$HOSTNAME - $USER.csv" | where { $_.copied -ne "in use" }
$b = Import-Csv "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 4.csv"
$a + $b | export-csv "\\comp4\TEMPPST\PST\$HOSTNAME - $USER 8.csv" -NoClobber -NoTypeInformation
Thanks for the help. Sometimes it takes a moments break and a large cup of coffee to see things a different way.
I have the following:
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Monday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Tuesday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Wednesday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Thursday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Friday.csv) |
sort first_name,last_name,phone1 -Unique |
Export-Csv C:\Users\Administrator\Desktop\dbs\joined.csv
Import-Module ActiveDirectory
#EDIT PATH SO IT POINTS TO DB FILE \/
$newUserList = Import-Csv C:\Users\Administrator\Desktop\dbs\joined.csv
ForEach ($item in $newUserList){
$fname = $($item.first_name)
$lname = $($item.last_name)
$phone = $($item.phone1)
$username=$fname+$lname.substring(0,1)
# Puts Domain name into a Placeholder.
$domain='#csilab.local'
# Build the User Principal Name Username with Domain added to it
$UPN=$username+$domain
# Create the Displayname
$Name=$fname+" "+$lname
$newusers1 = (New-ADUser -GivenName $fname -Surname $lname -HomePhone $phone -Name $Name -DisplayName $Name -SamAccountName $username -AccountPassword (ConvertTo-SecureString "1NewPassword" -asplaintext -force) -ChangePasswordAtLogon $true -UserPrincipalName $UPN -Path "ou=test,dc=csi,dc=lab" -Enabled $true -PassThru) |
# I need this block to check for duplicates missed by the csv sort & merge
# as well as any in the destination OU itself as the script will run daily
# with inevitable possibility that user is unique to the csv but not the AD.
$newusers1 | Get-ADUser -Filter * -SearchBase "OU=Active Users,DC=csilab,DC=local" |
Sort-Object -Unique |
Remove-ADUser -confirm:$false
However I run it and get:
Get-ADUser : The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input or the input and its
properties do not match any of the parameters that take pipeline input.
At C:\Users\Administrator\Desktop\Team2.ps1:40 char:14
+ $newusers1 | Get-ADUser -Filter * -SearchBase "OU=Active Users,DC=csilab,DC=loca ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (CN=Bethanie Cut...csilab,dc=local:PSObject) [Get-ADUser], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,Microsoft.ActiveDirectory.Management.Commands.GetADUser
I also worry even if it did work that it'd delete the unique users instead of duplicates.
get-AdUser $username | Move-ADObject -TargetPath 'OU=Active Users,dc=csilab,dc=local'
}
What can I do to ensure all users are there without any originals getting deleted, just the duplicates?
You still have an empty pipe at the end of your New-ADUser statement, which would cause your script to fail with an "empty pipe element is not allowed" error, but oh well ...
To avoid collisions just check if an account already exists before you try to create it, and create it only if it doesn't:
$username = $fname + $lname.substring(0,1)
...
if (Get-ADUser -Filter "SamAccountName -eq '$username'") {
Write-Host "Account $username already exists."
} else {
New-ADUser -SamAccountName $username -Name $Name ... -PassThru
}
Also, you're overcomplicating the CSV handling. Simply process a list of the files via ForEach-Object:
$domain = '#csilab.local'
Set-Location 'C:\Users\Administrator\Desktop\dbs'
'Monday.csv', 'Tuesday.csv', 'Wednesday.csv', 'Thursday.csv', 'Friday.csv' |
ForEach-Object { Import-Csv $_ } |
Sort-Object first_name, last_name, phone1 -Unique |
ForEach-Object {
$fname = $_.first_name
$lname = $_.last_name
$phone = $_.phone1
...
}
You might want to use Try/Catch:
Try {$newusers1=(New-ADUser–GivenName$fname–Surname$lname-HomePhone$phone–Name$Name-DisplayName $Name –SamAccountName$username–AccountPassword(ConvertTo-SecureString"1NewPassword"-asplaintext-force) -ChangePasswordAtLogon$true–UserPrincipalName$UPN–Path"dc=csi,dc=lab"-Enabled$true)}
Catch {
If ($_.Exception.Message -match "account already exists")
{
#do whatever here, eg $NewUsers1 = Get-ADUser $Name
}
}
Also, if you can't see the user when browsing via ADUC it could be that you are connected to a different DC.
As mentioned above, the newuser1 variable will be null if the command failed. It will not load with the other user automatically, and it would be scary bad if it did. You need to decide what to do if the account already exist, that may simply be loading the variable with the other account, or doing something like appending a "1" to $name and retrying the command.
I've been trying to write a script that pings a list of computers from a text file and exports the output to a HTML file.
Using a ForEach loop and and if/else statement I have been able to get a working ping script working that displays in PowerShell but haven't been able to export the results to a html file.
When I run the script the HTML file opens but only displays the line "Here are the ping results for $date"
I'm pretty new to PowerShell so any kind of input or help would be appreciated!
$ComputersAry = Get-Content -Path "C:\Script\ping.txt"
$filepath = "C:\Script\"
$date = "{0:yyy_MM_dd-HH_mm}" -f (get-date)
$file = $filepath + "Results_" + $date + ".htm"
New-Item $filepath -type directory -force -Verbose
$Header = #"
<style>
TABLE {border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}
TH {border-width: 1px;padding: 3px;border-style: solid;border-color: black;background-color: #6495ED;}
TD {border-width: 1px;padding: 3px;border-style: solid;border-color: black;}
</style>
<title>
LRS Ping Results
</title>
"#
Foreach ($MachineName in $ComputersAry) {
$PingStatus = Gwmi Win32_PingStatus -Filter "Address ='$MachineName'" | Select-Object StatusCode
if($PingStatus.StatusCode -eq 0){
$output = write-host "$MachineName,Ping Success!!,$Date"
} else {
$output = write-host "$MachineName,Ping FAIL, please investigate cause ASAP!!"
}
}
$pre= "Here are the ping results for $date"
$output | Select-Object Name, Status, Date | ConvertTo-HTML -Head $Header -PreContent $pre | Out-File $file
Invoke-Item $file
Nothing gets assigned to $output when you use Write-Host. Try this instead:
$output = "$MachineName,Ping Success!!,$Date"
...
$output = "$MachineName,Ping FAIL, please investigate cause ASAP!!"
Write-Host tells PowerShell to write directly to the host's display. This bypasses the Output (stdout) stream. While you could replace Write-Host with Write-Output almost no one uses Write-Output because the default stream every goes to is the output stream anyway. So when a string like "Hello World" reaches the end of the pipeline and there isn't an Out-File or Out-Printer it will by default get stuck into the Output stream and when the result of the pipeline execution is assigned to a variable, it gets whatever is in the output stream.
Try doing the following, save the code as a script and run it.
PS C:\Scripts> .\Demo.ps1 | ConvertTo-Html | Out-File C:\Scripts\out.htm
$result = '' | Select Online
$ComputersAry = GC C:\Scripts\2.txt
Foreach ($MachineName in $ComputersAry) {
$PingStatus = Gwmi Win32_PingStatus -Filter "Address ='$MachineName'" | Select-Object StatusCode
if($PingStatus.StatusCode -eq 0){
$result.Online = "$MachineName,Ping Success!!"
} else {
$result.Online = "$MachineName,Ping FAIL, please investigate cause ASAP!!"
}
$result
}
I'm trying to take input from a CSV file, which has a list of group names (canonical names) and get the Distinguished Name from it, then output to another CSV file. The code:
#get input file if passed
Param($InputFile)
#Set global variable to null
$WasError = $null
#Prompt for file name if not already provided
If ($InputFile -eq $NULL) {
$InputFile = Read-Host "Enter the name of the input CSV file (file must have header of 'Group')"
}
#Import Active Directory module
Import-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
$DistinguishedNames = Import-Csv -Path $InputFile -Header Group | foreach-Object {
$GN = $_.Group
$DN = Get-ADGroup -Identity $GN | Select DistinguishedName
}
$FileName = "RESULT_Get-DistinguishedNames" + ".csv"
#Export list to CSV
$DNarray | Export-Csv -Path $FileName -NoTypeInformation
I've tried multiple solutions, and none have seemed to work. Currently, it throws an error because
Cannot validate argument on parameter 'Identity'. The argument is null. Supply a non-null argument and try the command again.
I tried using -Filter also, and in a previous attempt I used this code:
Param($InputFile)
#Set global variable to null
$WasError = $null
#Prompt for file name if not already provided
If ($InputFile -eq $NULL) {
$InputFile = Read-Host "Enter the name of the input CSV file(file must have header of 'GroupName')"
}
#Import Active Directory module
Import-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
$DistinguishedNames = Import-Csv -Path $InputFile | foreach {
$strFilter = "*"
$Root = [ADSI]"GC://$($objDomain.Name)"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher($root)
$objSearcher.Filter = $strFilter
$objSearcher.PageSize = 1000
$objsearcher.PropertiesToLoad.Add("distinguishedname") | Out-Null
$objcolresults = $objsearcher.FindAll()
$objitem = $objcolresults.Properties
[string]$objDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
[string]$DN = $objitem.distinguishedname
[string]$GN = $objitem.groupname
#Get group info and add mgr ID and Display Name
$props = #{'Group Name'= $GN;'Domain' = $objDomain;'Distinguished Name' = $DN;}
$DNS = New-Object psobject -Property $props
}
$FileName = "RESULT_Get-DistinguishedNames" + ".csv"
#Export list to CSV
$DistinguishedNames | Sort Name | Export-Csv $FileName -NoTypeInformation
The filter isn't the same one I was using here, I can't find the one I was using, the I currently have is a broken attempt.
Anyway, the main issue I was having is that it will get the group name, but search for it in the wrong domain (it wouldn't include Organizational Units) which caused none of them to be found. When I search for a group in PowerShell though (using Get-ADGroup ADMIN) they show up with the correct DN and everything. Any hints or code samples are appreciated.
You seemingly miss the point of $variable = cmdlet|foreach {script-block} assignment. The objects to assign to $variable should be returned (passed through the script block) in order to end up in $variable. Both your main loops contain the structure of the line $somevar=expectedOutput where expectedOutput is either a New-Object psobject or Get-ADGroup call. The assignment to $someVar suppresses the output, so that the script block does not have anything to return, and $variable remains null. To fix, do not prepend the call that should return an object into outside variable with an assignment.
$DistinguishedNames = Import-Csv -Path $InputFile -Header Group | foreach-Object {
$GN = $_.Group
Get-ADGroup -Identity $GN | Select DistinguishedName # drop '$DN=`
}
$DistinguishedNames | Export-CSV -Path $FileName -NoTypeInformation
The same issue with the second script.