Sunday, February 26, 2023

Setting User Expectations - Part 2

 In Part 1 of this series, we discussed how to create a QoS indicator to inform the user of their expected experience before they even logon by making a few simple changes to the NetScaler. In part 2 of the series, we will add a graphical representation of the results. The strength of the connection will be represented by the number of bars in the graphic - the more bars, the stronger the connection.




The first step will be to upload the bar images which may be found here:
https://samjacobs.sharefile.com/d-s15afc8842d044e8cb6f0da39dc6e0381

into the following directory:

/var/netscaler/logon/themes/QoS/custom_media.

Next, we need to add some css to the file: /var/netscaler/logon/themes/QoS/css/theme.css.

Insert the following:

#QoSbars {
   display:table-cell;   
   min-width:70px;
   height:40px;
}
.FourBars, .ThreeBars, .TwoBars, .OneBar, .NoBars {
   min-width:70px;
   height:40px;
   background-repeat: no-repeat;
}
.FourBars {
   background-image: url("../custom_media/FourBars.png");
}
.ThreeBars {
   background-image: url("../custom_media/ThreeBars.png");
}
.TwoBars {
   background-image: url("../custom_media/TwoBars.png");
}
.OneBar {
   background-image: url("../custom_media/OneBar.png");
}
.NoBars {
   background-image: url("../custom_media/NoBars.png");
}

The above changes need to be made to the primary NetScaler node and will be replicated to the secondary node.

The final changes need to be made to the file: /var/logon/LogonPoint/tmindex.html.

After making a backup, open the file and look for the following code (that we added in Part 1):

<div id="QoSinfo">
   <div id="QoStext">&nbsp;</div>
</div>

Add the following line after the QoSinfo <div>:

<div id="QoSbars" class="ThreeBars">&nbsp;</div>

Then, scroll to the bottom of the file to find where you added the JavaScript code, and replace the entire <script> section that you added with the below. You can customize your ranges by simply changing the values in the aLatencyRanges array below. The default below says that anything 50 ms. or below is considered Excellent, 51-100 ms. is Very Good, 101-130 ms is Good, 131-200 is Fair, and 200+ ms. is Poor.

<script>
    // QoS latency ranges - max value for range, class name for graphic, legend
    var aLatencyRanges = 
        [[50,"FourBars","Excellent"],
         [100,"ThreeBars","Very Good"],
         [130,"TwoBars","Good"],
         [200,"OneBar","Fair"],
         [99999,"NoBars","Poor"]];
    
    var testImgUrl = "https://" + location.hostname + "/vpn/images/Tick32.gif";
    
    //alert ("Using image: " + testImgUrl);
    //This method calculates the time it takes for the image to load 
    function checkLatency(url, callback) {
        //alert("checkLatency()...");
        var t=[], n=2, tcp, rtt;
        var ld = function() {
           t.push(+new Date);
           if(t.length > n) {
             rtt=t[2]-t[1];
             callback(rtt);
           }
           else {
             var img = new Image;
             img.onload = ld;
             img.src=url+"?" + Math.random() + '=' + new Date;
           }
        };
            
        ld();
    }
    
    function displayQoS(latency) {
        var QoSbars = document.getElementById("QoSbars");
        var QoStext = document.getElementById("QoStext");
    
        for (i=0; i<aLatencyRanges.length;i++) {
            if(latency<=aLatencyRanges[i][0]) {
                var QoSclass = aLatencyRanges[i][1];
                var QoSrange = aLatencyRanges[i][2] + ' (' + latency + ' ms.)';
    
                QoSbars.className = QoSclass;
                QoStext.innerHTML = 'Connection Strength:<br>'+QoSrange; 
    
                break;
            }
        }
    }
        
    function runQoS() {
       checkLatency(testImgUrl, displayQoS);
    }
    runQoS();

    // end of QoS modifications
    </script>

Now when you browse to your logon page, in addition to having the connection strength is milliseconds, you will also have the strength displayed graphically.

Sunday, January 8, 2023

Setting User Expectations - Part 1

