ABX Action – DNS Record

This ABX Action will automatically create and delete DNS A, PTR, and CNAME records as part of a vRA Extensibility Subscription. This ABX is written to work with Microsoft DNS server and has been tested on the following software versions:

  • MS Active Directory/DNS Server 2019
  • vRA 8.5.0

Features

  • Action Constants providing values/secrets for:
    • domain_username: service account with sufficient rights to create and delete DNS records in UPN format – e.g. – username@mydomain.local
    • domain_password: password for the above account
    • dns_server1: DNS server FQDN e.g. – dns1.mydomain.local
    • dns_server2: DNS server FQDN (optional)
    • domain_name: e.g. –  mydomain.local
  • Custom property for the desired CNAME passed from the vRA deployment. This can be statically defined in the blueprint or requested as a user input
    • This property is optional and does not need to be present in the blueprint if you do not wish to have a CNAME
    • The CNAME will point to the hostname DNS A record
  • Custom property create_dns used to target the action subscription
  • Authentication to DNS using CredSSP secure transport
  • IP Address and hostname imported from the vRA deployment
  • Removes A, PTR, and CNAME records if the deployment is deleted
  • Imports the pywinrm[credssp] python module to establish the connection

Pre-requisites

1. Account permissions:

A service account member of the Domain Admins group is required for Active Directory integrated DNS zones. This script has not been tested on Non-AD integrated DNS zones.

2. Enable PowerShell Remoting on each DNS Server:

Enable-psremoting -force

3. Enable CredSSP Authentication on each DNS Server:

Enable-WSManCredSSP -Role Server

4. Enable HTTPS for Windows Remote Management (WS-Management) service on each DNS server:

  • Install a valid SSL certificate on the DNS server (self-signed, internal CA or Public).
  • Configure the WinRM Listener for HTTPS.
  • Open the Windows Firewall port.

Detailed instructions can be found in the following article: https://adamtheautomator.com/winrm-ssl

5. Grant the NETWORK SERVICE on each DNS Server group Full Control permissions over the certificate’s private key

  1. Open the Microsoft Management Console and add the Certificates Snap-in.
  2. Expand Certificates (Local Computer) > Personal > Certificates.
  3. Right-click the certificate, and select All Tasks > Manage Private Keys.
  4. Add the NETWORK SERVICE user to the list of groups and user names.
  5. Select the NETWORK SERVICE user and grant it Full Control rights.
  6. Click OK

6. Create a DNS reverse domain zone if you need PTR records

Script Execution

This is the flow of the python script – note: python is used with the pywinrm module because it did not work with the PS Core container spun up by vRA.

  1. Import the action constants
  2. Import the hostname, IP address, cnamerecord, and deployment event status from vRA
  3. Establish a connection to DNS Server 1. If this server is unavailable, try DNS Server 2, else fail
  4. Check the current deployment event topic
    • If the event topic is “compute.provision”, then:
      1. Create the A and PTR records
      2. Create a CNAME record if it was defined in the blueprint
    • If the event topic is “compute.removal”, then:
      1. Delete the A and PTR records
      2. Delete the CNAME record if it is found in the reverse zone

Action

Create the DNS Record action as follows:

1. Copy the following python code:

# ABX Action for lifecycle of DNS records in vRA deployments
# Created by Dennis Gerolymatos and Guillermo Martinez
# Version 1.0 - 24.10.2021

import winrm, sys

