Create offline backups of the NetScaler config

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.