Skip to main content

Create offline backups of the NetScaler config

··8 mins
Author
John Billekens
Technical Consultant | End User Computing

I’ve created a PowerShell script that can be used to generate an (offline) backup of a Citrix NetScaler. If you want you can use the supplied batchfile for example to schedule the backup in Scheduled Tasks to run everyday. Some more information about the parameters used:

(Table removed during migration — content was stored in a WordPress plugin database.)

* Use the -NSCredential parameter or -NSUsername & -NSPassword, default is Username & Password ** Make sure to install WinSCP (msi) to use the default values or specify the location to the “WinSCPnet.dll” .Net assembly. You can download it here If you need to create a user just for the backup purpose, you can give it these minimal rights. This will be enough to create and download the backup.

(^sftp.*)|(^scp.*)|(^(create|rm)\s+system\s+backup)|(^(create|rm)\s+system\s+backup\s+.*)|(^(save|show)\s+ns\s+config)|(^(save|show)\s+ns\s+config\s+.*)

PowerShell Script (BackupNS.ps1):

<#
.SYNOPSIS
    Create a backup from the NetScaler and download a copy
.DESCRIPTION
    Create a backup from the NetScaler and download a copy
.PARAMETER NSManagementURL
    Management URL, used to connect to the NetScaler
.PARAMETER NSUserName
    NetScaler username with enough access to configure it
.PARAMETER NSPassword
    NetScaler username password
.PARAMETER NSCredential
    Use a PSCredential object instead of a username or password. Use "Get-Credential" to generate a credential object
    C:\PS> $Credential = Get-Credential
.PARAMETER WinSCPAssembly
    Specify the location for the WinSCP .NET assembly (Optional)
    When not specified the default location in the %ProgramFiles% / %ProgramFiles(x86)% will be used.
.PARAMETER BackupTargetLocation
    Specify the target location where to store the configuration and logfile
.PARAMETER NSBackupLevel
    Level to be used for the Backup. `"basic`" or `"full`" (Optional)
.EXAMPLE
    .\BackupNS.ps1 -NSManagementURL "http://nsvpx01.domain.local" -NSPassword "P@ssw0rd" -NSUserName "nsroot" -BackupTargetLocation "C:\Backup" -Verbose
    Create and download a backup from netscaler `"nsvpx01.domain.local`" and store it in `"C:\Backup`". And generate verbose output.