One of the main gripes from Citrix administrators are clients complaining that Citrix is slow when the issue is not anything in the Citrix infrastructure, but rather at the user's endpoint. This can include issues such as the user's PC (e.g. slow or pegged CPU), or a weak Wi-Fi connection. 

In this 3-part series, I will show you how to set the user's expectations before they even log on by placing what I call a QoS (Quality of Service) indicator on your NetScaler's logon page. This is accomplished by measuring what is known as Time To First Byte (TTFB). TTFB is a metric that measures the time between the request for a resource and when the first byte of a response begins to arrive by downloading a very small file from the NetScaler.

For this series, we will be using firmware version 13.1 of NetScaler, and we will be using the RfWebUI (Receiver for Web UI) theme. The tool we will use for most of our work will be WinSCP - a free secure copy utility (https://winscp.net/eng/download.php).

In Part 1, we will get the basics of the QoS indicator done, and when finished, it should look something like this:



We begin by creating a NetScaler theme which will contain the look and feel of our customizations. The easier way to do this is by opening up a PuTTY (terminal emulation software) session to the primary node and entering:

add vpn portaltheme QoS -basetheme RfWebUI

These changes will be replicated from the primary node to the secondary node. Make sure you make these changes to the primary, or they will be removed the next time the config is synched from the primary node.

The above command will create a new theme called QoS in the following directory: /var/netscaler/logon/themes. 

Open the file /var/netscaler/logon/themes/QoS/css/theme.css and insert the following:

#QoSinfo {
   display:table-row;
   height:125px;
   width:250px;
   float:right;
   color: #fff;
}
#QoStext {
   display:table-cell;
   font-family:'Open Sans', sans-serif;
   font-size:12px;
   width:170px;
   min-height:30px;
   margin-left:10px;
   vertical-align:middle;
   padding-top:7px;
   text-align:left;
}
#QoStext a {
   color:white;
}

Then save the file.

We then need to make a backup up of the following file on the NetScaler: /var/logon/LogonPoint/tmindex.html.  Open the file. Around line 386, you should see:

<div class="logon-spacer"></div>

Add the following lines between <div class="logon-spacer"> and </div>:

<div id="QoSinfo">
  <div id="QoStext">&nbsp;</div>
</div>

This is where the QoS indicator will be inserted on the screen.

When done, it should look like this:




The final step will be to insert the JavaScript code for the Time To First Byte calculation and to insert the result into the QoStext <div> above.

Scroll to the bottom of the file and locate the following two lines:

<script src="receiver/js/ctxs.core.min.js"></script>
<script src="receiver/js/ctxs.webui.min.js"></script>

Insert the following:

    <script>
     var testImgUrl = "https://" + location.hostname + "/vpn/images/Tick32.gif";
     function checkLatency(url, callback) {
        //alert("checkLatency()...");
        var t=[], n=2, tcp, rtt;
        var ld = function() {
           t.push(+new Date);
           if(t.length > n) {
             rtt=t[2]-t[1];
             callback(rtt);
           }
           else {
             var img = new Image;
             img.onload = ld;
             img.src=url+"?" + Math.random() + '=' + new Date;
           }
        };
        ld();
    }
    function displayQoS(latency) {
         var QoStext = document.getElementById("QoStext");
         for (i=0; i<aLatencyRanges.length;i++) {
            if(latency<=aLatencyRanges[i][0]) {
                var QoSclass = aLatencyRanges[i][1];
                var QoSrange = aLatencyRanges[i][2] + ' (' + latency + ' ms.)';
                 QoStext.innerHTML = 'Connection Strength:<br>'+QoSrange; 
                 break;
            }
        }
    }  
    function runQoS() {
       checkLatency(testImgUrl, displayQoS);
    }

    runQoS();
    </script>

Then save the file. Note: if you have an HA pair, any changes to this file must be done to both NetScalers - source code changes are not replicated between the nodes.

That's it! In Part 2, we will add a graphical representation of the QoS indicator.

Thursday, September 15, 2022

Citrix Application Performance Reports

 A user had the following request:

