IPAM Automation with NetBox, Ansible, and Microsoft Windows DNS Server


Introduction

Managing IP addresses and DNS records manually can be a daunting task, especially in dynamic IT environments. This blog post demonstrates how to leverage NetBox, Ansible, and Microsoft Windows DNS Server to automate IP Address Management (IPAM) and DNS record updates, making your infrastructure more efficient and reliable.

Why Automate IPAM and DNS?

  • Consistency: Automation minimizes human errors and ensures uniformity.
  • Efficiency: Automating repetitive tasks saves time and allows teams to focus on strategic activities.
  • Scalability: As networks grow, automated solutions adapt more easily than manual processes.

My goal

  • Get a free IP address is dynamically fetched from a defined subnet in NetBox.
  • The IP address is immediately assigned to the specified FQDN in NetBox.
  • A corresponding Host A record is created in your Windows DNS Server.

Prerequisites

Before diving into the implementation, ensure the following:

  • A functional NetBox instance configured with appropriate IPAM data.
  • A Microsoft Windows DNS Server with administrative access.
  • Ansible installed and configured on a control node.
  • API access credentials for NetBox.
  • pywinrm Python module
  • PowerShell Remoting

Ansible Project

For this automation project, I structured my workflow into multiple steps to keep it organized and modular. I use an ansible.cfg file to integrate and manage my inventory. At the core of the setup is a master playbook, which orchestrates the entire automation process.

To simplify and separate concerns, I divided the tasks into two sub-playbooks:

NetBox playbook: Handles all interactions with NetBox, such as fetching available IPs or updating DNS-related metadata. DNS playbook: Focuses on managing DNS records on my Microsoft Windows DNS Server. This approach not only makes the automation workflow easier to manage but also allows me to test and modify individual components independently while maintaining a clear overview of the entire process through the master playbook.

Getting Started

To begin, I will list the files and their roles in this automation project. While these files are currently stored in my local Gitea instance, I’m considering creating a public Git repository for future projects to make them more accessible and easier to share.

inventory.yml

all:
  hosts:
    dnsserver.lab.home:
      ansible_host: dc.lab.home  # IP-Adresse or Hostname of Windows-DNS-Servers
      ansible_user: administrator  # Username 
      ansible_password: xxx  # Password
      ansible_connection: winrm  # connection
      ansible_winrm_transport: basic  # auth
      ansible_winr_server_cert_validation: ignore #don't check the certificate
      ansible_port: 5986 #winrm https port

ansible.cfg

[defaults]
inventory = inventory.yml

register_ip.yml

---
- name: Validate input variables
  fail:
    msg: "You must provide 'netbox_token', 'prefix', and 'dns_name' as extra-vars."
  when: netbox_token == "" or prefix == "" or dns_name == ""

- name: Get the prefix ID from NetBox
  uri:
    url: "{{ netbox_url }}/api/ipam/prefixes/?prefix={{ prefix }}"
    method: GET
    headers:
      Authorization: "Token {{ netbox_token }}"
      Accept: "application/json"
    return_content: yes
  register: prefix_data

- name: Fail if the prefix does not exist
  fail:
    msg: "Prefix {{ prefix }} does not exist in NetBox."
  when: prefix_data.json.results | length == 0

- name: Get available IPs in the prefix
  uri:
    url: "{{ netbox_url }}/api/ipam/prefixes/{{ prefix_data.json.results[0].id }}/available-ips/"
    method: GET
    headers:
      Authorization: "Token {{ netbox_token }}"
      Accept: "application/json"
    return_content: yes
  register: available_ips

- name: Fail if no available IPs are found
  fail:
    msg: "No available IPs found in prefix {{ prefix }}."
  when: available_ips.json | length == 0

- name: Assign the first available IP
  uri:
    url: "{{ netbox_url }}/api/ipam/ip-addresses/"
    method: POST
    headers:
      Authorization: "Token {{ netbox_token }}"
      Accept: "application/json"
      Content-Type: "application/json"
    body: >
      {
        "address": "{{ available_ips.json[0].address }}",
        "status": "active",
        "description": "Created by Ansible",
        "dns_name": "{{ dns_name }}"
      }      
    body_format: json
    status_code: 201
    return_content: yes
  register: ip_assignment

- name: Extract host and zone from DNS name
  set_fact:
    dns_host: "{{ dns_name.split('.')[0] }}"
    dns_zone: "{{ dns_name.split('.', 1)[1] }}"
    assigned_ip: "{{ ip_assignment.json.address.split('/')[0] }}"

add_dns_record.yml

---
- name: Add DNS A Record
  win_shell: |
    Add-DnsServerResourceRecordA -Name "{{ zdns_host }}" -ZoneName "{{ zdns_zone }}" -IPv4Address "{{ zassigned_ip }}"    
  args:
    executable: powershell

mp_dns.yml (my masterplaybook)

---
- name: Register IP in NetBox
  hosts: localhost
  gather_facts: no
  vars:
    prefix: "{{ prefix }}"  # variables
    dns_name: "{{ dns_name }}"  # variables
    netbox_url: "http://netbox.lab.home"  #NetBox-URL
    netbox_token: "xxx"  # Ntebox API token

  tasks:
    - name: Run NetBox IP Registration Playbook
      include_tasks: register_ip.yml
      vars:
        prefix: "{{ prefix }}"
        dns_name: "{{ dns_name }}"
        
 