.EXAMPLE
    .\BackupNS.ps1 -NSManagementURL "http://192.168.100.1" -Credential $(get-credential) -Target "C:\Backup" -Verbose
    Create and download a backup from netscaler `"192.168.100.1`" and store it in `"C:\Backup`". And generate verbose output.
.NOTES
    File Name : BackupNS.ps1
    Version   : v0.3
    Author    : John Billekens
    Requires  : PowerShell v3 and up
                NetScaler 11.x and up
                Run As Administrator
                WinSCP
.LINK
    https://blog.j81.nl
#>

[cmdletbinding(DefaultParametersetName="UsernamePassword")]
param(
        [ValidateNotNullOrEmpty()]
        [alias("URL")]
        [string]$NSManagementURL,
        
        [Parameter(ParameterSetName="UsernamePassword",Mandatory=$true)]
        [alias("User", "Username")]
        [string]$NSUserName,
        
        [Parameter(ParameterSetName="UsernamePassword",Mandatory=$true)]
        [alias("Password")]
        [string]$NSPassword,

        [Parameter(ParameterSetName="Credential",Mandatory=$true)]
        [alias("Credential")]
        [ValidateScript({
            if ($_ -is [System.Management.Automation.PSCredential]) {
                $true
            } elseif ($_ -is [string]) {
                $Script:Credential=Get-Credential -Credential $_
                $true
            } else {
                Write-Error "You passed an unexpected object type for the credential (-NSCredential)"
            }
        })][object]$NSCredential,

        [Parameter(Mandatory=$true)]
        [alias("Target")]
        [string]$BackupTargetLocation,
        
        [Parameter(Mandatory=$false)]
        [ValidateSet("full", "basic")]
        [alias("Level")]
        [string]$NSBackupLevel="full",
        
        [Parameter(Mandatory=$false)]
        [string]$WinSCPAssembly = $null
)

#requires -version 3.0
#requires -runasadministrator

#region Functions

function InvokeNSRestApi {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [PSObject]$Session,

        [Parameter(Mandatory=$true)]
        [ValidateSet('DELETE', 'GET', 'POST', 'PUT')]
        [string]$Method,

        [Parameter(Mandatory=$true)]
        [string]$Type,

        [string]$Resource,

        [string]$Action,

        [hashtable]$Arguments = @{},

        [switch]$Stat = $false,

        [ValidateScript({$Method -eq 'GET'})]
        [hashtable]$Filters = @{},

        [ValidateScript({$Method -ne 'GET'})]
        [hashtable]$Payload = @{},

        [switch]$GetWarning = $false,

        [ValidateSet('EXIT', 'CONTINUE', 'ROLLBACK')]
        [string]$OnErrorAction = 'EXIT'
    )
    if ([string]::IsNullOrEmpty($($Session.ManagementURL))) {
        throw "ERROR. Probably not logged into the NetScaler"
    }
    if ($Stat) {
        $uri = "$($Session.ManagementURL)/nitro/v1/stat/$Type"
    } else {
        $uri = "$($Session.ManagementURL)/nitro/v1/config/$Type"
    }
    if (-not ([string]::IsNullOrEmpty($Resource))) {
        $uri += "/$Resource"
    }
    if ($Method -ne 'GET') {
        if (-not ([string]::IsNullOrEmpty($Action))) {
            $uri += "?action=$Action"
        }

        if ($Arguments.Count -gt 0) {
            $queryPresent = $true
            if ($uri -like '*?action*') {
                $uri += '&args='
            } else {
                $uri += '?args='
            }
            $argsList = @()
            foreach ($arg in $Arguments.GetEnumerator()) {
                $argsList += "$($arg.Name):$([System.Uri]::EscapeDataString($arg.Value))"
            }
            $uri += $argsList -join ','
        }
    } else {
        $queryPresent = $false
        if ($Arguments.Count -gt 0) {
            $queryPresent = $true
            $uri += '?args='
            $argsList = @()
            foreach ($arg in $Arguments.GetEnumerator()) {
                $argsList += "$($arg.Name):$([System.Uri]::EscapeDataString($arg.Value))"
            }
            $uri += $argsList -join ','
        }
        if ($Filters.Count -gt 0) {
            $uri += if ($queryPresent) { '&filter=' } else { '?filter=' }
            $filterList = @()
            foreach ($filter in $Filters.GetEnumerator()) {
                $filterList += "$($filter.Name):$([System.Uri]::EscapeDataString($filter.Value))"
            }
            $uri += $filterList -join ','
        }
    }
    Write-Verbose -Message "URI: $uri"

    $jsonPayload = $null
    if ($Method -ne 'GET') {
        $warning = if ($GetWarning) { 'YES' } else { 'NO' }
        $hashtablePayload = @{}
        $hashtablePayload.'params' = @{'warning' = $warning; 'onerror' = $OnErrorAction; <#"action"=$Action#>}
        $hashtablePayload.$Type = $Payload
        $jsonPayload = ConvertTo-Json -InputObject $hashtablePayload -Depth 100
        Write-Verbose -Message "JSON Payload:`n$jsonPayload"
    }

    $response = $null
    $restError = $null
    try {
        $restError = @()
        $restParams = @{
            Uri = $uri
            ContentType = 'application/json'
            Method = $Method
            WebSession = $Session.WebSession
            ErrorVariable = 'restError'
            Verbose = $false
        }

        if ($Method -ne 'GET') {
            $restParams.Add('Body', $jsonPayload)
        }

        $response = Invoke-RestMethod @restParams

        if ($response) {
            if ($response.severity -eq 'ERROR') {
                throw "Error. See response: `n$($response | Format-List -Property * | Out-String)"
            } else {
                Write-Verbose -Message "Response:`n$(ConvertTo-Json -InputObject $response | Out-String)"
                if ($Method -eq "GET") { return $response }
            }
        }
    }
    catch [Exception] {
        if ($Type -eq 'reboot' -and $restError[0].Message -eq 'The underlying connection was closed: The connection was closed unexpectedly.') {
            Write-Verbose -Message 'Connection closed due to reboot'
        } else {
            throw $_
        }
    }
}