I have a Citrix XA/XD 7.15 LTSR environment and would like to write performance data for two applications (Ex: App1, App2) to an Excel spreadsheet using a Powershell script with the following information:
- Application launch and load times
- Application usage report with the number of users for a particular timespan

Please see the linked PowerShell scripts below. You will need to modify the variables $SQLServer and $SQLDBName for your environment and run the scripts as a Citrix Admin with rights to the Director Monitoring database.

Get-AppLogonDuration.ps1 - will create a report on logon duration for the specified applications for the past x days (defaults to 7).

Examples:

    >.\Get-AppLogonDuration.ps1 -days 3

    Will display information for ALL applications for the past 3 days

    >.\Get-AppLogonDuration.ps1 -apps "'Notepad','SnippingTool'" | Sort StartDate -Desc

    Will display information for the applications 'Notepad' and 'SnippingTool' for the past 7 days (default),
    and sort latest entries first
   
   >.\Get-AppLogonDuration.ps1 -days 30 | Sort LogonSecs -Desc | Select -first 10

    Will show the longest 10 logon times over the past 30 days


Get-UniqueAppCounts.ps1 - will create a report on the number of unique users executing the specified applications for the specified duration

Examples:
    >.\Get-UniqueAppCounts.ps1 -days 3

    Will display counts for all applications for the past 3 days

    >.\Get-UniqueAppCounts.ps1 -apps "'Notepad','SnippingTool'"

    Will display counts for the applications 'Notepad' and 'SnippingTool' for the past 7 days (default).

Thursday, June 2, 2022

Kill a User's Citrix Desktop or Application Session

 A user asked for an easy way via PowerShell to kill a user's Citrix application or desktop session.

On one of the Delivery Controllers, save the below script as Logoff-Sessions.ps1, and execute it. 

You will be prompted for the username (entered as domain\username), and then all of the user's sessions will be listed.

You will be asked for confirmation when you make your selection before logging the session off.

Function Logoff-Session {

    # logoff a specified user's session

    asnp Citrix*

    $user = Read-Host -Prompt "Enter User Name (domain\user format)"
    if ($user -ne '') {
        $sessions = @(Get-BrokerSession | ? username -eq $user |
            Select  @{n='SessionKey'; e={$_.SessionKey}},
                    @{n='Host/Desktop'; e={$_.MachineName}},
                    @{n='Application(s)'; e={$_.ApplicationsInUse}})

        if ($sessions.count -eq 0) {
            Write-Host "Sorry ... there are no applications active for $($user)."
        } else {
            Write-Host 'Session#'.Padright(10) 'Host/Desktop'.PadRight(30) 'Application(s)'
            for ($i=0; $i -lt $sessions.Count; ++$i) {
                $sessnum = $i + 1
                Write-Host $sessnum.ToString().PadLeft(5) '    ' $sessions[$i].'Host/Desktop'.PadRight(30) $sessions[$i].'Application(s)'
            }
            $logoff = Read-Host "Which session would you like to log off (0 to exit)?"

            if ($logoff -eq 0 -or $logoff -eq '') {
                return
            }
            if ($logoff -le $sessions.Count) {
                $sessnum = $logoff - 1
                Write-Host $sessions[$sessnum].'Host/Desktop'.PadRight(30) $sessions[$sessnum].'Application(s)'
                $yn = Read-Host "Is this the session you would like to logoff (Y/N)?"
                if ($yn -eq 'Y') {
                    Stop-BrokerSession $sessions[$sessnum].SessionKey
                }
            }
        }
    }
}

Tuesday, March 29, 2022

Request from a forum user:

I'm running Citrix 7.15 Enterprise edition. Can I get a PowerShell script to retrieve a count of disconnected sessions and available machines in each delivery group ? I would like to send the output to a .csv file.

Here is a very simple way to accomplish this via PowerShell:

Add-PSSnapIn Citrix*
Get-BrokerDesktopGroup | Sort-Object Name | Select-Object Name, 
DesktopsDisconnected, DesktopsAvailable |
Export-Csv -Path "C:\temp\DGinfo.csv" -NoTypeInformation


