Adding playbook and role to setup DNS. 95/13095/1
authorArnaldo Hernandez <arjuhe@gmail.com>
Thu, 27 Jun 2024 15:42:43 +0000 (10:42 -0500)
committerArnaldo Hernandez <arjuhe@gmail.com>
Thu, 27 Jun 2024 15:43:22 +0000 (10:43 -0500)
Issue-ID: INF-441
Change-Id: Ic14a0a52f752ea17f864093fb24460d13621274e
Signed-off-by: Arnaldo Hernandez <arjuhe@gmail.com>
16 files changed:
okd/playbooks/deploy_dns.yml [new file with mode: 0644]
okd/roles/insert_dns_records/README.md [new file with mode: 0644]
okd/roles/insert_dns_records/defaults/main.yml [new file with mode: 0644]
okd/roles/insert_dns_records/files/nm-dnsmasq.conf [new file with mode: 0644]
okd/roles/insert_dns_records/handlers/main.yml [new file with mode: 0644]
okd/roles/insert_dns_records/tasks/configure_firewall.yml [new file with mode: 0644]
okd/roles/insert_dns_records/tasks/create_host_entry.yml [new file with mode: 0644]
okd/roles/insert_dns_records/tasks/dnsmasq.yml [new file with mode: 0644]
okd/roles/insert_dns_records/tasks/main.yml [new file with mode: 0644]
okd/roles/insert_dns_records/tasks/network-manager.yml [new file with mode: 0644]
okd/roles/insert_dns_records/templates/nm-dnsmasq.conf.j2 [new file with mode: 0644]
okd/roles/insert_dns_records/templates/openshift-cluster.conf.j2 [new file with mode: 0644]
okd/roles/validate_dns_records/README.md [new file with mode: 0644]
okd/roles/validate_dns_records/defaults/main.yml [new file with mode: 0644]
okd/roles/validate_dns_records/tasks/check.yml [new file with mode: 0644]
okd/roles/validate_dns_records/tasks/main.yml [new file with mode: 0644]

diff --git a/okd/playbooks/deploy_dns.yml b/okd/playbooks/deploy_dns.yml
new file mode 100644 (file)
index 0000000..839378c
--- /dev/null
@@ -0,0 +1,14 @@
+---
+- 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
diff --git a/okd/roles/insert_dns_records/README.md b/okd/roles/insert_dns_records/README.md
new file mode 100644 (file)
index 0000000..dc1686c
--- /dev/null
@@ -0,0 +1,55 @@
+# 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
+```
diff --git a/okd/roles/insert_dns_records/defaults/main.yml b/okd/roles/insert_dns_records/defaults/main.yml
new file mode 100644 (file)
index 0000000..0dc5ec6
--- /dev/null
@@ -0,0 +1,31 @@
+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 %}"
diff --git a/okd/roles/insert_dns_records/files/nm-dnsmasq.conf b/okd/roles/insert_dns_records/files/nm-dnsmasq.conf
new file mode 100644 (file)
index 0000000..53a8b17
--- /dev/null
@@ -0,0 +1,2 @@
+[main]
+dns=dnsmasq
diff --git a/okd/roles/insert_dns_records/handlers/main.yml b/okd/roles/insert_dns_records/handlers/main.yml
new file mode 100644 (file)
index 0000000..796f35b
--- /dev/null
@@ -0,0 +1,8 @@
+- name: "Restart {{ dns_service_name }}"
+  ansible.builtin.service:
+    name: "{{ dns_service_name }}"
+    state: restarted
+  async: 45
+  poll: 5
+  listen: restart_service
+  become: true
diff --git a/okd/roles/insert_dns_records/tasks/configure_firewall.yml b/okd/roles/insert_dns_records/tasks/configure_firewall.yml
new file mode 100644 (file)
index 0000000..dd99c94
--- /dev/null
@@ -0,0 +1,47 @@
+---
+- 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
diff --git a/okd/roles/insert_dns_records/tasks/create_host_entry.yml b/okd/roles/insert_dns_records/tasks/create_host_entry.yml
new file mode 100644 (file)
index 0000000..414af70
--- /dev/null
@@ -0,0 +1,20 @@
+---
+- 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],
+          }
+        }
+      ) }}"
diff --git a/okd/roles/insert_dns_records/tasks/dnsmasq.yml b/okd/roles/insert_dns_records/tasks/dnsmasq.yml
new file mode 100644 (file)
index 0000000..f25ee69
--- /dev/null
@@ -0,0 +1,13 @@
+---
+- 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
diff --git a/okd/roles/insert_dns_records/tasks/main.yml b/okd/roles/insert_dns_records/tasks/main.yml
new file mode 100644 (file)
index 0000000..ca77350
--- /dev/null
@@ -0,0 +1,107 @@
+---
+- 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
diff --git a/okd/roles/insert_dns_records/tasks/network-manager.yml b/okd/roles/insert_dns_records/tasks/network-manager.yml
new file mode 100644 (file)
index 0000000..4a62209
--- /dev/null
@@ -0,0 +1,24 @@
+---
+- 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
diff --git a/okd/roles/insert_dns_records/templates/nm-dnsmasq.conf.j2 b/okd/roles/insert_dns_records/templates/nm-dnsmasq.conf.j2
new file mode 100644 (file)
index 0000000..53a8b17
--- /dev/null
@@ -0,0 +1,2 @@
+[main]
+dns=dnsmasq
diff --git a/okd/roles/insert_dns_records/templates/openshift-cluster.conf.j2 b/okd/roles/insert_dns_records/templates/openshift-cluster.conf.j2
new file mode 100644 (file)
index 0000000..cf68a55
--- /dev/null
@@ -0,0 +1,82 @@
+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 %}
diff --git a/okd/roles/validate_dns_records/README.md b/okd/roles/validate_dns_records/README.md
new file mode 100644 (file)
index 0000000..99d439b
--- /dev/null
@@ -0,0 +1,3 @@
+# validate_dns_records
+
+Checks for the required dns entries for ingress and API VIPs.
diff --git a/okd/roles/validate_dns_records/defaults/main.yml b/okd/roles/validate_dns_records/defaults/main.yml
new file mode 100644 (file)
index 0000000..2073c06
--- /dev/null
@@ -0,0 +1,13 @@
+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 }}"
diff --git a/okd/roles/validate_dns_records/tasks/check.yml b/okd/roles/validate_dns_records/tasks/check.yml
new file mode 100644 (file)
index 0000000..5603788
--- /dev/null
@@ -0,0 +1,16 @@
+- 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
diff --git a/okd/roles/validate_dns_records/tasks/main.yml b/okd/roles/validate_dns_records/tasks/main.yml
new file mode 100644 (file)
index 0000000..ab93672
--- /dev/null
@@ -0,0 +1,36 @@
+- 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