IPAM Automation with NetBox, Ansible, and Microsoft Windows DNS Server
IPAM Automation with Netbox and Ansible
netboxansiblehomelabautomation
1626 Words Words // ReadTime 7 Minutes, 23 Seconds
2024-12-20 02:00 +0100
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"
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