Use powershell to stop connections to stuck RDS server

When an RDS server in a cluster is partially running but not allowing the broker to remotely manage it the Server Manager console gets stuck in a situation where it waits for the server to respond and you cannot manage your cluster.

Use the following commands to disable connections to an RDS server that is not allowing logins to complete. (simple but handy to know in a hurry)

NOTE: you must run powershell as administrator or these commands do not function.

Import-Module RemoteDesktop
Set-RDSessionHost servername.domain.local -NewConnectionAllowed "No"

I have found that the Server Manager GUI has trouble displaying the new state of the server. Sometimes a refresh of the page helps and sometime I have to close and reopen the console to see the updated state.

Advertisements

Managing remote processes with Powershell

This is to manage remote processes on a server that is not allowing you to log in due to high CPU on a process.

Invoke-Command servername {Get-Process}
Example to look for chrome processes
Invoke-Command servername {Get-Process chrome}

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ComputerName
------- ------ ----- ----- ----- ------ -- ----------- ------------
    254     39  93252 104076  852    12.97  21204 chrome servername
    238     24  38156  49512  759     1.17  21380 chrome servername
    223     20  23184  27044  731     0.25  22012 chrome servername
    399     48  87928 112292  382    11.02  22444 chrome servername
    544     80 293940 328044 1137 34509.08  22576 chrome servername

The last line with the large number for CPU is the cause of the issue.

This command will kill the offending process with ID 22576

Invoke-Command servername {Stop-Process -ID 22576 -Force}

This command will kill all chrome processes

Invoke-Command servername {Get-Process chrome | Stop-Process -Force}

After that the server was responsive again.

Powershell script to enable Allow New Connections for all RDS servers

This will enable “Allow New Connections” for all servers in all collections.  Handy when you disable logins for troubleshooting and forget to enable them again.  I have configured this to run at 5am each morning on our broker server.

Server Manager can sometimes not display the updated values until it is closed and reopened so keep that in mind. Generally it updates with a simple refresh.

Import-Module RemoteDesktop ForEach ($CollectionName in Get-RDSessionCollection) { ForEach ($HostToEnable in (get-rdsessionhost -collectionname $CollectionName.CollectionName | where {$_.NewConnectionAllowed -ne "Yes"})) { $HostToEnable Set-RDSessionHost $HostToEnable.SessionHost -NewConnectionAllowed "Yes" } }

If you would like to continue to block logins to a specific server that may be in long term diagnostics or maintenance you could either disable the script temporarily (as in change the start date in the scheduled task to some time in the future) or add a line to the end of the code such as,

Set-RDSessionHost servername.contoso.com -NewConnectionAllowed “No”

In a static environment a script such as the following would work,

Set-RDSessionHost servername1.contoso.com -NewConnectionAllowed "Yes" Set-RDSessionHost servername2.contoso.com -NewConnectionAllowed "Yes" Set-RDSessionHost servername3.contoso.com -NewConnectionAllowed "Yes" #The following server is in maintenance Set-RDSessionHost servername4.contoso.com -NewConnectionAllowed "No"

 
Updated version that sends an email message when a change has been made.