- name: Add DNS A Record
  hosts: dnsserver.lab.home
  gather_facts: no
  vars:
    ansible_winrm_server_cert_validation: ignore
    zassigned_ip: "{{ hostvars['localhost']['sip'] }}" # variables
    zdns_host: "{{ hostvars['localhost']['sdns'] }}"   # variables
    zdns_zone: "{{ hostvars['localhost']['szone'] }}"  # variables

  tasks:
    - name: Include DNS Record Playbook
      include_tasks: add_dns_record.yml

How the Playbooks Work

The process is coordinated by a master playbook (mp_dns.yml) and relies on sub-playbooks for discrete tasks.

Master Playbook (mp_dns.yml)

The master playbook serves as the central control file. It performs the following steps:

Registers an IP Address in NetBox: This step invokes the register_ip.yml sub-playbook to allocate an available IP address within a specified prefix and associate it with the given DNS name in NetBox.

  • Sets Facts: After obtaining the IP address and DNS details from NetBox, it uses set_fact to store these values in variables (sip, sdns, szone) for use in the next task.

  • Adds a DNS A Record: The second phase connects to the DNS server and calls the add_dns_record.yml sub-playbook to create a DNS A record using the information retrieved from NetBox.

Sub-Playbook: register_ip.yml

This playbook interacts with NetBox’s API to:

  • Validate input variables like the NetBox token, prefix, and DNS name.
  • Retrieve the prefix and find available IPs.
  • Assign the first available IP to the provided DNS name and register it in NetBox.

The playbook sends a POST request to the NetBox API to assign an available IP address to the provided DNS name. The response is returned in JSON format and parsed to extract the necessary variables for the DNS record creation.

The JSON response is parsed to extract key values:

  • dns_host and dns_zone are derived by splitting the FQDN.
  • assigned_ip captures the raw IP address, omitting the CIDR notation.
  dns_host: "{{ dns_name.split('.')[0] }}"  # Extracts the hostname (e.g., "myhost" from "myhost.lab.local")
  dns_zone: "{{ dns_name.split('.', 1)[1] }}"  # Extracts the zone (e.g., "lab.local")
  assigned_ip: "{{ ip_assignment.json.address.split('/')[0] }}"  # Removes the subnet mask (e.g., "192.168.1.10/24" to "192.168.1.10")

This parsing ensures the required details are extracted for creating the DNS record in subsequent tasks, linking NetBox’s IP allocation to the DNS configuration seamlessly.

Sub-Playbook: add_dns_record.yml

This playbook uses PowerShell (win_shell) to execute the Add-DnsServerResourceRecordA cmdlet on the Windows DNS server. It creates a DNS A record with the assigned IP, host, and zone.

Why Use host_vars?

hostvars is a built-in Ansible variable that provides access to variables from other hosts in the inventory. This is particularly useful when you need to share or reference facts or variables gathered from one host on another host. The NetBox-related tasks (e.g., registering IP addresses and extracting DNS details) are performed on localhost since they interact with external APIs and don’t require remote server execution. Variables like sip, sdns, and szone are set as facts on localhost during the first phase of the playbook execution. The hostvars[’localhost’] construct is used to retrieve these facts and make them available to the subsequent tasks running on the DNS server (dnsserver.lab.home).

Variable Assignments:

  • zassigned_ip: This retrieves the IP address (sip) assigned to the host from the NetBox interaction on localhost.
  • zdns_host: This extracts the host portion of the DNS name (sdns) derived from the FQDN split.
  • zdns_zone: This fetches the DNS zone (szone), also derived from the FQDN split.

This approach ensures that:

  • Data derived or computed in one phase (NetBox-related tasks) is seamlessly passed to the next phase (DNS-related tasks).
  • sThe DNS playbook (add_dns_record.yml) running on the DNS server has access to the correct IP, host, and zone information without redundant processing.

Ok, Enough Code and Explanations, Let’s See It in Action

Starting the playbook:

ansible-playbook mp_dns.yml -e "prefix=192.168.2.0/24 dns_name=hello-world.lab.home"
Ansible output

Ansible output (click to enlarge)

Netbox

Netbox (click to enlarge)

DNS

DNS (click to enlarge)

Conclusion

This project represents just the first step toward a fully automated IPAM and DNS management workflow. While the current solution works well in my lab environment, there is plenty of room for improvement and expansion.

Key Takeaways:

  • Modular Design: Starting with a modular playbook structure ensures flexibility for future enhancements and easier debugging.

  • Lab vs. Production: This setup is tailored for a lab environment. For production systems, avoid using highly privileged accounts like the local administrator on the DNS server. A more secure approach with role-based access control (RBAC) should be implemented in future iterations.

  • Continuous Improvement: I acknowledge that the playbook is not perfect. Over time, I plan to refine and optimize it, addressing any current shortcomings and making it more robust for complex workflows.

Automation is a journey, and I’m excited to see how this project evolves. Stay tuned for updates and new features in future versions!

Update: automatic PTR creation

Here’s a quick update to my blog: With the adjusted code, you can automatically create a PTR record when adding a Host A record. Note: The Reverse Lookup Zone must already exist.

---
- name: Add DNS A Record
  win_shell: |
    $ip = "{{ zassigned_ip }}"
    $hostname = "{{ zdns_host }}.{{ zdns_zone }}"
    $reverseZone = ("{0}.{1}.{2}.in-addr.arpa" -f $ip.Split(".")[2], $ip.Split(".")[1], $ip.Split(".")[0])
    Add-DnsServerResourceRecordA -Name "{{ zdns_host }}" -ZoneName "{{ zdns_zone }}" -IPv4Address "{{ zassigned_ip }}"
    Add-DnsServerResourceRecordPtr -ZoneName $reverseZone -Name ($ip.Split(".")[3]) -PtrDomainName "$hostname.$zoneName"     
  args:
    executable: powershell