diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71f147e76..553589390 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,11 +127,14 @@ jobs: run: ./setup-environment - name: Start VMs run: | - ./forge vms start --vms "quadlet client ${{ matrix.database == 'external' && 'database' || '' }}" + ./forge vms start --vms "quadlet client named ${{ matrix.database == 'external' && 'database' || '' }}" - name: Configure remote-database if: matrix.database == 'external' run: | ./forge remote-database + - name: Configure remote-named + run: | + ./forge remote-named - name: Configure repositories run: | ./forge setup-repositories @@ -173,6 +176,8 @@ jobs: --add-feature ansible \ --add-feature bmc \ --add-feature webhooks \ + --add-feature dns \ + --dns-provider nsupdate --dns-server named.example.com --dns-nsupdate-keyfile $(pwd)/.var/lib/foremanctl/rndc.key \ ${{ matrix.iop == 'enabled' && '--add-feature iop' || '' }} - name: Run tests run: | @@ -349,7 +354,10 @@ jobs: run: ./setup-environment - name: Start VMs run: | - ./forge vms start --vms "quadlet client" + ./forge vms start --vms "quadlet client named" + - name: Configure remote-named + run: | + ./forge remote-named - name: Configure repositories run: | ./forge setup-repositories @@ -377,7 +385,9 @@ jobs: --add-feature bmc \ --add-feature google \ --add-feature remote-execution \ - --add-feature webhooks + --add-feature webhooks \ + --add-feature dns \ + --dns-provider nsupdate --dns-server named.example.com --dns-nsupdate-keyfile $(pwd)/.var/lib/foremanctl/rndc.key - name: Run tests run: | ./forge test --pytest-args="--featureset=migration" @@ -552,7 +562,10 @@ jobs: run: cp -a vendor_overrides/satellite src/playbooks/_vendor_overrides - name: Start VMs run: | - ./forge vms start --vms "quadlet client" + ./forge vms start --vms "quadlet client named" + - name: Configure remote-named + run: | + ./forge remote-named - name: Configure repositories run: | ./forge setup-repositories @@ -573,7 +586,9 @@ jobs: - name: Deploy features run: | ./foremanctl deploy \ - --add-feature bmc + --add-feature bmc \ + --add-feature dns \ + --dns-provider nsupdate --dns-server named.example.com --dns-nsupdate-keyfile $(pwd)/.var/lib/foremanctl/rndc.key \ - name: Run tests run: | ./forge test --pytest-args="--featureset=satellite" diff --git a/Vagrantfile b/Vagrantfile index ecb5dd775..599bae621 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -76,6 +76,16 @@ Vagrant.configure("2") do |config| end end + config.vm.define "named" do |override| + override.vm.box = ENV.fetch("FOREMANCTL_BASE_BOX", "centos/stream10") + set_centos_box_url(override.vm) + override.vm.hostname = "named.#{DOMAIN}" + + override.vm.provider "libvirt" do |libvirt, provider| + libvirt.memory = 2048 + end + end + # Load user-local box definitions from boxes.yaml (gitignored) boxes_yaml = File.join(__dir__, 'boxes.yaml') if File.exist?(boxes_yaml) diff --git a/development/playbooks/remote-named/metadata.obsah.yaml b/development/playbooks/remote-named/metadata.obsah.yaml new file mode 100644 index 000000000..530011ec7 --- /dev/null +++ b/development/playbooks/remote-named/metadata.obsah.yaml @@ -0,0 +1,2 @@ +--- +help: Setup a machine with BIND diff --git a/development/playbooks/remote-named/remote-named.yaml b/development/playbooks/remote-named/remote-named.yaml new file mode 100644 index 000000000..cca82da45 --- /dev/null +++ b/development/playbooks/remote-named/remote-named.yaml @@ -0,0 +1,13 @@ +--- +- name: Setup remote named + hosts: + - named + become: true + roles: + - role: bind + tasks: + - name: Fetch rndc key + ansible.builtin.fetch: + src: "/etc/named/rndc.key" + dest: "{{ obsah_state_path }}/rndc.key" + flat: true diff --git a/development/playbooks/test/test.yaml b/development/playbooks/test/test.yaml index 5e60df2c9..b82ddd94f 100644 --- a/development/playbooks/test/test.yaml +++ b/development/playbooks/test/test.yaml @@ -8,6 +8,7 @@ ansible.builtin.package: name: - nmap + - bind-utils - name: Execute tests gather_facts: false diff --git a/development/roles/bind/files/empty.zone b/development/roles/bind/files/empty.zone new file mode 100644 index 000000000..b8c050f5f --- /dev/null +++ b/development/roles/bind/files/empty.zone @@ -0,0 +1,9 @@ +$TTL 2d ; default TTL for zone +@ IN SOA @ hostmaster.invalid. ( + 0 ; serial number + 12h ; refresh + 15m ; update retry + 3w ; expiry + 2h ; minimum + ) + IN NS localhost. diff --git a/development/roles/bind/tasks/main.yml b/development/roles/bind/tasks/main.yml new file mode 100644 index 000000000..b1f814abf --- /dev/null +++ b/development/roles/bind/tasks/main.yml @@ -0,0 +1,69 @@ +--- +- name: Install BIND + ansible.builtin.package: + name: + - bind + - bind-utils + state: present + +- name: Generate the rndc key + ansible.builtin.command: + cmd: rndc-confgen -a -c /etc/named/rndc.key + creates: /etc/named/rndc.key + +- name: Set permissions on rndc key + ansible.builtin.file: + path: /etc/named/rndc.key + owner: root + group: named + mode: '0640' + +- name: Copy the base zone files + ansible.builtin.copy: + src: empty.zone + dest: "/var/named/{{ item }}.db" + owner: root + group: named + mode: '0640' + loop: + - example.test + - 2.0.192.in-addr.arpa + - 8.b.d.0.1.0.0.2.ip6.arpa + +- name: Configure example.test zone + ansible.builtin.blockinfile: + path: /etc/named.conf + block: | + include "/etc/named/rndc.key"; + zone "example.test" { + type primary; + file "example.test.db"; + allow-update { key "rndc-key"; }; + }; + zone "2.0.192.in-addr.arpa" { + type primary; + file "2.0.192.in-addr.arpa.db"; + allow-update { key "rndc-key"; }; + }; + zone "8.b.d.0.1.0.0.2.ip6.arpa" { + type primary; + file "8.b.d.0.1.0.0.2.ip6.arpa.db"; + allow-update { key "rndc-key"; }; + }; + +- name: Configure BIND to listen on any IP + ansible.builtin.lineinfile: + path: /etc/named.conf + regexp: '^(\s+{{ item }})' + line: '\g<1> { any; };' + backrefs: true + loop: + - listen-on port 53 + - listen-on-v6 port 53 + - allow-query + +- name: Start BIND + ansible.builtin.service: + name: named + state: started + enabled: true diff --git a/docs/user/parameters.md b/docs/user/parameters.md index 7d973d248..67c9a3b16 100644 --- a/docs/user/parameters.md +++ b/docs/user/parameters.md @@ -158,6 +158,10 @@ There are multiple use cases from the users perspective that dictate what parame | `--templates-url` | URL that hosts will use to contact the proxy for provisioning templates | `--foreman-proxy-templates-url` | | `--add-feature registration` | Enable Registration feature | `--foreman-proxy-registration` | | `--registration-url` | URL that hosts use to reach the registration endpoint | `--foreman-proxy-registration-url` | +| `--add-feature dns` | Enable DNS feature | `--foreman-proxy-dns` | +| `--dns-provider` | Select DNS provider | `--foreman-proxy-dns-provider` | +| `--dns-server` | Configure the target DNS server | `--foreman-proxy-dns-server` | +| `--dns-nsupdate-keyfile` | Configure the key for the target DNS server | `--foreman-proxy-keyfile` | ### Unmapped @@ -166,6 +170,7 @@ There are multiple use cases from the users perspective that dictate what parame | `--foreman-proxy-oauth-consumer-key` | OAuth consumer key for Smart Proxy | Not required(Managed automatically via auth bundle) | | `--foreman-proxy-oauth-consumer-secret` | OAuth consumer secret for Smart Proxy | Not required(Managed automatically via auth bundle) | | `--foreman-proxy-templates-listen-on` | Templates proxy to listen on https, http, or both | No longer supported | +| `--foreman-proxy-dns-managed` | Installer managed DNS | not supported | ### Undetermined @@ -184,11 +189,6 @@ There are multiple use cases from the users perspective that dictate what parame | `--foreman-proxy-dhcp-server` | | foreman_proxy | dhcp_server | | `--foreman-proxy-libvirt-network` | | foreman_proxy | libvirt_network | | `--foreman-proxy-libvirt-url` | | foreman_proxy | libvirt_url | -| `--foreman-proxy-dns` | | foreman_proxy | dns | -| `--foreman-proxy-dns-managed` | | foreman_proxy | dns_managed | -| `--foreman-proxy-dns-provider` | | foreman_proxy | foreman_proxy::dns_provider | -| `--foreman-proxy-dns-server` | | foreman_proxy | foreman_proxy::dns_server | -| `--foreman-proxy-keyfile` | | foreman_proxy | foreman_proxy::keyfile | | `--foreman-proxy-plugin-dns-powerdns-rest-api-key` | | foreman_proxy::plugin::dns_powerdns | rest_api_key | | `--foreman-proxy-plugin-dns-powerdns-rest-url` | | foreman_proxy::plugin::dns_powerdns | rest_url | | `--foreman-proxy-plugin-dns-route53-aws-access-key` | | foreman_proxy::plugin::dns_route53 | aws_access_key | diff --git a/src/features.yaml b/src/features.yaml index 4c63ff808..fdf3c15bb 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -135,3 +135,7 @@ theme-satellite: internal: true foreman: plugin_name: foreman_theme_satellite +dns: + description: DNS management + foreman_proxy: + plugin_name: dns diff --git a/src/playbooks/_foreman_proxy/metadata.obsah.yaml b/src/playbooks/_foreman_proxy/metadata.obsah.yaml index 10bcb6fa7..13a97b92c 100644 --- a/src/playbooks/_foreman_proxy/metadata.obsah.yaml +++ b/src/playbooks/_foreman_proxy/metadata.obsah.yaml @@ -22,3 +22,21 @@ variables: foreman_proxy_registration_url: parameter: --registration-url help: URL that hosts use to reach the registration endpoint (e.g. https://loadbalancer.example.com). + foreman_proxy_dns_provider: + parameter: --dns-provider + help: DNS provider to use. + choices: + - nsupdate + foreman_proxy_dns_nsupdate_keyfile: + parameter: --dns-nsupdate-keyfile + help: Key file to be used to authenticate with the DNS server + type: AbsolutePath + foreman_proxy_dns_server: + parameter: --dns-server + help: Hostname of the DNS server to communicate with + type: FQDN + +constraints: + required_in_list: + - [[[features, dns]], [foreman_proxy_dns_provider]] + - [[[features, dns], [foreman_proxy_dns_provider, nsupdate]], [foreman_proxy_dns_nsupdate_keyfile, foreman_proxy_dns_server]] diff --git a/src/roles/foreman_proxy/defaults/main.yaml b/src/roles/foreman_proxy/defaults/main.yaml index 628fad8f4..a917e142a 100644 --- a/src/roles/foreman_proxy/defaults/main.yaml +++ b/src/roles/foreman_proxy/defaults/main.yaml @@ -44,3 +44,10 @@ foreman_proxy_bmc_ipmi_implementation: ipmitool foreman_proxy_bmc_redfish_verify_ssl: true foreman_proxy_with_pulp_mirror: "{{ pulp_mirror | default(false) }}" + + # DNS settings +foreman_proxy_dns_provider_list: + - dns_nsupdate +foreman_proxy_dns_provider: "{{ undef(hint='You must specify a DNS provider to use') }}" +foreman_proxy_dns_provider_name: "dns_{{ foreman_proxy_dns_provider }}" +foreman_proxy_dns_ttl: 86400 diff --git a/src/roles/foreman_proxy/tasks/feature/dns.yaml b/src/roles/foreman_proxy/tasks/feature/dns.yaml new file mode 100644 index 000000000..c1fadab54 --- /dev/null +++ b/src/roles/foreman_proxy/tasks/feature/dns.yaml @@ -0,0 +1,7 @@ +--- +- name: Include tasks for {{ item }} + ansible.builtin.include_tasks: feature.yaml + vars: + feature_enabled: "{{ foreman_proxy_dns_provider_name == item }}" + feature_name: "{{ item }}" + loop: "{{ foreman_proxy_dns_provider_list }}" diff --git a/src/roles/foreman_proxy/tasks/feature/dns_nsupdate.yaml b/src/roles/foreman_proxy/tasks/feature/dns_nsupdate.yaml new file mode 100644 index 000000000..c2ac4b0b1 --- /dev/null +++ b/src/roles/foreman_proxy/tasks/feature/dns_nsupdate.yaml @@ -0,0 +1,22 @@ +--- +- name: Create RNDC Key podman secret + containers.podman.podman_secret: + state: present + name: foreman_proxy-dns_nsupdate-rndc_key + data: "{{ lookup('ansible.builtin.file', foreman_proxy_dns_nsupdate_keyfile) }}" + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy + +- name: Mount RNDC key + ansible.builtin.copy: + dest: /etc/containers/systemd/foreman-proxy.container.d/dns_nsupdate-rndc_key.conf + content: | + [Container] + Secret=foreman_proxy-dns_nsupdate-rndc_key,type=mount,target=/etc/foreman-proxy/rndc.key + mode: '0644' + owner: root + group: root + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy diff --git a/src/roles/foreman_proxy/templates/settings.d/dns.yml.j2 b/src/roles/foreman_proxy/templates/settings.d/dns.yml.j2 new file mode 100644 index 000000000..ed7f0e0a9 --- /dev/null +++ b/src/roles/foreman_proxy/templates/settings.d/dns.yml.j2 @@ -0,0 +1,5 @@ +--- +:enabled: {{ feature_enabled }} + +:use_provider: {{ foreman_proxy_dns_provider_name }} +:dns_ttl: {{ foreman_proxy_dns_ttl }} diff --git a/src/roles/foreman_proxy/templates/settings.d/dns_nsupdate.yml.j2 b/src/roles/foreman_proxy/templates/settings.d/dns_nsupdate.yml.j2 new file mode 100644 index 000000000..32da2d68e --- /dev/null +++ b/src/roles/foreman_proxy/templates/settings.d/dns_nsupdate.yml.j2 @@ -0,0 +1,3 @@ +--- +:dns_key: /etc/foreman-proxy/rndc.key +:dns_server: {{ foreman_proxy_dns_server }} diff --git a/tests/feature/foreman-proxy/base_test.py b/tests/feature/foreman-proxy/base_test.py index 35630a529..3bc6e9706 100644 --- a/tests/feature/foreman-proxy/base_test.py +++ b/tests/feature/foreman-proxy/base_test.py @@ -161,3 +161,56 @@ def test_registration_url(obsah_params): def test_registration_endpoint(proxy_v2_features): assert 'registration' in proxy_v2_features assert proxy_v2_features['registration'].get('state') == 'running' + + +@pytest.mark.feature('dns') +def test_dns_provider(proxy_v2_features): + settings = proxy_v2_features['dns'].get('settings', {}) + assert settings.get('use_provider') == 'dns_nsupdate' + + +@pytest.mark.feature('dns') +@pytest.mark.parametrize('dns_name,dns_value,dns_type', [ + ('test-v4.example.test', '192.0.2.100', 'A'), + ('test-v6.example.test', '2001:db8::1', 'AAAA'), + ('test-cname.example.test', 'test.example.test', 'CNAME'), + ('reverse-v4.example.test', '100.2.0.192.in-addr.arpa', 'PTR'), + ('reverse-v6.example.test', '0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa', 'PTR') +]) +def test_dns_nsupdate(server, certificates, server_fqdn, dns_name, dns_value, dns_type): + + def dns_cmd(curl_opts, extra_url=""): + return server.run( + f"curl --cacert {certificates['server_ca_certificate']} " + f"--cert {certificates['client_certificate']} " + f"--key {certificates['client_key']} " + f"--output /dev/null --write-out '%{{http_code}}' " + f"{curl_opts} " + f"https://{server_fqdn}:8443/dns/{extra_url}" + ) + + if dns_type == 'PTR': + expected = f"{dns_name}." + query = dns_value + elif dns_type == 'CNAME': + expected = f"{dns_value}." + query = dns_name + else: + expected = dns_value + query = dns_name + + create_cmd = dns_cmd(f"--data 'fqdn={dns_name}&value={dns_value}&type={dns_type}'") + assert create_cmd.succeeded + assert create_cmd.stdout == '200' + + verify_cmd = server.run(f"dig +short {query} {dns_type} @named.example.com") + assert verify_cmd.succeeded + assert verify_cmd.stdout.strip() == expected + + delete_cmd = dns_cmd("-X DELETE", f"{query}/{dns_type}") + assert delete_cmd.succeeded + assert delete_cmd.stdout == '200' + + verify_cmd = server.run(f"dig +short {query} {dns_type} @named.example.com") + assert verify_cmd.succeeded + assert verify_cmd.stdout.strip() == "" diff --git a/tests/features_test.py b/tests/features_test.py index 72c58cef8..89d44b6a4 100644 --- a/tests/features_test.py +++ b/tests/features_test.py @@ -12,6 +12,7 @@ 'content/deb', 'content/python', 'content/rpm', + 'dns', 'dynflow', 'foreman', 'foreman-proxy', @@ -29,7 +30,7 @@ EXPECTED_FEATURES = { 'default': DEFAULT_FEATURES, 'default-iop': DEFAULT_FEATURES | {'iop', 'rh-cloud'}, - 'upgrade': (DEFAULT_FEATURES | {'iop', 'rh-cloud'}) - {'ansible', 'webhooks'}, + 'upgrade': (DEFAULT_FEATURES | {'iop', 'rh-cloud'}) - {'ansible', 'dns', 'webhooks'}, 'migration': DEFAULT_FEATURES, 'proxy': { 'bmc',