Sunday, January 16, 2022

Copy Applications from One Delivery Group to Another

A forum user asked if there was an easy way to copy all applications from one delivery group to another. Here is a simple PowerShell script to accomplish this:

Function Select-DG ($DGs, $Title = 'Select Delivery Group'){

    Write-Host ""
    Write-Host "=====   Delivery Groups   ====="
    $menuDGs = @{}
    For ($i=1;$i -le $DGs.count; $i++)
    {
        Write-Host "$i. $($DGs[$i-1].Name)"
        $menuDGs.Add($i,($DGs[$i-1].Name))
    }
    [int]$ansDG = if(($ansDG = Read-Host $Title) -eq ''){0} else {$ansDG}

    if ($ansDG -eq 0) { return 0 }
    if ($ansDG -gt 0 -and $ansDG -lt $DGs.Count) {
        $DGSelected = $menuDGs.Item($ansDG)
        (Get-BrokerDesktopGroup | ? Name -eq "$($DGSelected)").UID
    } else {
        Write-Host "Selection not valid, please make a valid selection between 1 and $($i-1)..."
        return 0
    }
}

# retrieve Delivery Groups and pass to function so you don't have to retrieve them twice
$DeliveryGroups = Get-BrokerDesktopGroup | Sort Name | Select Name, UID

$sourceDG = Select-DG $DeliveryGroups "Select the source Delivery Group (0 to exit)"
if ($sourceDG -ne 0) {
    $targetDG = Select-DG $DeliveryGroups "Select the target Delivery Group (0 to exit)"
}
if ($targetDG -eq 0) { return }
if ($sourceDG -eq $targetDG) {
    Write-Host "Target Delivery Group may not be the same as the Source Delivery Group."
    return
}
$DGApps = Get-BrokerApplication | ? AllAssociatedDesktopGroupUids -contains $sourceDG

$sourceName = (Get-BrokerDesktopGroup | ? Uid -eq $sourceDG).Name
$targetName = (Get-BrokerDesktopGroup | ? Uid -eq $targetDG).Name
Write-Host ""
Write-Host "Source Delivery Group: $($sourceName)"
Write-Host "Target Delivery Group: $($targetName)"

# copy each app
foreach ($app in $DGApps) {
    Write-Host "Copying: $($app.BrowserName)"
    $app | Add-BrokerApplication -DesktopGroup $targetName
}
Write-Host "Done."

Thursday, October 14, 2021

Retrieve Local Administrators from Multiple Computers

 A forum user asked for assistance with a PowerShell script that would read a list of servers from a text file and then either show the list of users in the local administrator groups on each server.

The script below, Get-LocalMembers, takes things a bit further. While it will default to the local administrators group, you can supply any number of groups to the script. It even supports wildcards! By default, the results are displayed on the console, but can also export the results as a .csv file.

The script takes 2 parameters (both optional):

Computers

A list of computers to query. The list may be provided as a parameter to the script, or read from a text file. Default: localhost

Groups

A list of local groups to query on each of the computers. Wildcards are support (see examples below). Default: Administrators

Examples

Get-LocalMembers

    Retrieves the members of the default group (Administrators) on the default computer(localhost).

Get-LocalMembers -Computers (Get-Content -Path "c:\temp\computers.txt")

    Will retrieve the members of the Administrators group on all the computers in the file computers.txt.

Get-LocalMembers -groups 'Remote*','Admin*'

    Will retrieve the members of the Administrators, Remote Desktop Users and Remote Management Users groups on localhost.

Get-LocalMembers | Export-Csv -Path "c:\reports\GroupMembers.csv" -NoTypeInformation

    Retrieves the members of the default group (Administrators) on the default computer(localhost) and exports them to the specified .csv file.