function Connect-NetScaler {
    [cmdletbinding()]
    param(
        [parameter(Mandatory)]
        [string]$ManagementURL,

        [parameter(Mandatory)]
        [pscredential]$Credential = (Get-Credential -Message 'NetScaler credential'),

        [int]$Timeout = 3600,

        [switch]$PassThru
    )
    Write-Verbose -Message "Connecting to $ManagementURL..."
    try {
        $login = @{
            login = @{
                username = $Credential.UserName;
                password = $Credential.GetNetworkCredential().Password
                timeout = $Timeout
            }
        }
        $loginJson = ConvertTo-Json -InputObject $login
        Write-Verbose "JSON Data:`n$($loginJson | Out-String)"
        $saveSession = @{}
        $params = @{
            Uri = "$ManagementURL/nitro/v1/config/login"
            Method = 'POST'
            Body = $loginJson
            SessionVariable = 'saveSession'
            ContentType = 'application/json'
            ErrorVariable = 'restError'
            Verbose = $false
        }
        $response = Invoke-RestMethod @params

        if ($response.severity -eq 'ERROR') {
            throw "Error. See response: `n$($response | Format-List -Property * | Out-String)"
        } else {
            Write-Verbose -Message "Response:`n$(ConvertTo-Json -InputObject $response | Out-String)"
        }
    } catch [Exception] {
        throw $_
    }

    $session = [PSObject]@{
        ManagementURL=[string]$ManagementURL;
        WebSession=[Microsoft.PowerShell.Commands.WebRequestSession]$saveSession;
    }

    $Script:NSSession = $session
    
    if($PassThru){
            return $session
    }
}

#endregion Functions

#region Script variables

