Updated 10.31.24! I modified the script from using the vm_recovery.bat to do everything, to calling a powershell script since that gave some extra flexibility in validating the current IP and DNS settings.

One of the best features of the Nutanix DR Recovery Plan is the ability to automate IP address changes based on failover criteria. Whether using Async, Near Sync or Sync rep, you have the ability to create a recovery plan that will automate the failover of specific VM(s), or by using a category to capture a group of VMs. I always try to use a Category when possible, to remove any possibilities of missing VMs that I do want to failover. The recovery plan allows me to set a power on sequence to the VMs that are part of the recovery plan, as well as modifying the network association and IP address to match the recovery site.

One item that is missing from the Nutanix DR capabilities natively is the ability to change the DNS Servers associated with the VM. In the case where VMs that failover from site to site, it might be necessary to update the DNS addresses for that specific site. Zerto provides this capability natively alongside the updating of the IP Address, but this feature still does not exist in Nutanix DR recovery plans. Further down we’ll talk about modifying the DNS, which currently requires either a script using the Nutanix Guest Tools (NGT), or other automation using something like Ansible. For the purpose of this post, we’re using a custom batch script.

Let’s briefly go over a few steps in the creation of a recovery plan. To create a recovery plan, we first must have created protection policies for protecting VMs between Availability Zones or clusters. This could be a single PC instance with multiple clusters, or multiple PC instances separated by sites.

To create the recovery plan, from within the PC instance that has the protection policy, create a new recovery plan, add the VM(s) or Categories to be part of the plan, and set any VM Power On Sequences by adding stages.

VM Recovery Script

With Nutanix DR, to automate in-guest script execution during a recovery plan activity, you will need to Enable custom scripts. To do this, once a VM is added to the recovery plan, click Manage Scripts and enable. To utilize this feature, the VM must have NGT installed, and the script must be installed in the proper path.

The script must be named vm_recovery, and depending on whether this is for Windows or Linux, the folder path is pretty specific. In many cases, you will find that the scripts folder does not exist, and will need to be manually created, as well as the production and test folders. I also found on windows that sometimes I needed to add .bat to the file name, even though I saved it as a batch file.

Windows VMs:

  • – Batch script file path for production failover:
    • – C:\Program Files\Nutanix\scripts\production\vm_recovery
  • – Batch script file path for test failover:
    • – C:\Program Files\Nutanix\scripts\test\vm_recovery

Linux VMs:

  • – Shell script file path for production failover:
    • – /usr/local/sbin/production_vm_recovery
  • – Shell script file path for test failover:
    • – /usr/local/sbin/test_vm_recovery

Network Mapping

Network Mapping allows you to map the different virtual networks that will be part of the recovery plan. This features allows for mapping both a Live and Test Failover network for the recovery plan. If the networs are stretched via Layer 2, VXLAN or another mechanism, this eliminates the need for IP changes in most cases.

Recovery Plan Configuration: Network Settings

One important feature of the Network Mapping is how Nutanix DR automates the IP changes. By default, the first 3 octets of the IP Address will change on failover, and the 4th octet of the VM will remain. For example in my lab, I have address space of 10.10.16.x/24 for the primary site, and 10.10.216.x/24 for the secondary site. A VM using 10.10.16.100 will be changed to 10.10.216.100 during the recovery plan failover process. This means that if you’re failing over into an existing network that has IP’s already in use, this could pose a problem. To get around this, the recovery plan has the ability for Custom IP Mapping, enabling the VM to have it’s failover IP Address modified from the current 4th octet to ensure not overlaps.

DNS Modification

As mentioned there are many different ways to do this, but in my case I’m using the vm_recovery.bat script to update the DNS servers, based on the VMs detected IP range. In my lab, I have my VMs in the primary site on the 10.10.16.0/24 subnet, and upon failover they will go to 10.10.216.0/24. My DNS Servers in the primary site are 172.20.19.253 and .254, while the at the secondary site they are 172.20.20.253 and .254. While the VMs in both sites can talk to all 4 DNS servers, in the event VMs have failed over I want them to use the DNS servers at their local site.

The script below is what I use to do this. I set the variables for the DNS servers by the site (and VLAN), and then use powershell to get the IP Address of the VM, and based on the first 10 values of the IP address, assign the proper DNS servers. This will work for both failover to the secondary site, and back to the primary site.

