Getting unassigned phone numbers for Skype for Business in powershell - powershell

Has anyone ever done something like this in powershell? I'm automating new user creation and have to set up their skype for business accounts. Here's what I have so far:
<# set up skype for business #>
Enable-CsUser -Identity $FULLNAME `
-RegistrarPool REDACTED
-SipAddress $SIP
Set-CsUser $FULLNAME `
-EnterpriseVoiceEnabled $true `
-ExchangeArchivingPolicy Uninitialized `
-LineURI $PHONENUMBER
# only for driver manager/managers
Grant-CsConferencingPolicy $FULLNAME `
-PolicyName $ConferencingPolicy
Grant-CsExternalAccessPolicy -identity $FULLNAME `
-PolicyName 'Allow external access'
All I have left is the to assign the LineURI, and I can't seem to find anything about how to do that. I did, however, find a script that does this, but it doesn't seem like it's specific to my needs. My question is this: how can I query Skype for Business in powershell to grab the first unassigned number and assign it as the LineURI? I have been using New-CSUnassignedNumber but cannot seem to make any headway.

SfB doesn't keep a record of your number ranges, so it doesn't know if you assigning 1299 is the last in a range, or if the range stretches into the 1300's
Get-CsUser | ? LineUri | Select LineUri | Sort
this will give you a sorted list of all assigned uri's in your organization, so you'll be able to pick out ones which may be assignable, regardless you can use a number of methods to find what you're looking for, but you may be interested in this for the longer-term approach, it's a script which takes in your number ranges, and outputs a list of the used and available ones in a nice Csv.
#region Input
$NumRangeKeyTable = #{
#Make sure numbers are in the same format as they are in Skype,
#Do not include any ';ext' or 'tel:' etc. formatting!
#Put single numbers in the format:
# "+35313456789" = ""
#Put number ranges in the format:
# "+35313456700" = "+35313456799"
"+35313456789" = ""
"+35313456700" = "+35313456799"
}
#Save Location, set to $null to be prompted for location.
$FileName = $null
#endregion
#region Code
#region Helper Functions
Function Get-CsAssignedURIs {
$AllNumbers = #()
$Users = Get-CsUser
$Users | ? {$_.LineURI -ne ""} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "User" }}
$Users | ? {$_.PrivateLine -ne ""} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.PrivateLine ; Type = "PrivateLine" }}
Get-CsRgsWorkflow | Where-Object {$_.LineURI -ne ""} | Select Name,LineURI | %{$AllNumbers += New-Object PSObject -Property #{Name = $_.Name ; SipAddress = $_.PrimaryUri ; Number = $_.LineURI ; Type = "Workflow" }}
Get-CsCommonAreaPhone -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "CommonArea" }}
Get-CsAnalogDevice -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "AnalogDevice" }}
Get-CsExUmContact -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "ExUmContact" }}
Get-CsDialInConferencingAccessNumber -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.PrimaryUri ; Number = $_.LineURI ; Type = "DialInAccess" }}
Get-CsTrustedApplicationEndpoint -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "ApplicationEndpoint" }}
Return $AllNumbers
}
function Get-UniqueExt {
Param(
[string]$Uri1,
[string]$Uri2
)
$Reg = "^([0-9+])+$"
if ([string]::IsNullOrEmpty($uri1) -and [string]::IsNullOrEmpty($Uri2)) { return "Two blank strings provided" }
if ($Uri1 -eq $Uri2) { return $Uri1 }
if ([string]::IsNullOrEmpty($uri1)) { return $Uri2 }
if ([string]::IsNullOrEmpty($uri2)) { return $Uri1 }
if ($Uri1.Length -ne $Uri2.Length) { return "Strings cannot be different lengths" }
if (($Uri1 -notmatch $Reg) -or ($Uri2 -notmatch $Reg)) { return "Strings must be in the format '0123..' or '+123..'" }
($Uri1.Length-1)..0 | % {
if ($Uri1[$_] -ne $Uri2[$_]) { $Diff = $_ }
}
$Start = $Uri1.Substring(0,$Diff)
$Sub1 = $Uri2.Substring($Diff)
$Sub2 = $Uri1.Substring($Diff)
if ($Sub1 -lt $Sub2) {
$Min = $Sub1 ; $Max = $Sub2
} else {
$Min = $Sub2 ; $Max = $Sub1
}
$FormatStr = "" ; 1..$Min.Length | % { $FormatStr += "0"}
$Min..$Max | % { "$($Start)$($_.ToString($FormatStr))" }
}
function Save-ToFile {
Param(
[Parameter(ValueFromPipeline=$True)]
$Item = $null,
[switch]$ReturnName,
$ExtFilter = "*",
$WinTitle = "Select File",
$FileTypeDisplay = $null
)
If ($FileTypeDisplay -eq $null) {
If ($ExtFilter -eq "*") {
$ExtName = "All"
} Else {
$ExtName = (Get-Culture).TextInfo.ToTitleCase($ExtFilter)
}} Else {
$ExtName = (Get-Culture).TextInfo.ToTitleCase($FileTypeDisplay) }
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$FolderDialog = New-Object System.Windows.Forms.SaveFileDialog
$FolderDialog.Filter = "$($ExtName) files (*.$($ExtFilter.ToLowerInvariant()))| *.$($ExtFilter.ToLowerInvariant())"
$FolderDialog.Title = $WinTitle
$Result = $FolderDialog.ShowDialog()
If ($Result -eq "OK"){
$Item | Out-File $FolderDialog.FileName -Append
If ($ReturnName) { return $FolderDialog.FileName }}
Else {
Write-Error "No file selected" }
}
#endregion
Function Main {
Param ( [Hashtable]$NumRanges )
#region Process Data
$AllNums = $NumRanges.Keys | % {
Get-UniqueExt -Uri1 $_ -Uri2 $NumRanges[$_]
}
$S4BNums = Get-CsAssignedURIs
$S4BNums | % { $_.Number = ($_.Number.Split(';')[0] -ireplace "tel:","") }
$KT = #{}
$S4BNums | % {
$KT[$_.Number] = $_
}
$FullRecord = $AllNums | Sort | % {
$Number = $_
$Type = ""
$Name = ""
if ($KT[$_] -ne $null){
$UseDetails = $KT[$_]
$Name = $UseDetails.Name
$Type = $UseDetails.Type
}
[PSCustomObject]#{
Number = $Number
Name = $Name
Type = $Type
}
}
#endregion
return $FullRecord
}
$Results = Main $NumRangeKeyTable
#region Output-Data
if ($FileName -eq $null) {
$FileName = (Save-ToFile -Item "" -ReturnName -ExtFilter "Csv")
}
if ($FileName -ne $null) {
$Results | Export-Csv -Path $FileName -NoTypeInformation
} else { $Results | Out-GridView }
#endregion
#endregion

