Showing posts with label Powershell. Show all posts
Showing posts with label Powershell. Show all posts

Sunday, January 10, 2021

Stupid Citrix Trick #9: Deploying A Delivery Group With Powershell

Problem: You're deploying a Delivery Group to a XenApp farm using Powershell.

Solution: NOT AS SIMPLE as you'd expect!

Citrix had done a really good job of integrating Powershell automation into their system.  Anything you can do in the GUI, you can do in Powershell, and I have heard that the system itself uses Powershell to make its changes.  Still, there are a few gotchas here and there that you have to look out for.

One gotcha in particular nearly made me lose my mind.

All I wanted to do was to create a delivery group and publish some applications to it.  Pretty simple, right?  There's a straightforward Powershell function that does just this:

New-BrokerDesktopGroup
-Name $GroupName
-Description $GroupDescription
-DeliveryType DesktopsAndApps
-DesktopKind Shared
-IsRemotePC $false
-SecureIcaRequired $false
-SessionSupport MultiSession
-TimeZone $TimeZone

As you can see from the options we're applying, this group is delivering both desktops and applications and its servers are used by multiple simultaneous sessions - basically an old-school Citrix server.  The information specified is pretty much what you'd need to supply when using the GUI, so you should be able to add some servers and start firing up your apps, right?

Not so fast, buckaroo.  If you published applications to that delivery group, no one would ever see them, and that's because you have to create another object, a Broker Entitlement Policy Rule.  Here's what the code looks like:

New-BrokerAppEntitlementPolicyRule
-Name $GroupName
-DesktopGroupUid $GroupUid
-Enabled $true
-ExcludedUserFilterEnabled $false
-IncludedUserFilterEnabled $false
-LeasingBehavior Allowed
-SessionReconnection Always

The $GroupName and $GroupUid are for the group you just created, natch. We're naming the rule the same as the group; you can name it something else if you prefer.