[string]$ScriptDateTime = (Get-Date).ToString("yyyyMMddHHmm")
[string]$WinSCPSite = "https://winscp.net/eng/download.php"
[string]$WinSCPErrorSite = "https://winscp.net/eng/docs/message_net_operation_not_supported"
[string]$WinSCPAssemblyx86 = "C:\Program Files\WinSCP\WinSCPnet.dll"
[string]$WinSCPAssemblyx64 = "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
[string]$WinSCPAssemblyScript = Join-Path $(Split-Path $MyInvocation.MyCommand.Path -Parent) "WinSCPnet.dll"
[ipaddress]$NSHostIP = [System.Net.Dns]::GetHostAddresses($NSManagementURL.replace("https://","").replace("http://","").replace("/","")) | select-object IPAddressToString -expandproperty  IPAddressToString
[string]$BackupFilename = "ns-backup-$($NSHostIP)-$($ScriptDateTime)"
[string]$BackupTargetLocation = $BackupTargetLocation.Trim("\")

#endregion Script variables

#region Target Directory

if ( -Not (Test-Path $BackupTargetLocation)) {
    New-Item -Path $BackupTargetLocation -ItemType Directory -Force | out-null
}

#endregion Target Directory

#region NSCredential
    
if (-not([string]::IsNullOrWhiteSpace($NSCredential))) {
    Write-Verbose "Using NSCredential"
} elseif ((-not([string]::IsNullOrWhiteSpace($NSUserName))) -and (-not([string]::IsNullOrWhiteSpace($NSPassword)))){
    Write-Verbose "Using NSUsername / NSPassword"
    [pscredential]$NSCredential = new-object -typename System.Management.Automation.PSCredential -argumentlist $NSUserName, $(ConvertTo-SecureString -String $NSPassword -AsPlainText -Force)
} else {
    Write-Verbose "No valid username/password or credential specified. Enter a username and password, e.g. `"nsroot`""
    [pscredential]$NSCredential = Get-Credential -Message "NetScaler username and password:"
}

#endregion NSCredential

#region Backup

try {
    Write-Verbose "Login to NetScaler and save session to global variable"
    $NSSession = Connect-NetScaler -ManagementURL $NSManagementURL -Credential $NSCredential -PassThru
    Write-Verbose "Saving NetScaler configuration"
    $response = InvokeNSRestApi -Session $NSSession -Method POST -Type nsconfig -Action save
    $payload = @{"filename"="$($BackupFilename)";"level"="$($NSBackupLevel)";"comment"="Backup created by BackupNS.ps1 PoSH Script"}
    $response = InvokeNSRestApi -Session $NSSession -Method POST -Type systembackup -Payload $payload -Action create
    
    try {
        Write-Verbose "Loading WinSCP .NET assembly"
        if (-not [string]::IsNullOrWhiteSpace($WinSCPAssembly)){
            if (Test-Path $WinSCPAssembly) {
                Write-Verbose "`"$WinSCPAssembly`" will be used"
            }
        } else {
            if (Test-Path $WinSCPAssemblyx64) {
                $WinSCPAssembly = $WinSCPAssemblyx64
            } elseif (Test-Path $WinSCPAssemblyx86) {
                $WinSCPAssembly = $WinSCPAssemblyx86
            } elseif (Test-Path $WinSCPAssemblyScript) {
                $WinSCPAssembly = $WinSCPAssemblyScript
                
            } else {
                start $WinSCPSite
                throw "The .NET Assembly could not be found"
            }
            Write-Verbose "using: $WinSCPAssembly"
        }
        Add-Type -Path "$WinSCPAssembly"
        Write-Verbose "assembly successfully locaded"
    
        Write-Verbose "Setup WinSCP session options"
        $WinSCPSessionOptions = New-Object WinSCP.SessionOptions
        $WinSCPSessionOptions.Protocol = [WinSCP.Protocol]::sftp
        $WinSCPSessionOptions.HostName = "$($NSHostIP.IPAddressToString)"
        $WinSCPSessionOptions.UserName = "$($NSCredential.UserName)"
        $WinSCPSessionOptions.Password = "$($NSCredential.GetNetworkCredential().Password)"
        $WinSCPSessionOptions.GiveUpSecurityAndAcceptAnySshHostKey = $true
    
        $WinSCPSession = New-Object WinSCP.Session
        Write-Verbose "Enable Logging"
        $WinSCPSession.SessionLogPath = "$($BackupTargetLocation)\$($BackupFilename)-log.txt" 
        try {
            Write-Verbose "Connecting"
            $WinSCPSession.Open($WinSCPSessionOptions)
    
            Write-Verbose "Try to download the backup file"
            $WinSCPTransferOptions = New-Object WinSCP.TransferOptions
            $WinSCPTransferOptions.TransferMode = [WinSCP.TransferMode]::Binary
            $WinSCPTransferResult = $WinSCPSession.GetFiles("/var/ns_sys_backup/$($BackupFilename).tgz", "$($BackupTargetLocation)\$($BackupFilename).tgz", $False, $WinSCPTransferOptions)
    
            Write-Verbose "Throw on any error"
            $WinSCPTransferResult.Check()
    
            Write-Verbose "Print results"
            foreach ($transfer in $WinSCPTransferResult.Transfers) {
                Write-Host ("Upload of {0} succeeded" -f $transfer.FileName)
            }
        } finally {
            Write-Verbose "Disconnect, clean up"
            $WinSCPSession.Dispose()
        }
    } catch [System.IO.IOException]{
        Start $WinSCPErrorSite
        Write-Error "DLL was probably downloaded with Internet Explorer, unblock before extracting"
        throw $($_.Exception.Message)
    } catch {
        throw $($_.Exception.Message)
    }
} catch {
    throw $($_.Exception.Message)
} finally {
    Write-Verbose "Removing Backup file from NetScaler"
    $response = InvokeNSRestApi -Session $NSSession -Method DELETE -Type systembackup -Resource "$($BackupFilename).tgz"
}

#endregion Backup

Batchfile (BackupNS.cmd):