Related

PowerShell collection exported to separate row in CSV

I have the following script that will export all the Office 365 licenses per user that is enabled for a license to a CSV. The export will list each user and then the multiple licenses on the same row like this:
There is not a set number of licenses for each user, users will have different licenses.
Instead of having them export on the same row, I need to have the license for each user on a separate row like this:
How can I modify the script to make that happen?
#### All lincesed users###
$JobCollection = #()
$mbx = Get-MsolUser -all | Where-Object { $_.isLicensed -eq "TRUE" }
$count=0
$fullcount = ($mbx | measure-object).count
foreach ($M in $MBX) {
$count++
$search = $m.userprincipalname.tostring()
Write-progress -activity "Getting Calendar $count out of $fullcount --- $search " -percentcomplete (($count / $fullcount)*100) -status "Processing"
$License = (((Get-MsolUser -UserPrincipalName $M.userprincipalname).Licenses.accountskuid) -join ",")
#$queuelink = $job.reportingqueueuri.absoluteuri
#$jobID = $Job.jobid
$Jobs = New-Object psobject
$JObs | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $M.userprincipalname
$JObs | Add-Member -MemberType NoteProperty -Name License -Value $license
$JobCollection += $jobs
}
$JobCollection | export-csv "C:\test\AllMBXlicensse.csv" -NoTypeInformation
You can create a nested loop to assign each value to it's corresponding userprincipalname in a PSCustomObject; like you're already doing:
$mbx = Get-MsolUser -all | Where-Object { $_.isLicensed -eq $true }
$mbxCount = $mbx.count
for ($i = 0; $i -lt $mbx.count; $i++)
{
$UPN = $mbx[$i].userprincipalname
$progressSplat = #{
Activity = "Getting Calendar $i out of $mbx --- $UPN"
PercentComplete = (($i / $mbxCount)*100)
Status = "Processing"
}
Write-Progress #progressSplat
$userLicenses = (Get-MsolUser -UserPrincipalName $UPN).Licenses.accountskuid
foreach ($license in $userLicenses)
{
[PSCustomObject]#{
UserPrincipalName = $UPN
License = $license
} | Export-Csv -Path "C:\test\AllMBXlicensse.csv" -Append -Force -NoTypeInformation
}
}
...or, as TheMadTechnician proposed, using a calculated property (very elegant might I add):
$mbx = Get-MsolUser -all | Where-Object { $_.isLicensed -eq $true }
$mbxCount = $mbx.count
for ($i = 0; $i -lt $mbx.count; $i++)
{
$UPN = $mbx[$i].userprincipalname
$progressSplat = #{
Activity = "Getting Calendar $i out of $mbxCount --- $UPN"
PercentComplete = (($i / $mbxCount)*100)
Status = "Processing"
}
Write-Progress #progressSplat
(Get-MsolUser -UserPrincipalName $UPN).Licenses.accountskuid |
Select-Object -Property #{
Name = "UserPrincipalName"
Expression = { $UPN }
}, #{
Name = "License"
Expression = { $_ }
} | Export-Csv -Path "C:\test\AllMBXlicensse.csv" -Append -Force -NoTypeInformation
}

