| |

Protecting your Citrix desktops on Nutanix using powershell

4 min read

Archive notice (October 2026): This post is from 2017 and stays online for reference. It uses the legacy Nutanix PowerShell snap-in, which only runs in Windows PowerShell 5.1, and Prism Element Protection Domains. Nutanix deprecates its legacy v0.8, v1, v2 and v3 APIs starting with the AOS and Prism Central release planned for Q4-CY2027, and all new DR features are built on Prism Central. To protect persistent Citrix desktops today, put the catalog VMs in a Prism Central category and attach a protection policy to that category, so new desktops are covered without a script. Still running Protection Domains? Move them with the PD to Prism Central-based DR migration tool. Don’t run the script below as-is: it stops after the first VM, and with the default variables its PD sizing math fails.

Another week, another idea comes up to check if I could make it happen with Powershell. I wanted to write a script that checks my current machine catalog and puts VMs that are not in a Nutanix Protection Domain in a PD to make sure that we can protect those VMs using the policies Nutanix offers.

The following tasks had to be automated using powershell:

  • Check if a machine catalog is persistent
  • Get all desktops in the machine catalog
  • Check if there’s a Nutanix Protection Domain available with a certain prefix (read more here).
  • Check if the VM isn’t already protected
  • Check if the VM can be added there
    • If so: Add the VM
    • If not: create a Protection Domain and add the VM(s) there

I took the liberty of borrowing bits and pieces of Steven Poitras’ NTNX-Protect-VM.ps1 PowerShell script, originally published on the Nutanix Automation GitHub repo, and Sandeep has helped me by answering my questions patiently. 

 

Here’s the script:

<# 
.SYNOPSIS 
Will fetch all desktops from a machine catalogs and put them into protection domains 
.DESCRIPTION 
Will fetch all desktops from a machine catalogs and put them into protection domains but it will asume at least one PD with the prefix is there.
.EXAMPLE 
PS C:\PSScript > .\XD_PD_v1.ps1
.INPUTS
None.  You cannot pipe objects to this script.
.OUTPUTS
No objects are output from this script.  
.NOTES
NAME: XD_PD_v1.ps1
VERSION: 1.0
AUTHOR: Kees Baggerman with some help from Sandeep MP and Steven Potrias a
LASTEDIT: August 2017
#>

[CmdletBinding(SupportsShouldProcess = $False, ConfirmImpact = "None", DefaultParameterSetName = "") ]

Param(      
    # Nutanix cluster IP address
    [Parameter(Mandatory = $false)]
    [Alias('IP')] [string] $nxIP,
    # Nutanix cluster username
    [Parameter(Mandatory = $false)]
    [Alias('User')] [string] $nxUser,
    # Nutanix cluster password
    [Parameter(Mandatory = $true)]
    [Alias('Password')] [Security.SecureString] $nxPassword
    )

# Importing the proper cmldlets and snapins
Import-Module "C:\Program Files (x86)\Nutanix Inc\NutanixCmdlets\Modules\NutanixCmdletsPSSnapin.dll"
Add-PSSnapin Nutanix*, Citrix*

# Defining script variables
$DDC = ""
$nxIP = ""
$nxUser = ""
$pdPrefix = "XenDesktop-PD"
$vmPerPD = ""
$MachineCatalog = ""

# Connecting to the Nutanix Cluster IP
Connect-NutanixCluster -Server $nxIP -UserName $nxUser -Password $nxPassword -AcceptInvalidSSLCerts

# Checking if the Machine catalog is persistent
$persistentMCs = Get-BrokerCatalog | where {$_.PersistUserChanges -eq "OnLocal"}

