From 124b3666f998e3151b8d222be2b1887fde5b72b7 Mon Sep 17 00:00:00 2001 From: Leos Stejskal Date: Tue, 2 Jun 2026 11:55:37 +0200 Subject: [PATCH] TFTP feature --- .github/workflows/test.yml | 5 +++ .../playbooks/tftp/metadata.obsah.yaml | 3 ++ development/playbooks/tftp/tftp.yaml | 21 +++++++++++++ docs/user/parameters.md | 13 +++++--- src/features.yaml | 4 +++ src/playbooks/deploy/metadata.obsah.yaml | 10 +++++- src/roles/foreman_proxy/defaults/main.yaml | 4 +++ .../foreman_proxy/tasks/feature/tftp.yaml | 21 +++++++++++++ src/roles/foreman_proxy/tasks/main.yaml | 2 ++ .../templates/settings.d/tftp.yml.j2 | 4 +++ tests/conftest.py | 23 ++++++++++++++ tests/foreman_proxy_test.py | 31 +++++++++++++------ 12 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 development/playbooks/tftp/metadata.obsah.yaml create mode 100644 development/playbooks/tftp/tftp.yaml create mode 100644 src/roles/foreman_proxy/tasks/feature/tftp.yaml create mode 100644 src/roles/foreman_proxy/templates/settings.d/tftp.yml.j2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd32d16e1..fb54fed0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,6 +128,9 @@ jobs: if: matrix.security != 'none' run: | ./forge security --mode ${{ matrix.security }} + - name: Setup TFTP server + run: | + ./forge tftp - name: Apply fapolicyd workarounds # https://access.redhat.com/solutions/7072618 / https://issues.redhat.com/browse/RHEL-37912 # https://github.com/theforeman/foreman-fapolicyd/blob/develop/15-foreman-container.rules @@ -160,6 +163,8 @@ jobs: --add-feature google \ --add-feature remote-execution \ --add-feature bmc \ + --add-feature tftp \ + --tftp-servername 127.0.0.1 \ ${{ matrix.iop == 'enabled' && '--add-feature iop' || '' }} - name: Run tests run: | diff --git a/development/playbooks/tftp/metadata.obsah.yaml b/development/playbooks/tftp/metadata.obsah.yaml new file mode 100644 index 000000000..031999d0e --- /dev/null +++ b/development/playbooks/tftp/metadata.obsah.yaml @@ -0,0 +1,3 @@ +--- +help: | + Setup TFTP server diff --git a/development/playbooks/tftp/tftp.yaml b/development/playbooks/tftp/tftp.yaml new file mode 100644 index 000000000..8d68bb1b7 --- /dev/null +++ b/development/playbooks/tftp/tftp.yaml @@ -0,0 +1,21 @@ +--- +- name: TFTP + hosts: + - quadlet + become: true + tasks: + - name: Install packages + ansible.builtin.package: + name: + - tftp-server + - tftp + state: present + + - name: Start TFTP services + ansible.builtin.systemd: + name: "{{ item }}" + state: started + enabled: true + with_items: + - tftp.socket + - tftp.service diff --git a/docs/user/parameters.md b/docs/user/parameters.md index b106abe32..d98597a4f 100644 --- a/docs/user/parameters.md +++ b/docs/user/parameters.md @@ -112,6 +112,15 @@ There are multiple use cases from the users perspective that dictate what parame | `--add-feature bmc` | Enable BMC feature | `--foreman-proxy-bmc` | | `--bmc-ipmi-implementation` | IPMI implementation to use for BMC | `--foreman-proxy-bmc-default-provider` | | `--bmc-redfish-verify-ssl` | Verify SSL certificates for Redfish BMC connections | `--foreman-proxy-bmc-redfish-verify-ssl` | +| `--add-feature tftp` | Enable TFTP feature | `--foreman-proxy-tftp` | +| `--tftp-servername` | IP address of the TFTP server (required when enabling TFTP) | `--foreman-proxy-tftp-servername` | +| `--tftp-root` | Directory to serve TFTP files from | `--foreman-proxy-tftp-root` | + +### Unmapped + +| foreman-installer Parameter | Description | Reason | +| --------------------------- | ----------- | ------ | +| `--foreman-proxy-tftp-managed` | Installer managed TFTP | not supported | ### Undetermined @@ -144,10 +153,6 @@ There are multiple use cases from the users perspective that dictate what parame | `--foreman-proxy-plugin-dns-route53-aws-access-key` | | foreman_proxy::plugin::dns_route53 | aws_access_key | | `--foreman-proxy-plugin-dns-route53-aws-secret-key` | | foreman_proxy::plugin::dns_route53 | aws_secret_key | | `--foreman-proxy-httpboot` | | foreman_proxy | httpboot | -| `--foreman-proxy-tftp` | | foreman_proxy | tftp | -| `--foreman-proxy-tftp-servername` | | foreman_proxy | tftp_servername | -| `--foreman-proxy-tftp-managed` | | foreman_proxy | tftp_managed | -| `--foreman-proxy-tftp-root` | | foreman_proxy | tftp_root | | `--foreman-proxy-plugin-dhcp-remote-isc-dhcp-config` | | foreman_proxy::plugin::dhcp_remote_isc | config | | `--foreman-proxy-plugin-dhcp-remote-isc-dhcp-leases` | | foreman_proxy::plugin::dhcp_remote_isc | dhcp_config | | `--foreman-proxy-plugin-dhcp-remote-isc-key-name` | | foreman_proxy::plugin::dhcp_remote_isc | key_name | diff --git a/src/features.yaml b/src/features.yaml index daf451bdc..95827c957 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -58,3 +58,7 @@ bmc: description: Power management for bare metal hosts (IPMI, Redfish) foreman_proxy: plugin_name: bmc +tftp: + description: Enable TFTP feature on Foreman Proxy + foreman_proxy: + plugin_name: tftp diff --git a/src/playbooks/deploy/metadata.obsah.yaml b/src/playbooks/deploy/metadata.obsah.yaml index c1a32b484..c855f8285 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -56,13 +56,21 @@ variables: parameter: --bmc-redfish-verify-ssl help: Verify SSL certificates for Redfish BMC connections. type: Boolean + foreman_proxy_tftp_root: + parameter: --tftp-root + help: Directory to serve TFTP files from. + type: AbsolutePath + foreman_proxy_tftp_servername: + parameter: --tftp-servername + help: Server name or IP address for TFTP. TFTP clients typically do not have DNS available, so an IP address is recommended. constraints: required_together: - [certificates_custom_server_certificate, certificates_custom_server_key, certificates_custom_server_ca_certificate] forbidden_if: - [certificates_source, installer, [certificates_custom_server_certificate, certificates_custom_server_key, certificates_custom_server_ca_certificate]] - + required_in_list: + - [[[features, tftp]], [foreman_proxy_tftp_servername]] include: - _certificate_source diff --git a/src/roles/foreman_proxy/defaults/main.yaml b/src/roles/foreman_proxy/defaults/main.yaml index 4791507b1..e0c764a1e 100644 --- a/src/roles/foreman_proxy/defaults/main.yaml +++ b/src/roles/foreman_proxy/defaults/main.yaml @@ -22,3 +22,7 @@ foreman_proxy_foreman_server_url: "https://{{ ansible_facts['fqdn'] }}" # BMC settings foreman_proxy_bmc_ipmi_implementation: ipmitool foreman_proxy_bmc_redfish_verify_ssl: true + +# TFTP settings +foreman_proxy_tftp_root: /var/lib/tftpboot +foreman_proxy_tftp_servername: "{{ undef(hint='You must specify a TFTP servername') }}" diff --git a/src/roles/foreman_proxy/tasks/feature/tftp.yaml b/src/roles/foreman_proxy/tasks/feature/tftp.yaml new file mode 100644 index 000000000..47775ffac --- /dev/null +++ b/src/roles/foreman_proxy/tasks/feature/tftp.yaml @@ -0,0 +1,21 @@ +--- +- name: TFTP root directory + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy + block: + - name: Create TFTP root directory + ansible.builtin.file: + path: "{{ foreman_proxy_tftp_root }}" + state: directory + mode: "0777" + + - name: Mount the directory into foreman-proxy container + ansible.builtin.copy: + dest: /etc/containers/systemd/foreman-proxy.container.d/tftp-root.conf + content: | + [Container] + Volume={{ foreman_proxy_tftp_root }}:/var/lib/tftpboot:rw + mode: '0644' + owner: root + group: root diff --git a/src/roles/foreman_proxy/tasks/main.yaml b/src/roles/foreman_proxy/tasks/main.yaml index 8033c9457..5724d876b 100644 --- a/src/roles/foreman_proxy/tasks/main.yaml +++ b/src/roles/foreman_proxy/tasks/main.yaml @@ -16,6 +16,8 @@ sdnotify: true network: host hostname: "{{ ansible_facts['hostname'] }}.local" + security_opt: + - "label=disable" secrets: - 'foreman-proxy-settings-yml,type=mount,target=/etc/foreman-proxy/settings.yml' - 'foreman-proxy-ssl-ca,type=mount,target=/etc/foreman-proxy/ssl_ca.pem' diff --git a/src/roles/foreman_proxy/templates/settings.d/tftp.yml.j2 b/src/roles/foreman_proxy/templates/settings.d/tftp.yml.j2 new file mode 100644 index 000000000..c91a9fda2 --- /dev/null +++ b/src/roles/foreman_proxy/templates/settings.d/tftp.yml.j2 @@ -0,0 +1,4 @@ +--- +:enabled: {{ feature_enabled }} +:tftproot: /var/lib/tftpboot +:tftp_servername: {{ foreman_proxy_tftp_servername }} diff --git a/tests/conftest.py b/tests/conftest.py index 9bf66ec3b..0aa14795a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ from requests.adapters import HTTPAdapter SSH_CONFIG = './.tmp/ssh-config' +FOREMAN_PROXY_PORT = 8443 class UserParameters: @@ -268,6 +269,28 @@ def get_connection_with_tls_context(self, request, verify, proxies=None, cert=No return conn +@pytest.fixture(scope="module") +def proxy_request(server, certificates, server_fqdn): + port = FOREMAN_PROXY_PORT + + def _request(path, method=None, data=None, return_body=False): + curl_opts = ( + f"--cacert {certificates['server_ca_certificate']} " + f"--cert {certificates['client_certificate']} " + f"--key {certificates['client_key']} " + f"--silent " + ) + if not return_body: + curl_opts += "--output /dev/null --write-out '%{http_code}' " + if method: + curl_opts += f"-X {method} " + if data: + curl_opts += f"-d '{data}' " + return server.run(f"curl {curl_opts}https://{server_fqdn}:{port}/{path}") + + return _request + + @pytest.fixture(scope="module") def local_request(ssh_config, server_fqdn): session = requests.Session() diff --git a/tests/foreman_proxy_test.py b/tests/foreman_proxy_test.py index 71ad8cbc0..c441f7823 100644 --- a/tests/foreman_proxy_test.py +++ b/tests/foreman_proxy_test.py @@ -3,23 +3,18 @@ import pytest -FOREMAN_PROXY_PORT = 8443 +from tests.conftest import FOREMAN_PROXY_PORT @pytest.fixture(scope="module") -def proxy_v2_features(server, certificates, server_fqdn): - cmd = server.run( - f"curl --cacert {certificates['server_ca_certificate']} " - f"--cert {certificates['client_certificate']} " - f"--key {certificates['client_key']} " - f"--silent https://{server_fqdn}:{FOREMAN_PROXY_PORT}/v2/features" - ) +def proxy_v2_features(proxy_request): + cmd = proxy_request("v2/features", return_body=True) assert cmd.succeeded, f"Failed to query /v2/features: {cmd.stderr}" return json.loads(cmd.stdout) -def test_foreman_proxy_features(server, certificates, server_fqdn, enabled_features): - cmd = server.run(f"curl --cacert {certificates['server_ca_certificate']} --silent https://{server_fqdn}:{FOREMAN_PROXY_PORT}/features") +def test_foreman_proxy_features(proxy_request, enabled_features): + cmd = proxy_request("features", return_body=True) assert cmd.succeeded features = json.loads(cmd.stdout) assert "logs" in features @@ -29,6 +24,10 @@ def test_foreman_proxy_features(server, certificates, server_fqdn, enabled_featu assert "bmc" in features else: assert "bmc" not in features + if 'tftp' in enabled_features: + assert "tftp" in features + else: + assert "tftp" not in features def test_foreman_proxy_service(server): @@ -56,6 +55,18 @@ def test_foreman_proxy_client_auth_to_foreman(server, certificates, server_fqdn) assert cmd.stdout == '201' +@pytest.mark.feature('tftp') +def test_tftp_write_and_fetch(proxy_request, server): + test_mac = "aa:bb:cc:dd:ee:ff" + cmd = proxy_request(f"tftp/{test_mac}", data="syslinux_config=foremanctl+test+probe") + assert cmd.succeeded + assert cmd.stdout == '200', f"Expected HTTP 200 when creating TFTP PXE config, got {cmd.stdout}" + + cmd = server.run("tftp 127.0.0.1 -c get pxelinux.cfg/01-aa-bb-cc-dd-ee-ff /tmp/foremanctl_tftp_test_download") + assert cmd.succeeded, f"TFTP get failed: {cmd.stdout}" + assert server.file("/tmp/foremanctl_tftp_test_download").content_string == "foremanctl test probe" + + @pytest.mark.feature('bmc') def test_bmc_capabilities(proxy_v2_features): assert 'bmc' in proxy_v2_features