Function Get-LocalMembers {
<#
.SYNOPSIS
    Gets the members of one or more local groups of the specified computer(s) 
    and optionally outputs the results to a CSV file.

.PARAMETER Computers
    Specifies the computers to query.
    Can be a string or a list retrieved from a file via Get-Content (see examples).
    Default: $env:computername (localhost)

.PARAMETER Groups
    Specifies the groups to query. Supports wildcards. 
    Can be a string or a list retrieved from a file via Get-Content (see examples).
    (e.g. Remote* will enumerate both Remote Desktop Users and Remote Management Users)
    Default: Administrators

.EXAMPLE
    Get-LocalMembers
    Retrieves the members of the default group (Administrators) on the default computer(localhost)

.EXAMPLE
    Get-LocalMembers -Computers (Get-Content -Path "c:\temp\computers.txt")
    Will retrieve the members of the Administrators group on all the computers in the file computers.text

.EXAMPLE
    Get-LocalMembers -groups 'Remote*'
    Will retrieve the members of the Remote Desktop Users and Remote Management Users groups on localhost

.EXAMPLE
    Get-LocalMembers | Export-Csv -Path "c:\reports\GroupMembers.csv" -NoTypeInformation

.LINK
    Heavily modified from script: https://gallery.technet.microsoft.com/223cd1cd-2804-408b-9677-5d62c2964883
#>

    Param(
        [string[]]$Computers,
        [string[]]$Groups
    )

    # defaults
    if ($Computers -eq $null) {
        $Computers = $env:COMPUTERNAME
    }
    if ($Groups -eq $null) {
        $groups = 'Administrators'
    }

    # testing the connection to each computer via ping before executing the script
    foreach ($computer in $Computers) {
        if (Test-Connection -ComputerName $computer -Quiet -count 1) {
            $livePCs += $computer
        } else {
            Write-Host ('Computer {0} is unreachable.' -f $computer) -ForegroundColor DarkRed -BackgroundColor White
        }
    }

    $list = new-object -TypeName System.Collections.ArrayList

    # cycle through each computer in the list
    foreach ($computer in $livePCs) {

        # cycle through each group in the list
        foreach($groupToTest in $groups) {
            $err = @()
            $admins = @(Get-WmiObject -Class win32_groupuser -ComputerName $computer -EA SilentlyContinue -EV err | 
                Where-Object {$_.groupcomponent -like "*`"$($groupToTest)`""})

            if ($err.Count -gt 0) {
                $errMsg =  $err[0].Exception.Message
                Write-Host ('Error accessing WMI on {0} ... {1}' -f $computer, $err[0].Exception.Message) `
                            -ForegroundColor DarkRed -BackgroundColor White
            } else {
               if ($admins.Count -gt 0) {
                    # get the group name
                    $null =  $admins[0].Groupcomponent -match '.+Domain\=(.+)\,Name\=(.+)$'
                    $group = $matches[2].trim('"')

                    # get the members of the group
                    $aAdmins = @() 
                    foreach ($admin in $admins) {
                        $null = $admin.partcomponent -match '.+Domain\=(.+)\,Name\=(.+)$' 
                        $null = $matches[1].trim('"') + '\' + $matches[2].trim('"') + "`n"
                        $aAdmins += $matches[1].trim('"') + '\' + $matches[2].trim('"')
                    }
                    $obj = New-Object -TypeName PSObject -Property @{
                        Computer = $computer
                        Group    = $group
                        Members  = ($aAdmins -join ',')
                    }
                    $null = $list.add($obj)
                }
            }
        }
    }
    $list | select computer, group, members
}

Sam Jacobs is the Director of Technology at Newtek Technology Systems (formerly IPM), the longest standing Citrix Platinum Partner on the East Coast. With more than 30 years of IT consulting, Sam is a NetScaler and StoreFront customizations and integrations industry expert. He holds Microsoft and Citrix certifications, and is the editor of TechDevCorner.com, a technical resource blog for IT professionals. He is one of the top Citrix support Forum contributors, and has earned industry praise for the tools he has developed to make NetScaler, StoreFront and Web Interface easier to manage for administrators and more intuitive for end users. Sam became a Citrix Technology Professional (CTP) in 2015, and can be reached at: sjacobs@newtekone.com or on Twitter at: @WIGuru.