I'm not really sure why this rule is required.  It's created silently when using the GUI, so why couldn't it be automatically configured when the DeliveryType is "DesktopsAndApps" or "AppsOnly"?  I suspect it's because that XenApp 7 grew out of the XenDesktop product, not from XenApp 6.5 (at least that's my understanding).  Support for publishing apps was more or less bolted on, and some of the joints are still visible.

In addition, and more aggravating, the documentation for New-BrokerDesktopGroup does not tell you that there's another step before you can publish apps.  If you don't create it, everything will look as though it's configured properly but the apps will not be visible.

Let me know in the comments if this helped you. 'Til next time...

Tuesday, August 4, 2020

Stupid Citrix Trick #8: Forcing A Powershell Function To Return An Array

Problem: You want to return an array object from a Powershell function as an array under all circumstances.

Solution: No one's a bigger fan of Powershell than me. It's not perfect but it's powerful, flexible, and can use lots of other programming environments natively, like WMI objects and the .Net framework.  But today I found one super-duper stupid thing it does that made my blood boil with the heat of a thousand suns.

Specifically if you return an array from a function, and the array only has one element, than just that variable is returned.  So if you have an array of strings containing only a single value, that value will be returned as a string.

Why is this a big deal?  Because ArrayC = ArrayA + ArrayB behaves differently if you are adding two arrays than if you're adding a string object to an array.  Let's run through an example.

Let's pretend we have a couple of functions, GetArrayA and GetArrayB.  These functions just return an array containing three values:

function GetArrayA
{
   $Array = @("1", "2", "3")
   Return  $Array
}

function GetArrayB
{
   $Array = @("40", "50", "60")
   Return  $Array
}

$A = GetArrayA
$B = GetArrayB

$C = $A + $B

So what is the expected value of $C?  Here's what you'd expect:

1
2
3
40
50
60

And you'd be right, that is the value of $C.  Now let's make one small change:

function GetArrayA
{
   $Array = @("1")
   Return  $Array
}

function GetArrayB
{
   $Array = @("40", "50", "60")
   Return  $Array
}

$A = GetArrayA
$B = GetArrayB

$C = $A + $B


Now the array $A only contains a single entry.  You would expect the contents of $C to look like this:

1
40
50
60

But you would be wrong.  Here's what you would actually get:

140
50
60

WHYYYY would it do such a thing?  Because the function GetArrayA did not return an array.  Since the array held only a single entry, it returned that object, a string, instead of the array.  And you can confirm that with the .GetType() function.  It then appended that string object to the first entry in array $B and went on its merry way.

You'd think when I realized this, realized that my code was correct but Powershell was sabotaging me, that I would've been relieved, but instead my wrath burned with the fire of a thousand suns. I trusted Powershell, Powershell was my friend, and suddenly it turned around and stabbed me in the back.

However there's a simple solution.  You just have to use a type declaration on the variables that will hold the array values, like this:

[array]$A = GetArrayA
[array]$B = GetArrayB

Despite the title of this post you're not actually forcing the function to return an array, you're forcing the variable to receive an array.  Which means you use that function frequently, you'll have to update a lot of code! But at least it WILL work.

I found that solution here:

https://stackoverflow.com/questions/11107428/how-can-i-force-powershell-to-return-an-array-when-a-call-only-returns-one-objec

If this helps even one of you avoid the painful consequences, then my suffering was worthy it.

Tuesday, July 21, 2020

Stupid Citrix Trick #5: Updating INI files

Problem: To configure some older programs we have to manipulate INI files. There's not a built-in way to do this in our preferred scripting language, Powershell.

Solution: Like so many things in IT, we must begin with a story about Lotus Notes.

For those not familiar with Notes, it's an email and workflow program, and its workflow capabilities are especially useful.  You can use it to set up a change request system, for example, with tickets being routed to various users for their approval.  For the time it was considered advanced.

So about ten years ago we had just upgraded Notes from version 7 to version 8.  The new version came with a redesigned graphical user interface which was much easier to use, but it still included the old GUI for users who preferred it.  Crucially, each interface had a different set of licenses, so if your account was licensed for the old interface, you needed to run that interface to stay in compliance.  Which interface the application launched with was configured by a command-line parameter, and since we had separate Active Directory groups for the users of each interface, it was simple to set up two Citrix published apps, each one assigned to the appropriate group and with the proper command-line settings.  And all was right with the world.

Then we changed our email system to Microsoft Exchange.  Users would not launch Notes to get their email as before; instead their workflow notifications would go to their Outlook inbox.  When they received one they'd just click a link in the message and it would launch Notes and take them directly to the right document.

And this was a huge problem! No longer could I use command-line parameters to configure the Notes interface.  Due to the limited number of licenses I couldn't just always use one or the other.  I had to find another way.

Oh, and just to add a dash of urgency, our senior executives were going on a retreat in a couple of weeks where they would only be able to access Outlook and Notes via Citrix, so I had to get this working ASAP.

But then, hallelujah! I found that you could use an INI setting in the Notes configuration file for each user that would configure the interface.  This INI file was stored in a known location in the user's home folder, so we could use a launch script for Outlook that would make sure that the INI file had the correct settings before launching, and when the user clicked a link to a Notes document, Notes would already be set up.

Too bad there's no way to change INI settings in Powershell.

Well, no native way.

Fortunately Powershell is the Swiss army knife of programming languages, and if something can be done, it can be done in Powershell, and in this case what we need to do is to use some functions from kernel.dll.

(Quick note: I am not an expert at this, and likely found it on the Internet. I'd like to give credit but this was at least eight years ago.)

For our purposes we'll pretend we're working with a file called "sample.ini", which contains this text:

[Section1]
Key1=FirstKey
Key2=SecondKey

[Section2]
Key3=ThirdKey

Here's the first function, Get-IniSetting, which reads from an INI file.  You can see the parameters are all required, and used to specify the file name, the section we want to work with, and the key we want to fetch.  The first and last are simple but for the section you'll want to use the name without the brackets. To get a key from [Section2] you'd just pass "Section2" to this function.


Function Get-IniSetting
{

[CmdletBinding()]
Param
(
    [Parameter(Mandatory = $True)]
    [string]$File,

    [Parameter(Mandatory = $True)]
    [string]$Section,

    [Parameter(Mandatory = $True)]
    [string]$Key
)

$Signature = @’
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
‘@

    $type = Add-Type -MemberDefinition $Signature -Name Win32Utils -Namespace GetPrivateProfileString -Using System.Text -PassThru

    $builder = New-Object System.Text.StringBuilder 1024

    $type::GetPrivateProfileString($Section, $Key, "", $builder, $builder.Capacity, $File) | Out-Null
   
    $builder.ToString()

}


Here's what a call to that function would look like. In this case the result would be the string value "SecondKey":

Get-IniSetting -File "C:\Users\Graham\Documents\sample.ini" -Section "Section1" -Key "Key2"

Next up is the function to write settings to an INI file.  If you look at the parameters you'll see that it has a parameter set, so based on the parameters passed it can do a couple of different things.


Function Write-IniSetting
{

[CmdletBinding()]
Param
(
    [Parameter(Mandatory = $True)]
    [string]$File,

    [Parameter(Mandatory = $True)]
    [string]$Section,

    [Parameter(ParameterSetName = "SetKey")]
    [string]$Key,

    [Parameter(ParameterSetName = "SetKey")]
    [string]$Value
)

$Signature = @’
[DllImport("kernel32.dll")]
public static extern bool WritePrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpString,
    string lpFileName);
‘@


    Switch ($PSCmdlet.ParameterSetName)
    {
        "SetKey"
            {
                If ($Value -ne (Get-IniSetting -File $File -Section $Section -Key $Key))
                {
                    $type = Add-Type -MemberDefinition $Signature -Name Win32Utils -Namespace WritePrivateProfileString -Using System.Text -PassThru

                    $type::WritePrivateProfileString($Section, $Key, $Value, $File) | Out-Null
                }
            }
        default
            {
                $type = Add-Type -MemberDefinition $Signature -Name Win32Utils -Namespace WritePrivateProfileString -Using System.Text -PassThru

                $type::WritePrivateProfileString($Section, [NullString]::Value, [NullString]::Value, $File) | Out-Null
            }
    }
}

This function does two different things based on what parameters are passed.  The first two, $File and $Section, are exactly the same as before, but the $Key and $Value parameters are optional.  If all four are specified then the function works as you'd expect, setting the value of the specified section and key, and creating it if it doesn't already exist.  Here's a what that call would look like:

Write-IniSetting -File "C:\Users\Graham\Documents\sample.ini" -Section "Section2" -Key "Key2" -Value "AnotherValue" 


But if you only include the $File and $Section parameters, it does something much different: it deletes the entire section. This can come in handy if you need to make wholesale changes.  This function call would delete Section2 and all the data it contains:

Write-IniSetting -File "C:\Users\Graham\Documents\sample.ini" -Section "Section2"

This is really a Stupid Powershell Trick more than a Stupid Citrix Trick - something that will come up many times - but it's extremely useful when dealing with older applications, all of which seem to live out their golden years on the Citrix farm.

Sunday, July 5, 2020

Stupid Citrix Trick #3: Fixing Provisioning Services Client Drive Letters

Problem: Citrix virtual machines using Provisioning Services are getting wrong drive letters.

Solution: Provisioning Services (hereafter "PVS") is a Citrix method of deploying and updating virtual machines using a standard image (or several different images).  I believe it predates Machine Creation Services ("MCS"), which is simpler to implement, but PVS is still very useful as the server image is stored as a file completely independent of any particular virtual machine; in MCS, a VM disk or an snapshot of one is replicated.

On a server running on PVS, the C: drive is virtual. It physically exists only as a file on the PVS server; each VM running the image has a local hard drive to cache the image file. But if you are not using the default Windows drive letters, this can cause problems if two circumstances are met: 1) not all of your VMs are deployed from the same template, and 2) you are not using the default drive letters.