Update!

Since the original posting I updated the script to make it a bit more flexible. Since we have to use the vm_recovery.bat batch file for guest customizations, I went ahead and had the script call a specific powershell script, which allowed me to do some better checks against the current IP and DNS settings, and make better decisions on those changes.

The vm_recovery.bat file, with logging enabled.

@echo off
:: -------------------------------------------------------------------------
:: vm_recovery.bat for Nutanix Disaster Recovery
:: -------------------------------------------------------------------------
:: Purpose:
::     This batch file initiates a PowerShell script to check and update 
::     the DNS configuration based on the current IP address. It logs all 
::     activities to a specified log file, creating a new log file with 
::     the current date if the previous log exists.
::
:: Description:
::     - Checks if an existing log file (vm_recovery.log) is present.
::       If it exists, renames it with the current date.
::     - Calls the PowerShell script (check_dns.ps1) in the same directory
::       to perform DNS checks and updates as required.
::
:: Requirements:
::     - Must run with administrative privileges.
::     - Requires PowerShell to be installed.
::
:: Version:
::     1.0
::
:: Author:
::     Mike Dent (mike.dent@egroup-us.com)
:: -------------------------------------------------------------------------

set "scriptDir=%~dp0"
set "logDir=C:\Program Files\Nutanix\scripts"
set "logFile=%logDir%\vm_recovery.log"

REM Check if log file exists and rename it with the current date if it does
if exist "%logFile%" (
    set "dateStr=%date:/=-%"
    ren "%logFile%" "vm_recovery_%dateStr%.log"
)

REM Call the PowerShell script and pass the log directory
powershell -ExecutionPolicy Bypass -File "%scriptDir%vm_recovery.ps1" -LogDir "%logDir%"

The vm_recovery.bat file calls the vm_recovery.ps1 PowerShell script, which does the heavy lifting. The PowerShell sscript uses variables for the Subnets and DNS servers to check, and ultimately prints out the current and updated DNS entries.

<#
.SYNOPSIS
    Nutanix Disaster Recovery DNS and IP Configuration Script

.DESCRIPTION
    This script checks the current IP address against predefined subnets for Site A and Site B. If the IP address
    matches a subnet and the DNS servers are different from the defined configuration, it updates the DNS servers
    accordingly.

.VERSION
    1.4

.AUTHOR
    Mike Dent (mike.dent@egroup-us.com)

.NOTES
    - Called from vm_recovery.bat
    - Logs all actions to "C:\Program Files\Nutanix\scripts\vm_recovery.log"
    - Maintains a summary table in the log file of previous and updated DNS configurations

.PARAMETER LogDir
    Directory to save the log file.
#>

param (
    [string]$LogDir
)

# Define the path for the log file
$LogFile = Join-Path -Path $LogDir -ChildPath "vm_recovery.log"

# Function to write log entries
function Write-Log {
    param ([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $entry = "$timestamp - $Message"
    Add-Content -Path $LogFile -Value $entry
}

# Start logging
Write-Log "-------------------------------------------------------------"
Write-Log "DNS and IP Configuration Checker - Version 1.4"
Write-Log "Script initiated to verify and update DNS settings if needed"
Write-Log "-------------------------------------------------------------"

# Define subnet and DNS information for Site A and Site B
$sites = @{
    "SiteA" = @{
        "Subnets" = @("172.x.x.0/24", "172.x.x0/24")
        "DNSServers" = @("172.x.x.250", "172.x.x.251")
    }
    "SiteB" = @{
        "Subnets" = @("172.x.x.0/24", "172.x.x0/24")
        "DNSServers" = @("172.x.x.250", "172.x.x.251")
    }
}

# Get the current IP and DNS information
$currentIP = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.IPAddress -ne "127.0.0.1"}).IPAddress
$currentDNS = (Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses)

Write-Log "Current IP Address: $currentIP"
Write-Log "Current DNS Servers Detected: $($currentDNS -join ', ')"

# Store previous and new DNS values for logging
$previousDNS = $currentDNS
$newDNS = $null