Get DNS script has missing output in report

<#
Gets the various dns record type for a domain or use -RecordType all for all and -Report to file output.
Use like .\get-dnsrecords.ps1 -Name Facebook -RecordType all or .\get-dnsrecords.ps1 -name facebook -RecordType MX
#>
param(
[Parameter(Mandatory = $True,
ValueFromPipelineByPropertyName = $True,
HelpMessage = "Specifies the domain.")]
[string]$Name,
[Parameter(Mandatory = $False,
HelpMessage = "Which Record.")]
[ValidateSet('A', 'MX', 'TXT', 'All')]
[string]$RecordType = 'txt',
[Parameter(Mandatory = $false,
HelpMessage = "DNS Server to use.")]
[string]$Server = '8.8.8.8',
[Parameter(Mandatory = $false,
HelpMessage = "Make a csv report in c:\Reports")]
[Switch]$Report
)
IF ($Name -notlike "*.*") {
$Name = $Name + '.com'
}
If ($Report) {
$filename = [environment]::getfolderpath("mydocuments") + '\' + $($RecordType) + '-' + ($Name.Split('.')[0]) + '.csv'
}
If ($RecordType -eq 'txt' -or $RecordType -eq 'All') {
$TXTRecord = Resolve-DnsName $Name -Type txt -Server $Server -ErrorAction Stop | ForEach-Object {
[PSCustomObject][ordered]#{
name = $_.name
type = $_.Type
ttl = $_.ttl
section = $_.Section
strings = ($_.strings | Out-String).trim()
}
}
If ($RecordType -eq 'txt') {
$TXTRecord
If ($Report) {
$TXTRecord | Export-Csv $filename -NoTypeInformation -Delimiter ','
}
$TXTRecord
Return write-host $filename -ForegroundColor blue
}
}
If ($RecordType -eq 'mx' -or $RecordType -eq 'All' ) {
$MXRecord = Resolve-DnsName $Name -Type mx -Server $Server -ErrorAction Stop | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
NameExchange = $_.nameexchange
}
}
If ($RecordType -eq 'MX') {
If ($Report) {
$MXRecord | Export-Csv $filename -NoTypeInformation
}
$MXRecord
Return Write-Host $filename -ForegroundColor blue
}
}
If ($RecordType -eq 'a' -or $RecordType -eq 'All' ) {
$ARecord = Resolve-DnsName $Name -Type A -Server $Server -ErrorAction Stop | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
IP4Address = $_.IP4Address
}
}
If ($RecordType -eq 'a') {
If ($Report) {
$ARecord | Export-Csv $filename -NoTypeInformation
}
$ARecord
Return write-host $filename -ForegroundColor blue
}
}
If ($Report) {
$TXTRecord | Export-Csv $filename -NoTypeInformation
$MXRecord | Select-Object name, Type, ttyl, section, NameExchange | Export-Csv $filename -NoTypeInformation -Append -Force
$ARecord | Select-Object name, Type, ttyl, section, IP4Address | Export-Csv $filename -NoTypeInformation -Append -Force
}
$TXTRecord
$MXRecord
$ARecord
Return Write-Host $filename -ForegroundColor blue
Running .\get-dnsrecords.ps1 -Name Facebook -RecordType all -Report
Example report Output file is like the following and the host blast is ugly as well.
"name","type","ttl","section","strings"
"Facebook.com","TXT","3600","Answer","String"
"Facebook.com","TXT","3600","Answer","String"
"FaceBook.com","MX","21001","Answer",
"FaceBook.com","MX","21001","Answer",
"FaceBook.com","A","20","Answer",
Mx is missing NameExchange
A record is missing IP4Address
Ideas on how to make the report output include the missing items and bonus how to make the host output more readable ?
The problem is when I try to combine the output variables at the end and then export to file. I am just not sure how to correct it.
Here is what I did. If someone has a better answer please let me know.
Running .\get-dnsrecords.ps1 -Name Facebook -RecordType all -Report
The key lines are as follows.
$final = $TXTRecord | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
strings = $_.strings
NameExchange = $_.nameexchange
IP4Address = $_.IP4Address
}
}
$final += $MXRecord | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
strings = $_.strings
NameExchange = $_.nameexchange
IP4Address = $_.IP4Address
}
}
$final += $ARecord | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
strings = $_.strings
NameExchange = $_.nameexchange
IP4Address = $_.IP4Address
}
}
$final | Export-Csv $filename -NoTypeInformation
Full code below
<#
Gets the various dns record type for a domain or use -RecordType all for all and -Report to file output.
Use like .\get-dnsrecords.ps1 -Name Facebook -RecordType all or .\get-dnsrecords.ps1 -name facebook -RecordType MX
#>
param(
[Parameter(Mandatory = $True,
ValueFromPipelineByPropertyName = $True,
HelpMessage = "Specifies the domain.")]
[string]$Name,
[Parameter(Mandatory = $False,
HelpMessage = "Which Record.")]
[ValidateSet('A', 'MX', 'TXT', 'All')]
[string]$RecordType = 'all',
[Parameter(Mandatory = $false,
HelpMessage = "DNS Server to use.")]
[string]$Server = '8.8.8.8',
[Parameter(Mandatory = $false,
HelpMessage = "Make a csv report in c:\Reports")]
[Switch]$Report
)
IF ($Name -notlike "*.*") {
$Name = $Name + '.com'
}
If ($Report) {
$filename = [environment]::getfolderpath("mydocuments") + '\' + $($RecordType) + '-' + ($Name.Split('.')[0]) + '.csv'
}
If ($RecordType -eq 'txt' -or $RecordType -eq 'All') {
$TXTRecord = Resolve-DnsName $Name -Type txt -Server $Server -ErrorAction Stop | ForEach-Object {
[PSCustomObject][ordered]#{
name = $_.name
type = $_.Type
ttl = $_.ttl
section = $_.Section
strings = ($_.strings | Out-String).trim()
}
}
If ($RecordType -eq 'txt') {
$TXTRecord
If ($Report) {
$TXTRecord | Export-Csv $filename -NoTypeInformation -Delimiter ','
}
$TXTRecord
Return write-host $filename -ForegroundColor blue
}
}
If ($RecordType -eq 'mx' -or $RecordType -eq 'All' ) {
$MXRecord = Resolve-DnsName $Name -Type mx -Server $Server -ErrorAction Stop | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
NameExchange = $_.nameexchange
}
}
If ($RecordType -eq 'MX') {
If ($Report) {
$MXRecord | Export-Csv $filename -NoTypeInformation
}
$MXRecord
Return Write-Host $filename -ForegroundColor blue
}
}
If ($RecordType -eq 'a' -or $RecordType -eq 'All' ) {
$ARecord = Resolve-DnsName $Name -Type A -Server $Server -ErrorAction Stop | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
IP4Address = $_.IP4Address
}
}
If ($RecordType -eq 'a') {
If ($Report) {
$ARecord | Export-Csv $filename -NoTypeInformation
}
$ARecord
Return write-host $filename -ForegroundColor blue
}
}
If ($Report) {
$final = $TXTRecord | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
strings = $_.strings
NameExchange = $_.nameexchange
IP4Address = $_.IP4Address
}
}
$final += $MXRecord | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
strings = $_.strings
NameExchange = $_.nameexchange
IP4Address = $_.IP4Address
}
}
$final += $ARecord | ForEach-Object {
[PSCustomObject]#{
Name = $_.name
Type = $_.type
TTL = $_.ttl
Section = $_.section
strings = $_.strings
NameExchange = $_.nameexchange
IP4Address = $_.IP4Address
}
}
$final | Export-Csv $filename -NoTypeInformation
}
$TXTRecord
$MXRecord
$ARecord
Return Write-Host $filename -ForegroundColor blue

powershell sort-object and keeps window open

I have following script to check the installed software on local and remote machines.
Usually it works fine but i have two problems. It only works when i open it in ISE. If i open it in a normal powershell, the script close immediately. Even a pause or read-host command won't work.
For example here is my script for a local machine. Hope you guys can help me.
Function Get-InstalledSoftware {
Param(
[Alias('Computer','ComputerName','HostName')]
[Parameter(
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$true,
Position=1
)]
[string]$Name = $env:COMPUTERNAME
)
Begin{
$lmKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall","SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
$lmReg = [Microsoft.Win32.RegistryHive]::LocalMachine
$cuKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
$cuReg = [Microsoft.Win32.RegistryHive]::CurrentUser
}
Process{
if (!(Test-Connection -ComputerName $Name -count 1 -quiet)) {
Write-Error -Message "Unable to contact $Name. Please verify its network connectivity and try again." -Category ObjectNotFound -TargetObject $Computer
Break
}
$masterKeys = #()
$remoteCURegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($cuReg,$computer)
$remoteLMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,$computer)
foreach ($key in $lmKeys) {
$regKey = $remoteLMRegKey.OpenSubkey($key)
foreach ($subName in $regKey.GetSubkeyNames()) {
foreach($sub in $regKey.OpenSubkey($subName)) {
$masterKeys += (New-Object PSObject -Property #{
"ComputerName" = $Name
"Name" = $sub.getvalue("displayname")
"SystemComponent" = $sub.getvalue("systemcomponent")
"ParentKeyName" = $sub.getvalue("parentkeyname")
"Version" = $sub.getvalue("DisplayVersion")
"UninstallCommand" = $sub.getvalue("UninstallString")
"InstallDate" = $sub.getvalue("InstallDate")
"RegPath" = $sub.ToString()
})
}
}
}
foreach ($key in $cuKeys) {
$regKey = $remoteCURegKey.OpenSubkey($key)
if ($regKey -ne $null) {
foreach ($subName in $regKey.getsubkeynames()) {
foreach ($sub in $regKey.opensubkey($subName)) {
$masterKeys += (New-Object PSObject -Property #{
"ComputerName" = $Name
"Name" = $sub.getvalue("displayname")
"SystemComponent" = $sub.getvalue("systemcomponent")
"ParentKeyName" = $sub.getvalue("parentkeyname")
"Version" = $sub.getvalue("DisplayVersion")
"UninstallCommand" = $sub.getvalue("UninstallString")
"InstallDate" = $sub.getvalue("InstallDate")
"RegPath" = $sub.ToString()
})
}
}
}
}
$woFilter = {$null -ne $_.name -AND $_.SystemComponent -ne "1" -AND $null -eq $_.ParentKeyName}
$props = 'Name','Version','ComputerName','Installdate','UninstallCommand','RegPath'
$masterKeys = ($masterKeys | Where-Object $woFilter | Select-Object $props | Sort-Object Name)
$masterKeys
}
End{}
}
Get-InstalledSoftware | select-object name | sort-object
Send your output somewhere, not just to the host window. I suspect the windows closes or your pause command is kicking in before its retrieved the results thus they aren't sent to the host window.
The following works fine for me run as a script:
Get-InstalledSoftware | Select-Object name | Sort-Object | Out-Gridview