if($persistentMCs.name -match $MachineCatalog) {

# Collecting the Persistent Machine Catalog and putting them into a variable
$VMs = get-brokermachine -adminaddress $ddc -CatalogName $machinecatalog

foreach ($VM in $VMs) {
        $VMName = $VM.MachineName.Split("\")[1]

        # Get existing PDs matching prefix
        $pds = Get-NTNXProtectionDomain | where {$_.name -match $pdPrefix} 

        # Get Un-protected VMs
        $unProtectedVM = Get-NTNXUnprotectedVM | where {$_.vmName -eq $VMName}

        # Stop the script when there's no unprotected VMs
        if ($unProtectedVM.count -gt 0) {
            Write-Host "Found $($unProtectedVM.count) unprotected vms..."
        } else {
            Write-Host "No unprotected VMs found, exiting..."
        break

        }
        # Getting the PDs count and determine next action
        if ($pds.Count -gt 0) {
        
        # Matching PDs exist
            Write-Host "Found $($pds.Count) matching existing PDs with space..."
    
          [int]$existingPDSpace = 0
    
        # Find available space in existing PDs
        $pdsWithSpace = $pds | where {($_.vms).count -lt $vmPerPD}
    
        $pdsWithSpace | %{
        
        # Find available space
        $l_pdSpace = $vmPerPD - ($_.vms).count
        
        # Add available space to counter
        $existingPDSpace += $l_pdSpace
        }
    
        # Find starting increment from existing
        $startingInt = (($pds.name).split("-") -match "^[0-9]+$" | measure -Maximum).maximum + 1
        } else {
            Write-Host "Found $($pds.Count) matching existing PDs with space..."
            $startingInt = 0
        }
        
        # Find needed space
        $newPDSpace = if ($existingPDSpace -lt $unProtectedVM.Count) {$unProtectedVM.Count - $existingPDSpace}

        # Find number of new PD required
        $numNewPD = [Math]::Round($newPDSpace/$vmPerPD)

        # Create $numNewPD Protection Domains
        $pdsWithSpace | %{
            
        # Get avail space
        $l_availSpace = $vmPerPD - ($_.vms).count }

        # Find number of new PD required
        $numNewPD = [Math]::Round($newPDSpace/$vmPerPD)

        # Create $numNewPD Protection Domains
        Write-Host "Creating $numNewPD new PDs..."

        if ($numNewPD -gt 0) {
            for($i = 0;$i -lt $numNewPD;$i++)
            {
                       # $l_pdName = $pdPrefix+"-"+$i
                       $l_pdName = "$pdPrefix-$i"

         # Create PD
         Write-Host "Creating PD: $l_pdName"
         $l_PD = Add-NTNXProtectionDomain -value $l_pdName
        
        # Add cron schedule to PD
        $pdsWithSpace += $l_PD
         }
        }
            $pdsWithSpace | %{
        # Get avail space
        $l_availSpace = $vmPerPD - ($_.vms).count }
        
        # Add VMs to PD (either existing or new)
        if ($Pds.name -notmatch $pdPrefix) {
        Write-Host "Adding $($l_vmObj.count) VMs to PD: $($_.name)"
        Add-NTNXProtectionDomainVM -Name $VMName -PdName $($_.name)
        }
        else {
        Write-Host "Adding VMs to PD: $($pds.name)"
        Add-NTNXProtectionDomainVM -Name $VMName -PdName $($pds.name)
        }

        break
        }
        }
        else { write-host "No persisent machine catalog defined"}

 

The following two tabs change content below.

Kees Baggerman

Kees Baggerman is Senior Technical Director — Performance & Solutions Engineering R&D at Nutanix, where he leads a global team responsible for defining how enterprise applications are delivered on the Nutanix platform. A former Citrix Technology Professional and NVIDIA Enterprise Platform Advisor, he has spent 15+ years driving EUC strategy and technical direction across architecture, product, and customer success. He has been writing here since 2011 — sharing what he learns at the intersection of platform engineering and enterprise IT.
Kees Baggerman

Kees Baggerman

Senior Technical Director at Nutanix - Former Citrix CTP - NVIDIA Enterprise Platform Advisor - 15+ years in EUC

Kees Baggerman is Senior Technical Director — Performance & Solutions Engineering R&D at Nutanix, where he leads a global team responsible for defining how enterprise applications are delivered on the Nutanix platform. A former Citrix Technology Professional and NVIDIA Enterprise Platform Advisor, he has spent 15+ years driving EUC strategy and technical direction across architecture, product, and customer success. He has been writing here since 2011 — sharing what he learns at the intersection of platform engineering and enterprise IT.

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.