First lets talk about the drive letters.  In PVS the virtual disk running from an image will always be C:.  By default, Windows will name the physical drive with the next drive letter, D:.  If you want to change that drive letter to anything else, then Windows needs to be able to remember your settings, and it does this by saving the selected drive letter and the unique ID of the drive in the registry.

So at your first site, the artfully named Site A, everything is fine and dandy.  You deploy a bunch of virtual machines, all from the same VM template, and they load the image. Raises and promotions for everyone!

But there's a problem.  When you create some VMs at the notoriously backwards and incompetent Site B, the drive letters are not correct.  In fact they revert to the defaults. All published applications that point to drive J: (an extremely cromulent drive letter) are failing because what should be J: is actually D:. What possibly could have gone wrong?

Remember when I said Windows saves a non-default drive letter in the registry, along with the drive's unique ID?  Different drives have different signatures, and if the ID doesn't match what's in the registry, default drive letters are used.

Happily there's a simple solution.  First, you'll need to create a scheduled task that runs at startup, or better yet, startup plus two minutes.  This task will first examine what's called a personality file, which PVS manages.  From this file you can tell if the disk is in private (editing) mode, or standard (running) mode.

If the disk is in private mode, the task saves the disk ID to a folder on the C: drive.  If it's in standard mode, the disk compares the saved ID to the local disk.  If they are not the same then it updates the disk ID and reboots. This works because disk IDs don't have to be globally unique, unlike a MAC address for example; they only have to be unique within the same computer.