$MailSubject = "RDS Enable Login Script" $MailServer = "Mail.contoso.local" $MailTo = "support@contoso.com.au" $MailFrom = "support@contoso.com.au" $MailBody = "The following servers were detected as having their logins disabled and have been automatically set to allow logins again:" Import-Module RemoteDesktop ForEach ($CollectionName in Get-RDSessionCollection) { foreach ($HostToEnable in (get-rdsessionhost -collectionname $CollectionName.CollectionName | where {$_.NewConnectionAllowed -ne "Yes"})) { $Output += "
" + $HostToEnable.SessionHost Set-RDSessionHost $HostToEnable.SessionHost -NewConnectionAllowed "Yes" } } If ($Output -ne $null) { $MailBody += $Output + "


This script runs on server $($env:computername) under the credentials of $($env:UserName)" Send-Mailmessage -To $MailTo -From $MailFrom -Body $MailBody -Subject $MailSubject -SMTPServer $MailServer -BodyAsHtml }

 

 

 

Update Environment Tab in AD with Powershell

This code will go through the AD and find any users who have the tick boxes,

“Connect client drives at logon”
“Connect client printers at logon”
“Default to main client printer”

Unticked and set them to ticked on.

The value may not exist on every account if the settings have not been change so there will be errors for those accounts but these accounts will show these values as ticked on if checked manually.

Get-ADUser -Filter * -SearchBase “OU=Users,DC=Contoso,DC=Com” | Foreach {
$User = [adsi](“LDAP://” + $_.distinguishedname)
Write-Host $_.distinguishedname
If ($User.InvokeGet(“ConnectClientDrivesAtLogon”) -eq 0)
{Write-Host “Changing ConnectClientDrivesAtLogon for $($_.distinguishedname)”
$User.InvokeSet(“ConnectClientDrivesAtLogon”,1)
$User.setinfo()
}

If ($User.InvokeGet(“ConnectClientPrintersAtLogon”) -eq 0)
{Write-Host “Changing ConnectClientPrintersAtLogon for $($_.distinguishedname)”
$User.InvokeSet(“ConnectClientPrintersAtLogon”,1)
$User.setinfo()
}

If ($User.InvokeGet(“DefaultToMainPrinter”) -eq 0)
{Write-Host “Changing DefaultToMainPrinter for $($_.distinguishedname)”
$User.InvokeSet(“DefaultToMainPrinter”,1)
$User.setinfo()
}
}

List computers in AD with LastLoginTimestamp

List Workstation OS computers,

get-adcomputer -filter {OperatingSystem -notlike '*server*'} -Properties OperatingSystem,LastLogonTimestamp | Sort LastLogonTimestamp | Select Name,OperatingSystem,@{Name='LastLogonTimestamp'; Expression={[DateTime]::FromFileTime($_.LastLogonTimestamp).ToString("dd/MM/yyyy")}}

List Server OS computers,

get-adcomputer -filter {OperatingSystem -notlike '*server*'} -Properties OperatingSystem,LastLogonTimestamp | Sort LastLogonTimestamp | Select Name,OperatingSystem,@{Name='LastLogonTimestamp'; Expression={[DateTime]::FromFileTime($_.LastLogonTimestamp).ToString("dd/MM/yyyy")}}

Download wallpapers from InterfaceLift

The following code will automatically scrape the latest wallpapers from InterfaceLift. This code could be scheduled to run at startup and automatically update the target folder to be used as a wallpaper slideshow. A second script is to download to a static image file that will update as InterfaceLift releases a new image.

Shout out to InterfaceLift which have some of the most beautiful wallpapers out there.  If you use AdBlock be sure to support the site by excluding it and allowing the ads to download.

# Written by Ben Penney https://sysadminben.wordpress.com # Change this line to match the resolution of the image to download. It must be in the same string format used by InterfaceLift $Resolution = "1920x1200" # Folder where the images will download to $TargetPath = "$($env:USERPROFILE)\Pictures\InterfaceLift" # If folder InterfaceLift does not exist in the users Pictures folder it creates it If (!(Test-Path $TargetPath)) {New-Item -ItemType Directory $TargetPath} # The following string is used to match the urls of the preview images in the main page. These names are used in the full resolution images [regex]$regex = "(?<=\/wallpaper\/previews\/).*?(?=_672x420.jpg)" # This line downloads the web page and looks for the above string matches $regex.Matches((Invoke-WebRequest "https://interfacelift.com/wallpaper/downloads/date/any").Content) | foreach-object { # Generate the final image name from the matched strings above $FileName = "$($_)_$Resolution.jpg" # Check the image has not already been downloaded If (Test-Path "$TargetPath\$FileName") { # Skip if the file exists "'$FileName' exists. Skipping." } Else { # Download the file "Downloading file '$FileName'" Invoke-WebRequest "http://interfacelift.com/wallpaper/7yz4ma1/$FileName" -OutFile "$TargetPath\$FileName" } }

Second script that overwrites a single image file. Once the script is run for the first time point your wallpaper to the InterfaceLift.jpg file in your Pictures folder.

# Written by Ben Penney https://sysadminben.wordpress.com # Change this line to match the resolution of the image to download. It must be in the same string format used by InterfaceLift $Resolution = "1920x1200" # Folder where the images will download to $TargetPath = "$($env:USERPROFILE)\Pictures\InterfaceLift.jpg" # The following string is used to match the urls of the preview images in the main page. These names are used in the full resolution images [regex]$regex = "(?<=\/wallpaper\/previews\/).*?(?=_672x420.jpg)" # Grab the name of the latest image $LatestImage = (Invoke-WebRequest "https://interfacelift.com/wallpaper/downloads/date/any/index$int.html").Content)[0] # Generate the final filename of the full resolution image $FileName = "$($regex.Matches($LatestImage)_$Resolution.jpg" # Download the file "Downloading file '$FileName'" Invoke-WebRequest "http://interfacelift.com/wallpaper/7yz4ma1/$FileName" -OutFile "$TargetPath"

Read email in folder in O365

Earlier I posted how to read email in your inbox in O365. After having to write code to read email in a specific folder I thought I would add it here for everyone to use.

This specific code reads through all the emails in the specified folder and outputs who the email was sent to (yes weird I know but I was cc’ed on these specific emails so I wanted to know who really was the recipient)

# Written by Ben Penney https://sysadminben.wordpress.com $mail="ben.penney@domain.com" $password="password" $FolderName = "foldername" # Set the path to your copy of EWS Managed API $dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll" # Load the Assemply [void][Reflection.Assembly]::LoadFile($dllpath) # Create a new Exchange service object $service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService #These are your O365 credentials $Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($mail,$password) # this TestUrlCallback is purely a security check $TestUrlCallback = { param ([string] $url) if ($url -eq "https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml") {$true} else {$false} } # Autodiscover using the mail address set above $service.AutodiscoverUrl($mail,$TestUrlCallback) # create Property Set to include body and header of email $PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties) # set email body to text $PropertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text; # Set how many emails we want to read at a time $numOfEmailsToRead = 100 # Index to keep track of where we are up to. Set to 0 initially. $index = 0 $folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(20) $folderView.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly) $folderView.PropertySet.Add([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName) $searchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$FolderName) $findFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$searchFilter,$folderView) $findFolderResults.Folders.ID.UniqueID # Do/while loop for paging through the folder do { # Set what we want to retrieve from the folder. This will grab the first $pagesize emails $view = New-Object Microsoft.Exchange.WebServices.Data.ItemView($numOfEmailsToRead,$index) # Retrieve the data from the folder $findResults = $findFolderResults.Folders[0].FindItems($view) foreach ($item in $findResults.Items) { # load the additional properties for the item $item.Load($propertySet) # Output the results "$($item.ToRecipients.Address)" } # Increment $index to next block of emails $index += $numOfEmailsToRead } while ($findResults.MoreAvailable) # Do/While there are more emails to retrieve

Simple Robocopy Backup Script

This simple backup script will copy a folder to another server using dated folders and delete old backup folders older than the specified number of days (15 in this example). Add this script to the task scheduler. In an AD environment, assign rights to the target location to the computer object and run the task as SYSTEM to avoid pesky service accounts.

# Written by Ben Penney https://sysadminben.wordpress.com
$limit = (Get-Date).AddDays(-15)
$SourcePath = "c:\folder"
$TargetPath = "\\backupnas\Backup\servername"
$SMTPRecipients = "user1@email.com","user2@email.com"
$SMTPFrom = "smtp@email.com" 
$SMTPServer = "smtp.domain.com" 
$SMTPSubject = "Server backup" 
$EmailBody = "Starting process $(Get-Date)`n" 
# Delete folders older than the $limit. 
Get-ChildItem -Path $path -Force | Where-Object { $_.PSIsContainer -and $_.CreationTime -lt $limit } | ForEach { $EmailBody += "Deleting folder "+($_.Name) Remove-Item -Force -recurse }
$DateString = (Get-Date).ToString("yyyyMMdd")
$RobocopyOutput = RoboCopy $SourcePath $TargetPath\$DateString /COPY:DAT /E /R:2 /W:2 /NFL /NDL /NP 
# OPTIONAL CODE TO TRUNCATE THE ROBOCOPY OUTPUT
If ($RobocopyOutput.Count -gt 200) { $RobocopyOutput = $RobocopyOutput[0..200] $RobocopyOutput[0] = "********** ROBOCOPY LOG TRUNCATED ************" }
$EmailBody += [system.string]::join("`n",$RobocopyOutput)
If ($EmailBody.Contains("ERROR")) {$SMTPSubject += " ***ERRORS***"}
$EmailBody += "`n`nFinished process $(Get-Date)"
Send-MailMessage -To $SMTPRecipients -From $SMTPFrom -Subject $SMTPSubject -SMTPServer $SMTPServer -Body $EmailBody

Using Powershell to read and write MSSQL

These examples are using a test database called EmailList with a table called Table1 which has three fields ‘username’,’email’ and ‘display name’.

This sample code will log in to MSSQL with your current logged in credentials,

$dataSource = "TESTSQLSERVER" $database = "EmailList" $connectionString = "Server=$dataSource;Database=$database;Integrated Security=True;" $connection = New-Object System.Data.SqlClient.SqlConnection $connection.ConnectionString = $connectionString $connection.Open() $command = $connection.CreateCommand()


This code will return the number of records with the specified email address,

$command.CommandText = "Select Count(*) FROM Table1 Where Email='testemail@test.com'" $command.ExecuteScalar()



This code with write a record to the database,

$command.CommandText = "INSERT INTO Table1 VALUES ('username','testemail@test.com','Users Name')" $command.ExecuteNonQuery()


This code will read records from the database,

$command.CommandText = "SELECT * FROM Table1 WHERE Email='testemail@test.com'" $reader = $command.ExecuteReader() While ($reader.Read()) {$reader[0];$reader[1];$reader[2]} $reader.Close()


Don’t forget to close your connection once you have finished,

$connection.Close()

Powershell – Removing data in brackets in string

I was given a task to auto generate email addresses based on first name and last name. Given that some people like to put data in their names in brackets to denote nicknames or maiden names one of the tasks was to reliably remove this data before working out the email address.

Trusty Google comes up with the following solution from this blog,

"string" -replace "\([^\)]+\)",""

This is a pretty elegant solution that produces the following results,

TEST 1
PS> "test(adsf)" -replace "\([^\)]+\)",""
test
TEST 2
PS> "test(adsf)(test)" -replace "\([^\)]+\)",""
test
TEST 3
PS> "test(adsf) (test)" -replace "\([^\)]+\)",""
test
TEST 4
PS> "test(adsf)xx(test)" -replace "\([^\)]+\)",""
testxx
TEST 5
PS> "test()" -replace "\([^\)]+\)",""
test()
TEST 6
PS> "test(a(dsf) (t)est)" -replace "\([^\)]+\)",""
test est)
TEST 7
PS> "test(adsf" -replace "\([^\)]+\)",""
test(adsf
TEST 8
PS> "test)" -replace "\([^\)]+\)",""
test)

Since I want a solution that removes every bracket no matter what I needed to take this a little further.

By replacing the + from the RegEx string with a * this will allow empty strings between the bracket start and end which resolves TEST 5,

TEST 5
PS> "test()" -replace "\([^\)]*\)",""
test

This works because the + symbol says I should have one or more of the character before me in the string which is [^/)] meaning not end bracket. By replacing it with the * symbol I am saying you can have none or more of those characters.

By adding a * character to the end of the RegEx string we can also accommodate the string with the missing closing bracket making the assumption that there should have been a closing bracket at the end of the string (ignore this if you do not want to make this assumption),

TEST 7
PS> "test(adsf" -replace "\([^\)]*\)*",""
test

As for the other two failures, we will just have to take it on the chin and accept that we can’t resolve everything and simply remove the extra characters with an additional replace statement,

"string" -replace "\([^\)]*\)*","" -replace "[\(\)]",""

Which provides the following results,

TEST 1
PS> "test(adsf)" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test
TEST 2
PS> "test(adsf)(test)" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test
TEST 3
PS> "test(adsf) (test)" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test
TEST 4
PS> "test(adsf)xx(test)" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
testxx
TEST 5
PS> "test()" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test
TEST 6
PS> "test(a(dsf) (t)est)" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test est
TEST 7
PS> "test(adsf" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test
TEST 8
PS> "test)" -replace "\([^\)]*\)*","" -replace "[\(\)]",""
test

As always let me know if I have forgotten anything or you have any questions.
Thanks