--- /dev/null
+---
+- name: Setup DNS Records
+ hosts: dns_host
+ gather_facts: false
+ vars:
+ SETUP_DNS_SERVICE: "{{ setup_dns_service | default(false) }}"
+ pre_tasks:
+ - name: Setup facts
+ ansible.builtin.setup:
+ when: SETUP_DNS_SERVICE | bool
+ roles:
+ - role: insert_dns_records
+ when: SETUP_DNS_SERVICE | bool
+ - role: validate_dns_records
--- /dev/null
+# Insert DNS Records roles
+
+Setups `dnsmasq` (either directly or via `NetworkManager`) inserting the DNS A records required for OpenShift install.
+
+## Role Variables
+
+| Variable | Required | Default | Options | Comments |
+| --------------------- | -------- | -------------- | ----------------------- | ----------------------------------------------------------- |
+| domain | yes | | | base for the DNS entries |
+| dns_entries_file_name | no | domains.dns | | |
+| dns_service_name | no | NetworkManager | NetworkManager, dnsmasq | the name of the service you want to manage your DNS records |
+| node_dns_records | no | | | dns records for the nodes of the OpenShift cluster |
+| extra_dns_records | no | | | used to defined dns records which are excess of the |
+
+The structure of `node_dns_records` and `extra_dns_records` is the same and as follows:
+
+```yaml
+node_dns_records:
+ master-0:
+ address: "<node.cluster.domain>"
+ ip: "<ip>"
+extra_dns_records:
+ place-0:
+ name: "place-0"
+ address: "<address>"
+ ip: "<ip>"
+ use_dhcp: false
+```
+
+## Example Playbook
+
+```yaml
+- name: Setup DNS Records
+ hosts: dns_host
+ roles:
+ - insert_dns_records
+ vars:
+ domain: "cluster.example.com"
+ node_dns_records:
+ master-0:
+ name: "master-0"
+ address: "master-0.cluster.example.com"
+ ip: "111.111.111.111"
+ use_dhcp: false
+ master-1:
+ name: "master-1"
+ address: "master-1.cluster.example.com"
+ ip: "111.111.111.112"
+ use_dhcp: false
+ master-2:
+ name: "master-2"
+ address: "master-2.cluster.example.com"
+ ip: "111.111.111.113"
+ use_dhcp: false
+```
--- /dev/null
+write_dnsmasq_config: true
+domain: "{{ cluster_name }}.{{ base_dns_domain }}"
+host_ip_keyword: "ansible_host"
+dns_entries_file_name: "{{ 'dnsmasq.' + cluster_name + '.conf' }}"
+dns_bmc_domain: "infra.{{ base_dns_domain }}"
+dns_bmc_address_suffix: "-bmc.{{ dns_bmc_domain }}"
+dns_service_name: NetworkManager
+dns_records:
+ apps:
+ address: ".apps.{{ domain }}"
+ ip: "{{ ingress_vip }}"
+ api:
+ address: "api.{{ domain }}"
+ ip: "{{ api_vip }}"
+ api_int:
+ address: "api-int.{{ domain }}"
+ ip: "{{ api_vip }}"
+
+node_dns_records: {}
+extra_dns_records: {}
+
+use_pxe: false
+use_dhcp: false
+dhcp_lease_time: 24h
+
+listen_address: "{{ ansible_default_ipv4.address }}"
+listen_addresses:
+ - "127.0.0.1"
+ - "{{ listen_address }}"
+
+TFTP_ROOT: "{% if (hostvars['tftp_host'] is defined) and (hostvars['tftp_host']['tftp_directory']) is defined %}{{ hostvars['tftp_host']['tftp_directory'] }}{% else %}/var/lib/tftpboot/{% endif %}"
--- /dev/null
+[main]
+dns=dnsmasq
--- /dev/null
+- name: "Restart {{ dns_service_name }}"
+ ansible.builtin.service:
+ name: "{{ dns_service_name }}"
+ state: restarted
+ async: 45
+ poll: 5
+ listen: restart_service
+ become: true
--- /dev/null
+---
+- name: Open port in firewall for DNS
+ ansible.posix.firewalld:
+ port: "53/udp"
+ permanent: true
+ immediate: true
+ state: enabled
+ zone: "{{ item }}"
+ loop:
+ - internal
+ - public
+
+- name: Open port in firewall for DHCP
+ ansible.posix.firewalld:
+ port: "67/udp"
+ permanent: true
+ immediate: true
+ state: enabled
+ zone: "{{ item }}"
+ loop:
+ - internal
+ - public
+ when: use_dhcp == true
+
+- name: Open port in firewall for proxy DHCP
+ ansible.posix.firewalld:
+ port: "4011/udp"
+ permanent: true
+ immediate: true
+ state: enabled
+ zone: "{{ item }}"
+ loop:
+ - internal
+ - public
+ when: use_pxe == true
+
+- name: Open port in firewall for PXE
+ ansible.posix.firewalld:
+ port: "69/udp"
+ permanent: true
+ immediate: true
+ state: enabled
+ zone: "{{ item }}"
+ loop:
+ - internal
+ - public
+ when: use_pxe == true
--- /dev/null
+---
+- name: Make sure ansible_fqdn is populated if required.
+ ansible.builtin.setup:
+ delegate_to: "{{ entry_name }}"
+ delegate_facts: true
+ when:
+ - entry_extra_check | default(true)
+ - hostvars[entry_name]['ansible_fqdn'] is not defined
+
+- name: "Populate dns entry for {{ entry_name }}"
+ ansible.builtin.set_fact:
+ other_host_dns_records: "{{ (other_host_dns_records | default({})) | combine(
+ {
+ entry_address : {
+ 'name': (other_host_dns_records[entry_address]['name'] | default([])) + [entry_name],
+ 'address': entry_address,
+ 'ip': hostvars[entry_name][host_ip_keyword],
+ }
+ }
+ ) }}"
--- /dev/null
+---
+- name: Create dns file
+ ansible.builtin.template:
+ src: openshift-cluster.conf.j2
+ dest: "/etc/dnsmasq.d/{{ dns_entries_file_name }}"
+ mode: "0644"
+ notify: restart_service
+
+- name: Start dnsmasq
+ ansible.builtin.service:
+ name: dnsmasq
+ state: started
+ enabled: true
--- /dev/null
+---
+- name: Get node_records for nodes
+ ansible.builtin.set_fact:
+ node_dns_records: "{{ (node_dns_records | default({})) | combine(
+ {
+ item: {
+ 'name': item,
+ 'address': item + '.' + cluster_name + '.' + base_dns_domain,
+ 'ip': hostvars[item][hostvars[item]['host_ip_keyword'] | default(host_ip_keyword)],
+ 'mac': hostvars[item]['mac'] | default(False),
+ 'use_dhcp': hostvars[item]['ip'] | default('dhcp') == 'dhcp',
+ }
+ } ) }}"
+ loop: "{{ groups['nodes'] }}"
+ when: hostvars[item][hostvars[item]['host_ip_keyword'] | default(host_ip_keyword)] is defined
+
+- name: Get node_records for node bmc_addresses when it is an IP address
+ ansible.builtin.set_fact:
+ bmc_dns_records: "{{ (bmc_dns_records | default({})) | combine(
+ {
+ item: {
+ 'name': item,
+ 'address': item + dns_bmc_address_suffix,
+ 'ip': hostvars[item]['bmc_ip'],
+ }
+ } ) }}"
+ loop: "{{ groups['nodes'] }}"
+ when:
+ - hostvars[item]['bmc_ip'] is defined
+ - hostvars[item]['bmc_ip'] | ansible.utils.ipaddr('bool')
+
+- name: Define bmc_address where required
+ ansible.builtin.set_fact:
+ bmc_address: "{{ item.data.address }}"
+ delegate_to: "{{ item.host }}"
+ delegate_facts: true
+ loop: "{{ bmc_dns_records | dict2items(key_name='host', value_name='data') }}"
+ when:
+ - bmc_dns_records is defined
+
+- name: Get bastions, services (not including registry) when it ansible_host is an IP address
+ ansible.builtin.include_tasks: create_host_entry.yml
+ vars:
+ entry_address: "{{ hostvars[item]['ansible_fqdn'] }}"
+ entry_name: "{{ item }}"
+ loop: "{{ groups['bastions'] + groups['services'] }}"
+ when:
+ - item != 'registry_host'
+ - hostvars[item][hostvars[item]['host_ip_keyword'] | default(host_ip_keyword)] | ansible.utils.ipaddr('bool')
+ - not (hostvars[item]['dns_skip_record'] | default(False)) | bool
+
+- name: Get registry_host when it ansible_host is an IP address
+ ansible.builtin.include_tasks: create_host_entry.yml
+ vars:
+ entry_address: "{{ hostvars['registry_host']['registry_fqdn'] | default(hostvars['registry_host']['ansible_fqdn']) }}"
+ entry_name: "registry_host"
+ entry_extra_check: "{{ hostvars['registry_host']['registry_fqdn'] is not defined }}"
+ when:
+ - "'registry_host' in hostvars"
+ - hostvars['registry_host'][hostvars['registry_host']['host_ip_keyword'] | default(host_ip_keyword)] | ansible.utils.ipaddr('bool')
+ - not (hostvars['registry_host']['dns_skip_record'] | default(False)) | bool
+
+- name: Get vm_hosts when ansible_host is an IP address
+ ansible.builtin.include_tasks: create_host_entry.yml
+ vars:
+ entry_address: "{{ hostvars[item]['sushy_fqdn'] | default(hostvars[item]['ansible_fqdn']) }}"
+ entry_name: "{{ item }}"
+ entry_extra_check: "{{ hostvars[item]['sushy_fqdn'] is not defined }}"
+ loop: "{{ groups['vm_hosts'] | default([]) }}"
+ when: >-
+ hostvars[item][hostvars[item]['host_ip_keyword'] | default(host_ip_keyword)] |
+ ansible.utils.ipaddr('bool') and (not (hostvars[item]['dns_skip_record'] | default(False))) | bool
+
+- name: Configure firewall
+ become: true
+ ansible.builtin.import_tasks: configure_firewall.yml
+
+- name: Install dnsmasq
+ become: true
+ ansible.builtin.package:
+ name: dnsmasq
+ state: present
+
+- name: "Make sure foder exists {{ TFTP_ROOT }}"
+ ansible.builtin.file:
+ path: "{{ TFTP_ROOT }}"
+ state: directory
+ recurse: true
+ when: use_pxe | bool
+
+- name: Configure dnsmasq via NetworkManager
+ become: true
+ ansible.builtin.import_tasks: network-manager.yml
+ when: dns_service_name == "NetworkManager"
+
+- name: Configure dnsmasq via dnsmasq
+ become: true
+ ansible.builtin.import_tasks: dnsmasq.yml
+ when: dns_service_name == "dnsmasq"
+
+- name: "Restart {{ dns_service_name }}"
+ become: true
+ ansible.builtin.service:
+ name: "{{ dns_service_name }}"
+ state: restarted
+ async: 45
+ poll: 5
--- /dev/null
+---
+- name: Setup network manager to run dnsmasq
+ ansible.builtin.copy:
+ src: nm-dnsmasq.conf
+ dest: /etc/NetworkManager/conf.d/dnsmasq.conf
+ mode: "0644"
+
+- name: Create dnsmasq openshift-cluster config file
+ ansible.builtin.template:
+ src: openshift-cluster.conf.j2
+ dest: "/etc/NetworkManager/dnsmasq.d/{{ dns_entries_file_name }}"
+ mode: "0644"
+ notify: restart_service
+
+- name: Start NetworkManager
+ ansible.builtin.service:
+ name: NetworkManager
+ state: started
+ enabled: true
+
+- name: Reload NetworkManager
+ ansible.builtin.service:
+ name: NetworkManager
+ state: reloaded
--- /dev/null
+[main]
+dns=dnsmasq
--- /dev/null
+domain={{ domain }}
+{% if write_dnsmasq_config %}
+domain-needed
+bogus-priv
+listen-address={{ listen_addresses | join(',') }}
+{% for listening_intf in (listening_interfaces | default([])) %}
+interface={{ listening_intf }}
+{% endfor%}
+{% for no_dhcp_intf in (no_dhcp_interfaces | default([])) %}
+no-dhcp-interface={{ no_dhcp_intf }}
+{% endfor%}
+expand-hosts
+{% if upstream_dns | default(False) %}
+server={{ upstream_dns }}
+{% endif %}
+{% endif %}
+
+{% if use_dhcp %}
+dhcp-range= tag:{{ cluster_name }},{{ dhcp_range_first }},{{ dhcp_range_last }}
+dhcp-option= tag:{{ cluster_name }},option:netmask,{{ (gateway + '/' + prefix | string) | ansible.utils.ipaddr('netmask') }}
+dhcp-option= tag:{{ cluster_name }},option:router,{{ gateway }}
+dhcp-option= tag:{{ cluster_name }},option:dns-server,{{ listen_address }}
+dhcp-option= tag:{{ cluster_name }},option:domain-search,{{ domain }}
+dhcp-option= tag:{{ cluster_name }},option:ntp-server,{{ ntp_server }}
+{% endif %}
+
+# Wildcard for apps and other api domains
+{% for item in dns_records.values() %}
+address=/{{ item.address }}/{{ item.ip }}
+{% endfor %}
+
+# Node addresses
+{% for item in node_dns_records.values() %}
+# {{ item.name }}
+{% if item.use_dhcp %}
+dhcp-host={{item.mac}},{{ item.ip }},{{ item.address }}, set:{{ cluster_name }}
+{% endif %}
+address=/{{ item.address }}/{{ item.ip }}
+ptr-record={{ item.ip.split('.')[::-1] | join('.') }}.in-addr.arpa,{{ item.address }}
+
+{% endfor %}
+
+{% if bmc_dns_records is defined %}
+# Node BMC addresses
+{% for item in bmc_dns_records.values() %}
+# {{ item.name }}
+address=/{{ item.address }}/{{ item.ip }}
+ptr-record={{ item.ip.split('.')[::-1] | join('.') }}.in-addr.arpa,{{ item.address }}
+
+{% endfor %}
+
+{% endif %}
+{% if other_host_dns_records is defined %}
+# Bastions, services and vm_hosts
+{% for item in other_host_dns_records.values() %}
+# {{ item.name | join(', ') }}
+address=/{{ item.address }}/{{ item.ip }}
+ptr-record={{ item.ip.split('.')[::-1] | join('.') }}.in-addr.arpa,{{ item.address }}
+
+{% endfor %}
+
+{% endif %}
+# User provided entries
+{% for item in extra_dns_records.values() %}
+# {{ item.name }}
+{% if item.use_dhcp %}
+dhcp-host={{item.mac}},{{ item.ip }},{{ item.address }}, set:{{ cluster_name }}
+{% endif %}
+address=/{{ item.address }}/{{ item.ip }}
+ptr-record={{ item.ip.split('.')[::-1] | join('.') }}.in-addr.arpa,{{ item.address }}
+
+{% endfor %}
+{% if use_pxe %}
+
+# PXE boot config
+enable-tftp
+tftp-root={{ TFTP_ROOT }}
+dhcp-vendorclass=BIOS,PXEClient:Arch:00000
+dhcp-boot=tag:BIOS,lpxelinux.0
+dhcp-boot=tag:!BIOS,BOOTX64.EFI
+
+{% endif %}
--- /dev/null
+# validate_dns_records
+
+Checks for the required dns entries for ingress and API VIPs.
--- /dev/null
+required_domains:
+ "api": "api.{{ domain }}"
+ "api-int": "api-int.{{ domain }}"
+ "apps": "*.apps.{{ domain }}"
+
+expected_answers:
+ "api": "{{ api_vip }}"
+ "api-int": "{{ api_vip }}"
+ "apps": "{{ ingress_vip }}"
+
+required_binary: dig
+required_binary_provided_in_package: bind-utils
+domain: "{{ cluster_name }}.{{ base_dns_domain }}"
--- /dev/null
+- name: Check required domain {item} exists
+ ansible.builtin.shell:
+ cmd: "{{ required_binary }} {{ item.value }} A {{ item.value }} AAAA +short"
+ register: res
+ changed_when: false
+
+- name: Check stdout for expected IP address
+ ansible.builtin.set_fact:
+ failed_domains: "{{ (failed_domains | default({})) | combine(
+ {item.value: {
+ 'stdout': res.stdout,
+ 'stderr': res.stderr,
+ 'expected': expected_answers[item.key],
+ }}
+ ) }}"
+ when: expected_answers[item.key] not in res.stdout
--- /dev/null
+- name: Check if the required binary for testing exists
+ ansible.builtin.shell:
+ cmd: "which {{ required_binary }}"
+ register: required_binary_check
+ ignore_errors: true
+ changed_when: false
+
+- name: (if binary is missing) Install the package providing the required binary
+ ansible.builtin.package:
+ name: "{{ required_binary_provided_in_package }}"
+ state: present
+ become: true
+ when: required_binary_check.rc != 0
+
+- name: Set inital failed_domains
+ ansible.builtin.set_fact:
+ failed_domains: {}
+
+- name: Check domains
+ ansible.builtin.include_tasks: "check.yml"
+ loop: "{{ required_domains | dict2items() }}"
+
+- name: List failed_domains
+ ansible.builtin.fail:
+ msg: |
+ Failed domains:
+ {% for failed in (failed_domains | dict2items) %}
+ {{ failed.key }}:
+ expected:
+ {{ failed.value.expected | indent(14) }}
+ stdout:
+ {{ failed.value.stdout | indent(14)}}
+ stderr:
+ {{ failed.value.stderr | indent(14) }}
+ {% endfor %}
+ when: failed_domains | length > 0