From 867047773da35c9d0aa7d5533236c16960b39468 Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Wed, 6 May 2026 17:02:49 -0400 Subject: [PATCH 1/2] Consolidate certificate vars into a single file The default and custom_server certificate vars files defined identical paths since custom certificates are normalized into the same directory structure during deployment. Remove the vars file indirection and use a single certificates.yml for all certificate sources. Co-Authored-By: Claude Opus 4.6 --- development/playbooks/deploy-dev/deploy-dev.yaml | 2 +- src/playbooks/deploy/deploy.yaml | 2 +- .../{default_certificates.yml => certificates.yml} | 0 src/vars/custom_server_certificates.yml | 14 -------------- tests/conftest.py | 7 +++---- 5 files changed, 5 insertions(+), 20 deletions(-) rename src/vars/{default_certificates.yml => certificates.yml} (100%) delete mode 100644 src/vars/custom_server_certificates.yml diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index caf89c1d9..803b3816c 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -5,7 +5,7 @@ vars_files: - "../../../src/vars/defaults.yml" - "../../../src/vars/flavors/{{ flavor }}.yml" - - "../../../src/vars/{{ certificates_source }}_certificates.yml" + - "../../../src/vars/certificates.yml" - "../../../src/vars/images.yml" - "../../../src/vars/database.yml" - "../../../src/vars/foreman.yml" diff --git a/src/playbooks/deploy/deploy.yaml b/src/playbooks/deploy/deploy.yaml index 837d36c98..8fb18f0ff 100644 --- a/src/playbooks/deploy/deploy.yaml +++ b/src/playbooks/deploy/deploy.yaml @@ -6,7 +6,7 @@ vars_files: - "../../vars/defaults.yml" - "../../vars/flavors/{{ flavor }}.yml" - - "../../vars/{{ certificates_source }}_certificates.yml" + - "../../vars/certificates.yml" - "../../vars/images.yml" - "../../vars/tuning/{{ tuning }}.yml" - "../../vars/database.yml" diff --git a/src/vars/default_certificates.yml b/src/vars/certificates.yml similarity index 100% rename from src/vars/default_certificates.yml rename to src/vars/certificates.yml diff --git a/src/vars/custom_server_certificates.yml b/src/vars/custom_server_certificates.yml deleted file mode 100644 index a70f33583..000000000 --- a/src/vars/custom_server_certificates.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -certificates_ca_directory: /root/certificates -ca_key_password: "{{ certificates_ca_directory }}/private/ca.pwd" -ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" -ca_key: "{{ certificates_ca_directory }}/private/ca.key" -server_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}.crt" -server_key: "{{ certificates_ca_directory }}/private/{{ ansible_facts['fqdn'] }}.key" -server_ca_certificate: "{{ certificates_ca_directory }}/certs/server-ca.crt" -ca_bundle: "{{ certificates_ca_directory }}/certs/ca-bundle.crt" -client_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}-client.crt" -client_key: "{{ certificates_ca_directory }}/private/{{ ansible_facts['fqdn'] }}-client.key" -client_ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" -localhost_key: "{{ certificates_ca_directory }}/private/localhost.key" -localhost_certificate: "{{ certificates_ca_directory }}/certs/localhost.crt" diff --git a/tests/conftest.py b/tests/conftest.py index 9bf66ec3b..98cfce562 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,11 +77,10 @@ def client_fqdn(client): @pytest.fixture(scope="module") -def certificates(certificate_source, server_fqdn): +def certificates(server_fqdn): env = Environment(loader=FileSystemLoader("."), autoescape=select_autoescape()) - template = env.get_template(f"./src/vars/{certificate_source}_certificates.yml") - context = {'certificates_ca_directory': '/var/lib/foremanctl/certs', - 'ansible_facts': {'fqdn': server_fqdn}} + template = env.get_template("./src/vars/certificates.yml") + context = {'ansible_facts': {'fqdn': server_fqdn}} # we have vars that refer to other vars, so load them once and then re-render the template context.update(yaml.safe_load(template.render(context))) return yaml.safe_load(template.render(context)) From 9ae734c6fa9c0cca11696ad26c244e93833637a2 Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Wed, 6 May 2026 16:24:09 -0400 Subject: [PATCH 2/2] Auto-detect and normalize installer certificates Move foreman-installer certificate normalization into the migrate subcommand so it runs once during migration rather than on every deploy. The migrate_certificates role copies certs from /root/ssl-build/ into /root/certificates/, persists the CA passphrase into parameters.yaml, and backs up the original directory. Detect custom server certificates by comparing the internal CA with the server CA. When they differ, persist certificates_source: custom_server to prevent subsequent deploys from overwriting the custom server cert. Remove the installer certificate source since migrated certs use the default source paths after normalization. Mark certificate path parameters as IGNORE in the answer file migration since the role handles cert files directly. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test.yml | 87 +++++++++- .../installer-certs/installer-certs.yaml | 7 - .../mock-installer/mock-installer.yaml | 7 + .../foreman_installer_certs/tasks/main.yml | 16 -- .../mock_foreman_installer/tasks/main.yml | 34 ++++ docs/user/certificates.md | 38 ++--- .../_certificate_source/metadata.obsah.yaml | 3 +- src/playbooks/deploy/metadata.obsah.yaml | 3 - src/playbooks/migrate/migrate.yaml | 8 + src/plugins/modules/migrate_answers.py | 11 +- .../defaults/main.yml | 2 + .../migrate_foreman_installer/tasks/main.yml | 152 ++++++++++++++++++ src/vars/installer_certificates.yml | 24 --- tests/certificates_test.py | 2 +- tests/conftest.py | 2 +- .../installer-answers/katello-answers.yaml | 34 ++++ .../installer-answers/last_scenario.yaml | 4 + tests/migration_test.py | 61 +++++++ tests/unit/migrate_test.py | 19 +++ 19 files changed, 423 insertions(+), 91 deletions(-) delete mode 100644 development/playbooks/installer-certs/installer-certs.yaml create mode 100644 development/playbooks/mock-installer/mock-installer.yaml delete mode 100644 development/roles/foreman_installer_certs/tasks/main.yml create mode 100644 development/roles/mock_foreman_installer/tasks/main.yml create mode 100644 src/roles/migrate_foreman_installer/defaults/main.yml create mode 100644 src/roles/migrate_foreman_installer/tasks/main.yml delete mode 100644 src/vars/installer_certificates.yml create mode 100644 tests/fixtures/installer-answers/katello-answers.yaml create mode 100644 tests/fixtures/installer-answers/last_scenario.yaml create mode 100644 tests/migration_test.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 356fd5855..1fa2e9f3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,6 @@ jobs: matrix: certificate_source: - default - - installer security: - none database: @@ -67,9 +66,6 @@ jobs: - centos/stream10 iop: - enabled - exclude: - - certificate_source: installer - box: centos/stream10 include: - certificate_source: default security: fapolicyd @@ -115,10 +111,6 @@ jobs: - name: Configure repositories run: | ./forge setup-repositories - - name: Create installer certificates - if: contains(matrix.certificate_source, 'installer') - run: | - ./forge installer-certs - name: Create custom certificates if: matrix.certificate_source == 'custom_server' run: | @@ -311,6 +303,84 @@ jobs: ## If no one connects after 5 minutes, shut down server. wait-timeout-minutes: 5 + migration: + strategy: + fail-fast: false + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Setup libvirt for Vagrant + uses: voxpupuli/setup-vagrant@v0 + - name: Install Ansible + run: pip install --upgrade ansible-core + - name: Setup environment + run: ./setup-environment + - name: Start VMs + run: | + ./forge vms start --vms "quadlet client" + - name: Configure repositories + run: | + ./forge setup-repositories + - name: Mock foreman-installer environment + run: | + ./forge mock-installer + - name: Run image pull + run: | + ./foremanctl pull-images + - name: Run migration + run: | + ./foremanctl migrate --output /var/lib/foremanctl/parameters.yaml + - name: Run deployment + run: | + ./foremanctl deploy \ + --tuning development \ + --add-feature hammer \ + --add-feature foreman-proxy \ + --add-feature azure-rm \ + --add-feature google \ + --add-feature remote-execution + - name: Run tests + run: | + ./forge test --pytest-args="--certificate-source=default \ + --deselect tests/hammer_test.py::test_hammer_ping \ + --deselect tests/hammer_test.py::test_hammer_organizations_list \ + --deselect tests/foreman_proxy_test.py::test_foreman_proxy_features \ + --deselect tests/foreman_proxy_test.py::test_foreman_proxy_service \ + --deselect tests/foreman_proxy_test.py::test_foreman_proxy_port \ + --deselect tests/client_test.py::test_foreman_rex \ + --deselect tests/foreman_compute_resources_test.py::test_foreman_compute_resources[AzureRm] \ + --deselect tests/foreman_compute_resources_test.py::test_foreman_compute_resources[GCE] \ + --deselect tests/foreman_plugins_test.py::test_foreman_compute_resources[foreman_azure_rm] \ + --deselect tests/foreman_plugins_test.py::test_foreman_compute_resources[foreman_google]" + - name: Run smoker + run: | + ./forge smoker + - name: Archive smoker report + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: smoker-migration + path: "/home/runner/smoker/report/" + - name: Generate sos reports + if: ${{ always() }} + run: ./forge sos + - name: Archive sos reports + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: sosreport-migration + path: sos/ + - name: Setup upterm session + if: ${{ failure() }} + uses: owenthereal/action-upterm@v1 + with: + limit-access-to-actor: true + wait-timeout-minutes: 5 + # A dummy job that you can mark as a required check instead of each individual test test-suite: if: always() @@ -318,6 +388,7 @@ jobs: - tests - devel-tests - upgrade + - migration - ansible-lint - python-lint runs-on: ubuntu-latest diff --git a/development/playbooks/installer-certs/installer-certs.yaml b/development/playbooks/installer-certs/installer-certs.yaml deleted file mode 100644 index 8dfd687b9..000000000 --- a/development/playbooks/installer-certs/installer-certs.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: Deploy certificates based on foreman-installer - hosts: - - quadlet - become: true - roles: - - foreman_installer_certs diff --git a/development/playbooks/mock-installer/mock-installer.yaml b/development/playbooks/mock-installer/mock-installer.yaml new file mode 100644 index 000000000..64e5534e5 --- /dev/null +++ b/development/playbooks/mock-installer/mock-installer.yaml @@ -0,0 +1,7 @@ +--- +- name: Mock foreman-installer environment for migration testing + hosts: + - quadlet + become: true + roles: + - mock_foreman_installer diff --git a/development/roles/foreman_installer_certs/tasks/main.yml b/development/roles/foreman_installer_certs/tasks/main.yml deleted file mode 100644 index 484ab9c77..000000000 --- a/development/roles/foreman_installer_certs/tasks/main.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -- name: Enable foreman-installer PR 935 Copr repo - community.general.copr: - host: copr.fedorainfracloud.org - state: enabled - name: packit/theforeman-foreman-installer-935 - chroot: rhel-9-x86_64 - -- name: Install foreman-installer package - ansible.builtin.package: - name: foreman-installer-katello - -# utilize https://github.com/theforeman/foreman-installer/pull/935 -- name: Generate certs - ansible.builtin.command: foreman-certs --apache true --foreman true --candlepin true --iop true - changed_when: false diff --git a/development/roles/mock_foreman_installer/tasks/main.yml b/development/roles/mock_foreman_installer/tasks/main.yml new file mode 100644 index 000000000..f906019b9 --- /dev/null +++ b/development/roles/mock_foreman_installer/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- name: Enable foreman-installer PR 935 Copr repo + community.general.copr: + host: copr.fedorainfracloud.org + state: enabled + name: packit/theforeman-foreman-installer-935 + chroot: rhel-9-x86_64 + +- name: Install foreman-installer package + ansible.builtin.package: + name: foreman-installer-katello + +# utilize https://github.com/theforeman/foreman-installer/pull/935 +- name: Generate certs + ansible.builtin.command: foreman-certs --apache true --foreman true --candlepin true --iop true + changed_when: false + +- name: Create installer scenarios directory + ansible.builtin.file: + path: /etc/foreman-installer/scenarios.d + state: directory + mode: '0755' + +- name: Place answers file fixture + ansible.builtin.copy: + src: "{{ playbook_dir }}/../../../tests/fixtures/installer-answers/katello-answers.yaml" + dest: /etc/foreman-installer/scenarios.d/katello-answers.yaml + mode: '0600' + +- name: Place scenario file fixture + ansible.builtin.copy: + src: "{{ playbook_dir }}/../../../tests/fixtures/installer-answers/last_scenario.yaml" + dest: /etc/foreman-installer/scenarios.d/last_scenario.yaml + mode: '0644' diff --git a/docs/user/certificates.md b/docs/user/certificates.md index b9ad45408..d8d9dc885 100644 --- a/docs/user/certificates.md +++ b/docs/user/certificates.md @@ -6,7 +6,7 @@ This document describes how certificate generation and management works in forem ### Certificate Sources -foremanctl supports three certificate sources that determine how certificates are obtained: +foremanctl supports two certificate sources that determine how certificates are obtained: **Default Source (`certificate_source: default`)** - Automatically generates self-signed certificates during deployment @@ -19,11 +19,6 @@ foremanctl supports three certificate sources that determine how certificates ar - Server certificate, key, and CA bundle are copied to `/root/certificates/` - Certificate source persists across deployments; original files only needed on first deploy or when updating certificates -**Installer Source (`certificate_source: installer`)** -- Uses existing certificates from a previous `foreman-installer` deployment -- Useful for migration scenarios where certificates already exist -- Certificate files must be present at expected foreman-installer paths - ### Usage #### Using Auto-Generated Certificates (Default) @@ -59,13 +54,17 @@ foremanctl deploy \ foremanctl deploy --certificate-source=default ``` -#### Using Existing Installer Certificates +#### Migrating from foreman-installer + +When migrating from a `foreman-installer` deployment, use the `migrate` command to normalize existing certificates into foremanctl's canonical structure: ```bash -# Use certificates from previous foreman-installer -foremanctl deploy --certificate-source=installer +foremanctl migrate --output /var/lib/foremanctl/parameters.yaml +foremanctl deploy ``` +The `migrate` command copies certificates from `/root/ssl-build/` into `/root/certificates/`, persists the CA passphrase so foremanctl can issue new certificates, and backs up the original directory to `/root/ssl-build.bak/`. See the [migration guide](../migration-guide.md) for full details. + ### Certificate Locations After deployment, certificates are available at: @@ -81,10 +80,9 @@ After deployment, certificates are available at: - Server CA Certificate: `/root/certificates/certs/server-ca.crt` (custom CA that signed server cert) - Client Certificate: `/root/certificates/certs/-client.crt` (generated by internal CA) -**Installer Source:** -- CA Certificate: `/root/ssl-build/katello-default-ca.crt` -- Server Certificate: `/root/ssl-build//-apache.crt` -- Client Certificate: `/root/ssl-build//-foreman-client.crt` +**After Migration:** +- Certificates from `foreman-installer` are normalized into the same paths as the Default Source above +- Original directory is backed up to `/root/ssl-build.bak/` ### CNAME Support @@ -131,7 +129,6 @@ The `--certificate-renew` flag is **not persisted** in foremanctl’s answers fi - Uses the same lifetime for both client and server certificates - Limited certificate customization options -- Custom server certificates cannot be combined with `certificate_source: installer` - CNAMEs are only applied to certificates generated by the internal CA ## Internal Design @@ -174,10 +171,9 @@ For `certificate_source: custom_server`: 2. **Custom Server Certificates**: Copy the custom server cert, key, and CA bundle from user-provided paths to `/root/certificates/` (only when certificate paths are provided) 3. **Host Certificate Issuance**: Generate client certificate and localhost certificate signed by the internal CA (server cert for FQDN is skipped) -For `certificate_source: installer`: +#### Migration from foreman-installer -- Uses existing certificates from `/root/ssl-build/` generated by foreman-installer -- No certificate generation performed; files must already exist +The `foremanctl migrate` command includes a `migrate_foreman_installer` role that normalizes `foreman-installer` certificates into the canonical `/root/certificates/` structure. It also reads the CA passphrase from the installer's password file and persists it into foremanctl's configuration so that subsequent deploys can issue new certificates using the original CA. #### Variable System @@ -197,14 +193,6 @@ client_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqd - The `server_ca_certificate` points to the custom CA that signed the server certificate - The `ca_bundle` contains both the internal CA and custom server CA -**Installer Source (`src/vars/installer_certificates.yml`):** -```yaml -ca_certificate: "/root/ssl-build/katello-default-ca.crt" -server_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" -server_ca_certificate: "/root/ssl-build/katello-server-ca.crt" -client_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.crt" -``` - #### Integration with Deployment In `src/playbooks/deploy/deploy.yaml`: diff --git a/src/playbooks/_certificate_source/metadata.obsah.yaml b/src/playbooks/_certificate_source/metadata.obsah.yaml index 03eb3a847..2be245b6a 100644 --- a/src/playbooks/_certificate_source/metadata.obsah.yaml +++ b/src/playbooks/_certificate_source/metadata.obsah.yaml @@ -1,9 +1,8 @@ --- variables: certificates_source: - help: Where certificates are coming from. Currently default Ansible role, the foreman-installer, or custom server certificates. + help: Where certificates are coming from. Currently default Ansible role or custom server certificates. parameter: --certificate-source choices: - default - - installer - custom_server diff --git a/src/playbooks/deploy/metadata.obsah.yaml b/src/playbooks/deploy/metadata.obsah.yaml index c1a32b484..2b04775e3 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -60,9 +60,6 @@ variables: 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]] - include: - _certificate_source diff --git a/src/playbooks/migrate/migrate.yaml b/src/playbooks/migrate/migrate.yaml index 6fa2ec577..eefaafd73 100644 --- a/src/playbooks/migrate/migrate.yaml +++ b/src/playbooks/migrate/migrate.yaml @@ -3,6 +3,7 @@ hosts: - quadlet gather_facts: false + become: true tasks: - name: Run migration migrate_answers: @@ -27,3 +28,10 @@ vars: _unmappable_warning: "Warning: {{ migration_result.unmappable | length }} parameter(s) could not be mapped - see warnings above" _output_file_msg: "Output written to: {{ migration_result.output_file }}" + +- name: Migrate foreman-installer certificates + hosts: + - quadlet + become: true + roles: + - role: migrate_foreman_installer diff --git a/src/plugins/modules/migrate_answers.py b/src/plugins/modules/migrate_answers.py index 920538813..0f267549d 100755 --- a/src/plugins/modules/migrate_answers.py +++ b/src/plugins/modules/migrate_answers.py @@ -26,11 +26,13 @@ def cast_database_mode(value): # Foreman configuration ('foreman', 'initial_admin_username'): 'foreman_initial_admin_username', ('foreman', 'initial_admin_password'): 'foreman_initial_admin_password', + ('foreman', 'initial_organization'): 'foreman_initial_organization', + ('foreman', 'initial_location'): 'foreman_initial_location', - # Certificate configuration - ('foreman', 'server_ssl_cert'): 'server_certificate', - ('foreman', 'server_ssl_key'): 'server_key', - ('foreman', 'server_ssl_ca'): 'ca_certificate', + # Certificate paths are handled by the migrate_foreman_installer role + ('foreman', 'server_ssl_cert'): 'IGNORE', + ('foreman', 'server_ssl_key'): 'IGNORE', + ('foreman', 'server_ssl_ca'): 'IGNORE', # TODO: Add more mappings as discovered } @@ -167,6 +169,7 @@ def write_output(data, output_path=None, working_directory=None): absolute_path = os.path.join(working_directory, output_path) else: absolute_path = os.path.abspath(output_path) + os.makedirs(os.path.dirname(absolute_path), exist_ok=True) with open(absolute_path, 'w') as f: f.write(yaml_content) return absolute_path diff --git a/src/roles/migrate_foreman_installer/defaults/main.yml b/src/roles/migrate_foreman_installer/defaults/main.yml new file mode 100644 index 000000000..ee4b0ad9f --- /dev/null +++ b/src/roles/migrate_foreman_installer/defaults/main.yml @@ -0,0 +1,2 @@ +--- +migrate_foreman_installer_ca_directory: /root/certificates diff --git a/src/roles/migrate_foreman_installer/tasks/main.yml b/src/roles/migrate_foreman_installer/tasks/main.yml new file mode 100644 index 000000000..060e2cf9c --- /dev/null +++ b/src/roles/migrate_foreman_installer/tasks/main.yml @@ -0,0 +1,152 @@ +--- +- name: Check if installer certificates exist + ansible.builtin.stat: + path: /root/ssl-build/katello-default-ca.crt + register: migrate_foreman_installer_installer_ca + +- name: Normalize installer certificates + when: migrate_foreman_installer_installer_ca.stat.exists + block: + - name: Install crypto dependencies + ansible.builtin.package: + name: + - python3-cryptography + state: present + + - name: Create certificate directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ migrate_foreman_installer_ca_directory }}/certs" + - "{{ migrate_foreman_installer_ca_directory }}/private" + - "{{ migrate_foreman_installer_ca_directory }}/requests" + + - name: Copy CA certificate from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-default-ca.crt + dest: "{{ migrate_foreman_installer_ca_directory }}/certs/ca.crt" + remote_src: true + mode: '0444' + + - name: Copy server CA certificate from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-server-ca.crt + dest: "{{ migrate_foreman_installer_ca_directory }}/certs/server-ca.crt" + remote_src: true + mode: '0444' + + - name: Detect custom server certificates + ansible.builtin.stat: + path: "{{ item }}" + checksum_algorithm: sha256 + loop: + - /root/ssl-build/katello-default-ca.crt + - /root/ssl-build/katello-server-ca.crt + register: migrate_foreman_installer_ca_checksums + + - name: Set custom server certificate flag + ansible.builtin.set_fact: + migrate_foreman_installer_custom_server_certs: >- + {{ migrate_foreman_installer_ca_checksums.results[0].stat.checksum + != migrate_foreman_installer_ca_checksums.results[1].stat.checksum }} + + - name: Persist certificates_source for custom server certificates + ansible.builtin.lineinfile: + path: /var/lib/foremanctl/parameters.yaml + regexp: '^certificates_source:' + line: "certificates_source: custom_server" + create: true + mode: '0600' + when: migrate_foreman_installer_custom_server_certs + + - name: Create CA bundle from installer certificates + ansible.builtin.assemble: + src: "{{ migrate_foreman_installer_ca_directory }}/certs" + dest: "{{ migrate_foreman_installer_ca_directory }}/certs/ca-bundle.crt" + regexp: '(ca|server-ca)\.crt$' + mode: '0444' + + - name: Copy CA key from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-default-ca.key + dest: "{{ migrate_foreman_installer_ca_directory }}/private/ca.key" + remote_src: true + mode: '0440' + + - name: Copy CA password from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-default-ca.pwd + dest: "{{ migrate_foreman_installer_ca_directory }}/private/ca.pwd" + remote_src: true + mode: '0600' + + - name: Read CA password from installer + ansible.builtin.slurp: + src: /root/ssl-build/katello-default-ca.pwd + register: migrate_foreman_installer_installer_ca_password + no_log: true + + - name: Persist CA password to foremanctl configuration + ansible.builtin.lineinfile: + path: /var/lib/foremanctl/parameters.yaml + regexp: '^certificates_ca_password:' + line: "certificates_ca_password: \"{{ migrate_foreman_installer_installer_ca_password.content | b64decode | trim }}\"" + create: true + mode: '0600' + no_log: true + + - name: Copy server certificate from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" + dest: "{{ migrate_foreman_installer_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}.crt" + remote_src: true + mode: '0444' + + - name: Copy server key from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.key" + dest: "{{ migrate_foreman_installer_ca_directory }}/private/{{ ansible_facts['fqdn'] }}.key" + remote_src: true + mode: '0440' + + - name: Copy client certificate from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.crt" + dest: "{{ migrate_foreman_installer_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}-client.crt" + remote_src: true + mode: '0444' + + - name: Copy client key from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.key" + dest: "{{ migrate_foreman_installer_ca_directory }}/private/{{ ansible_facts['fqdn'] }}-client.key" + remote_src: true + mode: '0440' + + - name: Copy localhost certificate from installer + ansible.builtin.copy: + src: /root/ssl-build/localhost/localhost-tomcat.crt + dest: "{{ migrate_foreman_installer_ca_directory }}/certs/localhost.crt" + remote_src: true + mode: '0444' + + - name: Copy localhost key from installer + ansible.builtin.copy: + src: /root/ssl-build/localhost/localhost-tomcat.key + dest: "{{ migrate_foreman_installer_ca_directory }}/private/localhost.key" + remote_src: true + mode: '0440' + + - name: Backup installer certificate directory + ansible.builtin.copy: + src: /root/ssl-build/ + dest: /root/ssl-build.bak/ + remote_src: true + mode: preserve + + - name: Remove original installer certificate directory + ansible.builtin.file: + path: /root/ssl-build + state: absent diff --git a/src/vars/installer_certificates.yml b/src/vars/installer_certificates.yml deleted file mode 100644 index 6939e6310..000000000 --- a/src/vars/installer_certificates.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -ca_key_password: "/root/ssl-build/katello-default-ca.pwd" -ca_certificate: "/root/ssl-build/katello-default-ca.crt" -ca_key: "/root/ssl-build/katello-default-ca.key" -server_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" -server_key: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.key" -server_ca_certificate: "/root/ssl-build/katello-server-ca.crt" -ca_bundle: "/root/ssl-build/ca-bundle.crt" -client_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.crt" -client_key: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.key" -client_ca_certificate: "{{ ca_certificate }}" -localhost_key: "/root/ssl-build/localhost/localhost-tomcat.key" -localhost_certificate: "/root/ssl-build/localhost/localhost-tomcat.crt" - -iop_gateway_server_certificate: "/root/ssl-build/localhost/localhost-iop-core-gateway-server.crt" -iop_gateway_server_key: "/root/ssl-build/localhost/localhost-iop-core-gateway-server.key" -iop_gateway_server_ca_certificate: "/root/ssl-build/katello-default-ca.crt" -iop_gateway_client_certificate: "/root/ssl-build/localhost/localhost-iop-core-gateway-client.crt" -iop_gateway_client_key: "/root/ssl-build/localhost/localhost-iop-core-gateway-client.key" -iop_gateway_client_ca_certificate: "/root/ssl-build/katello-server-ca.crt" -iop_vmaas_client_ca_certificate: "/root/ssl-build/katello-server-ca.crt" -iop_cvemap_downloader_client_cert: "{{ client_certificate }}" -iop_cvemap_downloader_client_key: "{{ client_key }}" -iop_cvemap_downloader_client_ca: "{{ client_ca_certificate }}" diff --git a/tests/certificates_test.py b/tests/certificates_test.py index 4619221a1..e65a813b5 100644 --- a/tests/certificates_test.py +++ b/tests/certificates_test.py @@ -21,7 +21,7 @@ def test_default_server_ca_matches_internal_ca(server, certificates, default_cer ca_info = certificate_info(server, certificates['ca_certificate']) server_ca_info = certificate_info(server, certificates['server_ca_certificate']) assert ca_info['subject'] == server_ca_info['subject'], \ - "Default/installer server CA should match the internal CA" + "Default server CA should match the internal CA" def test_custom_server_ca_differs_from_internal_ca(server, certificates, custom_certificates): diff --git a/tests/conftest.py b/tests/conftest.py index 98cfce562..84d884769 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ def enabled_features(self): def pytest_addoption(parser): - parser.addoption("--certificate-source", action="store", default="default", choices=('default', 'installer', 'custom_server'), help="Certificate source used during deployment") + parser.addoption("--certificate-source", action="store", default="default", choices=('default', 'custom_server'), help="Certificate source used during deployment") parser.addoption("--database-mode", action="store", default="internal", choices=('internal', 'external'), help="Whether the database is internal or external") diff --git a/tests/fixtures/installer-answers/katello-answers.yaml b/tests/fixtures/installer-answers/katello-answers.yaml new file mode 100644 index 000000000..16f403834 --- /dev/null +++ b/tests/fixtures/installer-answers/katello-answers.yaml @@ -0,0 +1,34 @@ +--- +# Fixture representing a foreman-installer-katello answers file. +# Used by forge mock-installer to simulate what foreman-installer leaves behind. +# Passwords are intentionally fake placeholders. +foreman: + db_host: localhost + db_port: 5432 + db_database: foreman + db_username: foreman + db_password: changeme + db_manage: true + db_manage_rake: true + initial_admin_username: admin + initial_admin_password: changeme + initial_organization: "Foreman CI" + initial_location: "Internet" + server_ssl_cert: /etc/pki/katello/certs/katello-apache.crt + server_ssl_key: /etc/pki/katello/private/katello-apache.key + server_ssl_ca: /etc/pki/katello/certs/katello-default-ca.crt + db_adapter: postgresql + db_pool: 5 + oauth_active: true + oauth_consumer_key: changeme + oauth_consumer_secret: changeme +katello: + candlepin_db_host: localhost + candlepin_db_port: 5432 + candlepin_db_name: candlepin + candlepin_db_user: candlepin + candlepin_db_password: changeme + candlepin_manage_db: true + pulp_worker_count: 2 +puppet: + enabled: false diff --git a/tests/fixtures/installer-answers/last_scenario.yaml b/tests/fixtures/installer-answers/last_scenario.yaml new file mode 100644 index 000000000..a83421f52 --- /dev/null +++ b/tests/fixtures/installer-answers/last_scenario.yaml @@ -0,0 +1,4 @@ +--- +# Fixture representing /etc/foreman-installer/scenarios.d/last_scenario.yaml. +# The :answer_file key uses Ruby YAML symbol notation as written by foreman-installer. +":answer_file": "/etc/foreman-installer/scenarios.d/katello-answers.yaml" diff --git a/tests/migration_test.py b/tests/migration_test.py new file mode 100644 index 000000000..898a2b112 --- /dev/null +++ b/tests/migration_test.py @@ -0,0 +1,61 @@ +import pytest +import yaml + + +@pytest.fixture(scope="module") +def migrated_environment(server): + if not server.file("/root/ssl-build.bak").exists: + pytest.skip("Not a migrated environment") + + +def test_installer_directory_removed(server, migrated_environment): + assert not server.file("/root/ssl-build").exists + + +def test_installer_backup_exists(server, migrated_environment): + backup = server.file("/root/ssl-build.bak") + assert backup.exists + assert backup.is_directory + + +@pytest.mark.parametrize("subdir", ["certs", "private", "requests"]) +def test_certificate_directories(server, migrated_environment, subdir): + d = server.file(f"/root/certificates/{subdir}") + assert d.exists + assert d.is_directory + assert d.mode == 0o755 + + +def test_ca_password_file(server, migrated_environment): + f = server.file("/root/certificates/private/ca.pwd") + assert f.exists + assert f.mode == 0o600 + + +def test_ca_password_persisted(server, migrated_environment): + f = server.file("/var/lib/foremanctl/parameters.yaml") + assert f.exists + params = yaml.safe_load(f.content_string) + assert "certificates_ca_password" in params + assert len(params["certificates_ca_password"]) > 0 + + +def test_default_certs_no_custom_source(server, migrated_environment): + f = server.file("/var/lib/foremanctl/parameters.yaml") + assert f.exists + params = yaml.safe_load(f.content_string) + assert "certificates_source" not in params + + +def test_answers_migration_database_mode(server, migrated_environment): + f = server.file("/var/lib/foremanctl/parameters.yaml") + assert f.exists + params = yaml.safe_load(f.content_string) + assert params.get("database_mode") == "internal" + + +def test_answers_migration_admin_username(server, migrated_environment): + f = server.file("/var/lib/foremanctl/parameters.yaml") + assert f.exists + params = yaml.safe_load(f.content_string) + assert params.get("foreman_initial_admin_username") == "admin" diff --git a/tests/unit/migrate_test.py b/tests/unit/migrate_test.py index d95bec7ba..dbb3a0460 100644 --- a/tests/unit/migrate_test.py +++ b/tests/unit/migrate_test.py @@ -59,6 +59,25 @@ def test_ignore_parameters(self): assert 'db_manage_rake' not in str(result['unmappable']) assert result['mapped']['database_host'] == 'localhost' + def test_certificate_parameters_ignored(self): + """Test that certificate path parameters are ignored (handled by migration role)""" + old_config = { + 'foreman': { + 'server_ssl_cert': '/etc/pki/katello/certs/server.crt', + 'server_ssl_key': '/etc/pki/katello/private/server.key', + 'server_ssl_ca': '/etc/pki/katello/certs/ca.crt', + 'db_host': 'localhost' + } + } + + result = migrate_answers.apply_mappings(old_config) + + assert 'server_certificate' not in result['mapped'] + assert 'server_key' not in result['mapped'] + assert 'ca_certificate' not in result['mapped'] + assert not any('ssl' in p for p in result['unmappable']) + assert result['mapped']['database_host'] == 'localhost' + def test_unmappable_parameters(self): """Test that unmappable parameters are reported""" old_config = {