Here's a sample script in Powershell:

# This program is used to ensure that all Citrix PVS session hosts
# have the same disk UniqueID value as the editing server where the
# image was last editied.
#
# To do this, it first looks in C:\Personality.ini and checks to see
# if the disk is in Private (_DiskMode=P) or Standard (_DiskMode=S)
# mode.  If Private mode, it SAVES the UniqueID to
# C:\SetDiskID\DiskID.txt.
#
# If Standard mode, it checks to see if the UniqueID matches the
# contents of C:\SetDiskID\DiskID.txt, and if it does not match,
# it SETS the ID using Diskpart.

# Set some variables.
$PvsDiskCaption = "Citrix Virtual Disk SCSI Disk Device"

$LaunchDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$LaunchDir = $LaunchDir.TrimEnd("\")

$DiskIDPath = $LaunchDir + "\DiskID.txt"
$DPInputPath = $LaunchDir + "\DpInput.txt"


# First, check to see what mode we are in.

$PersonalityFile = "c:\Personality.ini"

If (Test-Path -Path $PersonalityFile)
{

    # Read in the file.
    $Personality = Get-Content -Path $PersonalityFile

    # Now examine each line until you find the _DiskMode field.

    ForEach ($PrsLine in $Personality)
    {
        $Matched = $PrsLine -match "(.*)\="

        # If the first match group equals _DiskMode, save the value.
        If ($Matched -and ($Matches[1] -eq "_DiskMode"))
        {
            $PrsLine -match "\=(.*)"

            $DiskMode = $Matches[1]
        }
    }


    # Identify which disk is the non-PVS disk.

    # Get the disks in this server.  There will be at least two, a fixed disk and a PVS disk.
    $Disks = Get-Disk

    # Find the first disk that does NOT have type "Citrix Virtual Disk SCSI Disk Device".
    $TargetDisk = $null
    $DiskCount = $Disks.Count

    For ($CurrDisk = 0$CurrDisk -lt $DiskCount$CurrDisk++)
    {
        $Disk = $Disks.Item($CurrDisk)
                       
        If (-not ($Disk.Friendlyname -eq $PvsDiskCaption))
        {
            $TargetDisk = $Disk
            $TargetDiskIndex = $CurrDisk     # Disk index is important if we have to use Diskpart below.
            Break     # Exit once the disk is found.
        }
    }

    # If we found a disk to use...
    If (-not ($TargetDisk -eq $null))
    {
        # Get the UniqueID of the non-PVS disk in hexadecimal format.
        $UniqueID = $TargetDisk.Signature.ToString("X")

        # Now check to see what the mode is.
        switch ($DiskMode)
        {
            "P"     {
                        # Private mode. We need to capture the disk's UniqueID and save it to a file.
                        Set-Content -Value $UniqueID -Path $DiskIDPath
                    }


            "S"     {
                        # Standard mode.  We must check to see if the Unique ID matches what's in the file
                        # If not, add the correct value and then reboot.

                        # Read in the target UniqueID from file.
                        $TargetID = Get-Content -Path $DiskIDPath

                        If (-not ($TargetID -eq $UniqueID))
                        {
                            # Unique ID does not match, set it using Diskpart.

                            # Write Diskpart commands out to a text file, DpInput.txt.
                            Remove-Item -Path $DPInputPath -Force
                            Add-Content -Value ("Select Disk " + $TargetDiskIndex-Path $DPInputPath
                            Add-Content -Value ("UniqueID Disk ID=" + $TargetID-Path $DPInputPath
                            Add-Content -Value ("Exit"-Path $DPInputPath

                            # Run Diskpart to update the ID.
                            Start-Process -FilePath "C:\Windows\System32\Diskpart.exe" -ArgumentList ("/s " + $DPInputPath-Wait

                            Start-Sleep -Seconds 5

                            # Reboot to fix the drive letters.
                            Restart-Computer -Force
                        }
                    }
        }
    }
}