`Write-Output` in `foreach` collection expression

I'm actually working my way through Bruce Payette's Powershell in Action and run to a code snippet I actually don't understand.
$characterData = #{
'Linus' = #{ age = 8; human = $true}
'Lucy' = #{ age = 8; human = $true}
'Snoopy' = #{ age = 2; human = $true}
}
function Get-Character ($name = '*')
{
foreach ($entry in $characterData.GetEnumerator() | Write-Output)
{
if ($entry.Key -like $name)
{
$properties = #{ 'Name' = $entry.Key } +
$entry.Value
New-Object PSCustomObject -Property $properties
}
}
}
My problem is that I don't understand why Write-Output is used in foreach ($entry in $characterData.GetEnumerator() | Write-Output)? Can this syntax be interpreted as equivalent to $characterData.GetEnumerator() | ForEach-Object { ... ?
Thx

How to display the variable result as table in email body or console screen?

The below script to get the logon users and send as email was working great but only on the console output only.
I am trying to get the result as a table so the result in the console and the email body will be like:
Server, ConnectionType, User, ID, State
PRDSVR16, rdp-tcp#44, SVC-SQL, 4, Active
PRDSVR10, rdp-tcp#27, Admin.domain, 6, Disc
SVR25-VM,console,domain.admin,8,Active
Open in new window
This is the script:
$Today = Get-Date -Format 'F'
$SessionList = "`n`nRDP Session List - " + $Today + "`n`n"
$CurrentSN = 0
# Get a list of servers from Active Directory
write-progress -activity "Getting list of servers from Active Directory" -status "... please wait ..."
$Servers = (Get-ADComputer -Filter { Enabled -eq $True -and OperatingSystem -like "*Server*" } -Properties OperatingSystem -SearchBase "OU=Data Center,DC=Company,DC=com") |
Where-Object { Test-Connection $_.Name -Count 1 -Quiet } |
Select-Object -ExpandProperty Name
$NumberOfServers = $Servers.Count
# Iterate through the retrieved list to check RDP sessions on each machine
ForEach ($Server in $Servers)
{
Write-Host "Processing $Server ..." -ForegroundColor Yellow
Write-progress -activity "Checking RDP Sessions" -status "Querying $Server" -percentcomplete (($CurrentSN / $NumberOfServers) * 100)
try
{
$SessionList += qwinsta /server:$Server |
Select-Object -Skip 1 |
% {
[PSCustomObject] #{
Type = $_.Substring(1, 18).Trim()
User = $_.Substring(19, 20).Trim()
ID = $_.Substring(41, 5).Trim()
State = $_.Substring(48, 6).Trim()
}
} |
? { $_.Type -notin 'console', 'services', 'rdp-tcp' -and $_.User -ne $null -and $_.User -ne 65536 } |
% {
"`n$Server logged in by $($_.User) on $($_.Type), session id $($_.ID) $($_.state)"
}
}
catch
{
$SessionList += "`n Unable to query " + $Server
write-host "Unable to query $Server! `n $($Error[0].Exception)" -foregroundcolor Red
}
$CurrentSN++
}
# Send the output the screen.
$SessionList + "`n`n"
$sendMailArgs = #{
From = "$env:USERNAME#$env:userdnsdomain"
To = 'SOC#domain.com'
SmtpServer = 'SMTP.domain.com'
Priority = 'High'
Body = $SessionList | Select-Object #{ N = 'Server'; E = { $Server } },
#{ N = 'User'; E = { $_.User } },
#{ N = 'LogonType'; E = { $_.Type } },
#{ N = 'ID'; E = { $_.ID } },
#{ N = 'State'; E = { $_.State } }
Subject = "$($SessionList.Count) Logged On users from $($NumberOfServers) online servers as at $($Today)"
}
Send-MailMessage #sendMailArgs
Rendering collected information in different places is way easier if you keep strict separation between data and presentation (or formatting) of said data.
For the $SessionList for example, that means doing less than what you're currently trying to do inside the loop:
$ErrorList = #()
$SessionList = foreach($server in $servers){
try{
qwinsta /server:$Server |Select-Object -Skip 1 |ForEach-Object {
[PSCustomObject] #{
Server = $server
Type = $_.Substring(1, 18).Trim()
User = $_.Substring(19, 20).Trim()
ID = $_.Substring(41, 5).Trim()
State = $_.Substring(48, 6).Trim()
}
} |Where-Object { $_.Type -notin 'console', 'services', 'rdp-tcp' -and $_.User -ne $null -and $_.User -ne 65536 }
}
catch{
$ErrorList += [pscustomobject]#{
Server = $server
ErrorRecord = $_
}
}
}
Notice how I don't construct any strings - I just create the custom objects, filter them - and then leave them as-is.
Now it becomes much easier to format the data as desired for different output media:
# For console output, simply pipe to Format-Table
$SessionList |Format-Table
if($ErrorList.Count -gt 0){
Write-Warning "The following servers had errors, please inspect"
$ErrorList |Format-Table
}
# For email output we can use `ConvertTo-Html`
$Body = $SessionList |ConvertTo-Html -As Table -Fragment
if($ErrorList.Count -gt 0){
$ErrorTable = $ErrorList |ConvertTo-Html -As Table -Fragment
$Body = $Body,$ErrorTable -join '<br />'
}
$sendMailArgs = #{
# ...
Body = ConvertTo-Html -Body $Body -Title "Session list"
BodyAsHtml = $true
# ...
}