# Function to check if an IP belongs to a subnet
function Test-Subnet {
    param (
        [string]$IP,
        [string]$Subnet
    )
    $ipAddress = [System.Net.IPAddress]::Parse($IP)
    $subnetAddress, $prefixLength = $Subnet -split '/'
    $subnetMask = [System.Net.IPAddress]::Parse($subnetAddress)
    $networkBits = [int]$prefixLength
    $subnetBytes = $subnetMask.GetAddressBytes()
    $ipBytes = $ipAddress.GetAddressBytes()

    for ($i = 0; $i -lt $networkBits / 8; $i++) {
        if ($ipBytes[$i] -ne $subnetBytes[$i]) {
            return $false
        }
    }
    return $true
}

# Get the active network adapter’s InterfaceAlias
$activeAdapter = Get-NetAdapter | Where-Object {$_.Status -eq "Up"}
if (-not $activeAdapter) {
    Write-Log "Error: No active network adapter found. Exiting."
    exit
}
$interfaceAlias = $activeAdapter.InterfaceAlias

Write-Log "Active Network Adapter InterfaceAlias: $interfaceAlias"

# Determine site based on current IP and update DNS if necessary
foreach ($site in $sites.Keys) {
    $siteData = $sites[$site]
    $matchingSubnet = $siteData["Subnets"] | Where-Object { Test-Subnet -IP $currentIP -Subnet $_ }
    
    if ($matchingSubnet) {
        Write-Log "Matched $currentIP to subnet $matchingSubnet in $site"
        
        # Strictly compare DNS server settings
        $desiredDNS = $siteData["DNSServers"]
        $dnsMismatch = ($currentDNS.Count -ne $desiredDNS.Count) -or ($currentDNS | Where-Object { $_ -notin $desiredDNS })

        if (-not $dnsMismatch) {
            Write-Log "DNS servers are already correctly configured for $site. No changes needed."
            $newDNS = $currentDNS  # New DNS remains the same
        }
        else {
            Write-Log "DNS servers mismatch detected. Expected: $($desiredDNS -join ', '), Current: $($currentDNS -join ', ')"
            Write-Log "Attempting to update DNS servers to $($desiredDNS -join ', ') for $site"
            
            # Clear existing DNS servers
            try {
                Set-DnsClientServerAddress -InterfaceAlias $interfaceAlias -ServerAddresses @()
                Write-Log "Cleared existing DNS servers on $interfaceAlias."
                
                # Set new DNS servers
                Set-DnsClientServerAddress -InterfaceAlias $interfaceAlias -ServerAddresses $desiredDNS -ErrorAction Stop
                Write-Log "DNS servers updated successfully to $($desiredDNS -join ', ') on $interfaceAlias."
                $newDNS = $desiredDNS  # New DNS after update
            }
            catch {
                Write-Log "Error: Failed to update DNS servers. $_"
            }

            # Verify the DNS settings after update
            $updatedDNS = (Get-DnsClientServerAddress -InterfaceAlias $interfaceAlias -AddressFamily IPv4).ServerAddresses
            if ($updatedDNS -join ',' -eq $desiredDNS -join ',') {
                Write-Log "DNS update verification passed: $($updatedDNS -join ', ')"
            } else {
                Write-Log "Warning: DNS update verification failed. Expected: $($desiredDNS -join ', ') | Actual: $($updatedDNS -join ', ')"
            }
        }
        break
    }
}

# Summary table in log file
Write-Log "-------------------------------------------------------------"
Write-Log "Summary of DNS Configuration Changes"
Write-Log "-------------------------------------------------------------"
Write-Log "| Parameter       | Previous Value             | Current Value           |"
Write-Log "|-----------------|---------------------------|--------------------------|"
Write-Log "| IP Address      | $currentIP                 | $currentIP               |"
Write-Log "| DNS Servers     | $($previousDNS -join ', ') | $($newDNS -join ', ')   |"
Write-Log "-------------------------------------------------------------"

Write-Log "DNS and IP check completed."

Recap

Nutanix DR provides a very robust method for protecting VMs between sites and/or clusters, with automated failover and ip address modification. To ensure we have full resiliency, adding in a custom script to modify the DNS servers based on the site also eliminates any manual intervention during failover or failback activities. The beauty of the vm_recovery scripts are that it’s not limited to just DNS changes, but anything you can might need to script post failover can be added.

I’d love to know how you’re handling any custom modifications to VMs during failovers, and what you’re’ using for that, so let me know!

Understanding DNS Changes in Nutanix DR Recovery Plans
Tagged on:             

Leave a Reply

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