@ECHO OFF
setlocal EnableDelayedExpansion
REM  --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"

REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
    echo Requesting administrative privileges...
    goto UACPrompt
) else ( goto gotAdmin )

:UACPrompt
    echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
    set params = %*:"=""
    echo UAC.ShellExecute "cmd.exe", "/c %~s0 %params%", "", "runas", 1 >> "%temp%\getadmin.vbs"

    "%temp%\getadmin.vbs"
    del "%temp%\getadmin.vbs"
    exit /B

:gotAdmin
    pushd "%CD%"
    CD /D "%~dp0"

goto StartScript

rem ===== Help Example =====

SET OPTIONS=-NSManagementURL "http://nsvpx01.domain.local"
SET OPTIONS=%OPTIONS% -NSPassword "P@ssw0rd"
SET OPTIONS=%OPTIONS% -NSUsername "nsroot"
SET OPTIONS=%OPTIONS% -BackupTargetLocation "C:\Backup"
SET OPTIONS=%OPTIONS% -Verbose

NOTE: Use the "-Verbose" parameter to get diagnostic output

rem ===== End Help Example =====

:StartScript

SET OPTIONS=-NSManagementURL "http://nsvpx01.domain.local"
SET OPTIONS=%OPTIONS% -NSPassword "P@ssw0rd"
SET OPTIONS=%OPTIONS% -NSUsername "nsroot"
SET OPTIONS=%OPTIONS% -BackupTargetLocation "C:\Backup"
SET OPTIONS=%OPTIONS% -Verbose

%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -File "%~dp0BackupNS.ps1" %OPTIONS%

Edit (08-04-2017, 0.2) Added extra info for when the dll was downloaded with IE, and returned an error. Edit (29-01-2019, 0.3) Added rights for a possible backup user and changed some typo’s, thank you Chris for pointing that out to me. Hope this can help you. If you have questions, please let me know.

Related

Generate an Let's Encrypt certificate what can be used on the NetScaler

··1 min
Edit 07-04-2017: Check out my new and updated version! I’m trying to create an (PowerShell) script to automate the Let’s Encrypt certificate creation. Specifically for the Citrix NetScaler. Currently still Work In Progress… It’s not yet finished. The prerequisite is that you have a configured NetScaler (http) Content Switch vServer. The script will present you with the required configuration rules (it will also be copied to your clipboard so you only have to copy it in the cli of the NetScaler) For the meantime you can find it on GitHub: GenCertForNS on GitHub More soon (I hope)…

The case of the empty Start Menu (Windows 10)

··5 mins
During a project I’m currently working on, with Windows 10, Citrix Xendesktop 7.9, XenServer 7.0 and RES ONE Workspace 2015 SR2 I stumbled upon a issue with RES ONE Workspace and the pinning of items in the Start Menu. I noticed that sometimes my Start Menu was empty, while I had items pinned when I logged off!? After some investigation with an engineer from RES Software, we managed to reproduce the issue in a closed test environment. At this point RES can try to fix the issue and at the time of writing no known solution is available. We still need to verify but as far as we know the issue is also still in the new version RES ONE Workspace 2016. We still needed a filled Start Menu for the time being, because currently there is no known date for the possible fix… So I created a PoSh script that will fill the Start Menu. (for the 2nd time, after the RES composer is finished) Yes I know not very pretty solution but it gets the job done and it’s a temporary fix. So here is the script I’ve made. (Building block is also available at the end for download)

OptimizeEndpoint

·1 min
I’ve been using my “Windows optimize script” for a while now. Most issues are resolved and it’s been tested thoroughly. So I thought why not give it back to the community, so here it is: OptimizeEndpoint. It can be used to optimize Windows 7, 8, 8.1 and 10. (It can also be used for Windows Server versions, but this is not tested) I used the script made by Ingmar Verheij, and made some changes. It contains most of the Citrix XenDesktop Best Practices. Please don’t run the script without reviewing the options, it can damage you master image if you’re not careful! At the top of the image there are some parameters that can be set. Read the comments. Run it on your own risk. If you have issues or questions let me know.