def handler(context, inputs):

    event         = str(inputs["__metadata"]["eventTopicId"])    # Gets the deployment Event Topic 
    ip_raw        = inputs["addresses"]                          # Raw IP input created vy vRA IPAM
    ipaddress     = str(ip_raw[0])[2:-2]                         # Cleaned up IP
    hostname_raw  = inputs["resourceNames"]                      # Raw Hostname from deployment
    hostname      = str(hostname_raw)[2:-2]                      # Cleaned up Hostname
    cnameRecord   = inputs["customProperties"]["cnameRecord"]    # Gets CNAME value from customproperties
    DNS_Server1 = context.getSecret(inputs["dns_server1"])       # DNS server where the command will be executed
    DNS_Server2 = context.getSecret(inputs["dns_server2"])       # DNS server where the command will be executed (secondary)
    DNS_Domain = context.getSecret(inputs["domain_name"])        # DNS Domain to be updated
    Username = context.getSecret(inputs["domain_username"])      # Username with righs to perform the operation
    Password = context.getSecret(inputs["domain_password"])      # Password for the account

    #Open session to DNS Server, try DNS 1 first and failback to DNS 2 if DNS 1 connection is not succesful
    try:
        session = winrm.Session('https://'+DNS_Server1+':5986/wsman', auth=(Username,Password), transport='credssp', server_cert_validation='ignore')
        result = session.run_ps("hostname")
        print("Connected to server "+result.std_out.decode())  
    except:
        try:
            session = winrm.Session('https://'+DNS_Server2+':5986/wsman', auth=(Username,Password), transport='credssp', server_cert_validation='ignore')
            result = session.run_ps("hostname")
            print("Connected to server "+result.std_out.decode())  
        except:
            print("Connections to server "+DNS_Server1+" or "+DNS_Server2+" failed. Aborting script...")
            sys.exit(0)
    
    #Check for provision event topic
    result = event.startswith('compute.provision')
    if result == True :
      print("Creating A record and PTR for "+hostname+" "+ipaddress)
      dns_command = "Add-DnsServerResourceRecordA -ZoneName "+DNS_Domain+" -Name "+hostname+" -IPv4Address "+ipaddress+" -CreatePtr"
      result = session.run_ps(dns_command)
      print(result.std_out.decode())
      
      #creates CNAME if requested in the deployment
      if (cnameRecord) :
         print("Creating CNAME record "+cnameRecord+" pointing to "+hostname+"."+DNS_Domain)
         dns_command = "Add-DnsServerResourceRecordCname -ZoneName "+DNS_Domain+" -HostNameAlias "+hostname+"."+DNS_Domain+" -Name "+cnameRecord
         result = session.run_ps(dns_command)
         print(result.std_out.decode())

    #Check for removal event topic
    result = event.startswith('compute.removal')
    if result == True :
      print("Deleting A record and PTR for "+hostname+" "+ipaddress)
      #Remove A record
      dns_command = "Remove-DnsServerResourceRecord -ZoneName "+DNS_Domain+" -Name "+hostname+" -RRType A -force"
      result = session.run_ps(dns_command)
      print(result.std_out.decode())
      
      #search for PTR record in order to get the zone name.
      dns_command = "get-dnsserverzone | where isreverselookupzone -eq True |Get-DnsServerResourceRecord -RRType PTR | where {$_.RecordData.PtrDomainName -eq '"+hostname+"."+DNS_Domain+".'} | foreach {$_.distinguishedname}"
      result = session.run_ps(dns_command)
      content = result.std_out.decode()
      if (not content):
          print("PTR doesn't exist")
      #if PTR record was found, remove the PTR record.
      else:
          tempvar = result.std_out.decode().split(",")
          hostname2 = tempvar[0].replace('DC=','')
          zonename2 = tempvar[1].replace('DC=','')
          print("Removing PTR record "+hostname2+" in zone "+zonename2)
          dns_command = "Remove-DnsServerResourceRecord -ZoneName "+zonename2+" -Name "+hostname2+" -RRType PTR -force"
          result = session.run_ps(dns_command)
          print(result.std_out.decode())
      
      #search for a CNAME record pointing to hostname.
      dns_command = "Get-DnsServerResourceRecord -RRType CNAME -ZoneName "+DNS_Domain+" | where {$_.RecordData.hostnamealias -eq '"+hostname+"."+DNS_Domain+".'} | foreach {$_.hostname}"
      result = session.run_ps(dns_command)
      content = result.std_out.decode()
      if (not content):
          print("CNAME record not found")
         
      #if CNAME record was found, remove the CNAME record.
      else:
          print("Removing CNAME record "+result.std_out.decode())
          dns_command = "Get-DnsServerResourceRecord -RRType CNAME -ZoneName "+DNS_Domain+" | where {$_.RecordData.hostnamealias -eq '"+hostname+"."+DNS_Domain+".'} | remove-dnsserverresourcerecord -zonename "+DNS_Domain+" -force"
          result = session.run_ps(dns_command)
          print(result.std_out.decode())
        

2. Create the Action Constants to present to the script handler:

ABX Action Constants

Subscriptions

There are two subscriptions needed for this action:

1. DNS-Record-Create

  • Subscribe to the Compute post provision Event Topic
  • Set a condition event.data.customProperties['create_dns'] == "true". This custom property must be in the vRA blueprint to trigger this subscription
  • Turn on Blocking to make sure that the DNS records are created before any other deployment or ABX action is run (for example in a multi-machine blueprint)
ABX Subscription - Create

2. DNS-Record-Delete

  • Subscribe to the Compute post removal Event Topic
  • Set a condition event.data.customProperties['create_dns'] == "true"
  • No need for Blocking on this one
ABX Subscription - Delete

Blueprint

Create two custom properties in your blueprint:
1.       create_dns: (type:boolean)
2.       cnameRecord: (type:string)

vRA Blueprint

I hope you found this a useful addition to your ABX arsenal – please feel free to share.

Leave a Reply

%d bloggers like this: