From d11ad8fd1bd20ee409adbe2268fe5f464f8f72f3 Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Wed, 6 May 2026 17:02:49 -0400 Subject: [PATCH 1/3] 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 +- docs/user/certificates.md | 8 ++------ src/playbooks/deploy/deploy.yaml | 2 +- .../{default_certificates.yml => certificates.yml} | 0 src/vars/custom_server_certificates.yml | 14 -------------- tests/conftest.py | 7 +++---- 6 files changed, 7 insertions(+), 26 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 86c220325..d716d3abe 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/docs/user/certificates.md b/docs/user/certificates.md index bc9405619..ad6f9cdb5 100644 --- a/docs/user/certificates.md +++ b/docs/user/certificates.md @@ -181,9 +181,8 @@ For `certificate_source: installer`: #### Variable System -Certificate paths are defined in source-specific variable files: +Certificate paths are defined in `src/vars/certificates.yml`: -**Default Source (`src/vars/default_certificates.yml`):** ```yaml ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" ca_bundle: "{{ certificates_ca_directory }}/certs/ca-bundle.crt" @@ -192,10 +191,7 @@ server_ca_certificate: "{{ certificates_ca_directory }}/certs/server-ca.crt" client_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}-client.crt" ``` -**Custom Server Source (`src/vars/custom_server_certificates.yml`):** -- Uses the same paths as default source -- 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 +All certificate sources use the same paths. For custom server certificates, `server_ca_certificate` points to the custom CA that signed the server certificate, and `ca_bundle` contains both the internal CA and custom server CA. **Installer Source (`src/vars/installer_certificates.yml`):** ```yaml diff --git a/src/playbooks/deploy/deploy.yaml b/src/playbooks/deploy/deploy.yaml index a70d57371..29940f6ad 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 078ade090..000000000 --- a/src/vars/custom_server_certificates.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -certificates_ca_directory: /var/lib/foremanctl/certs -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 30708710e8e046b3649266bfa071861071b6fd60 Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Thu, 11 Jun 2026 15:07:15 -0400 Subject: [PATCH 2/3] Preserve existing CA artifacts during deploy Guard CA generation with a stat check so existing CA artifacts are never overwritten during deploy. Without this, the community.crypto modules detect parameter mismatches (different subject, passphrase) against a migrated CA and silently regenerate it, breaking certificate trust. The CA generation block now only runs when ca.crt does not exist (fresh install) or when certificates_renew_ca is explicitly set to true. Add --certificate-renew-ca as a deploy parameter for explicit CA renewal. Co-Authored-By: Claude Opus 4.6 --- docs/user/certificates.md | 11 +- .../_certificate_validity/metadata.obsah.yaml | 5 + src/roles/certificates/defaults/main.yml | 1 + src/roles/certificates/tasks/ca.yml | 118 ++++++++++-------- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/docs/user/certificates.md b/docs/user/certificates.md index ad6f9cdb5..045a6943c 100644 --- a/docs/user/certificates.md +++ b/docs/user/certificates.md @@ -125,7 +125,16 @@ To re-issue server and client certificates that foremanctl generated earlier, wh foremanctl deploy --certificate-renew ``` -The `--certificate-renew` flag is **not persisted** in foremanctl’s answers file (one-shot). +To regenerate the CA certificate itself, use: + +```bash +foremanctl deploy --certificate-renew-ca +``` + +> [!WARNING] +> Regenerating the CA invalidates all previously issued certificates. Does not apply to custom certificates. + +Both flags are **not persisted** in foremanctl’s answers file (one-shot). ### Current Limitations diff --git a/src/playbooks/_certificate_validity/metadata.obsah.yaml b/src/playbooks/_certificate_validity/metadata.obsah.yaml index 6629b6ba1..55a16fb8b 100644 --- a/src/playbooks/_certificate_validity/metadata.obsah.yaml +++ b/src/playbooks/_certificate_validity/metadata.obsah.yaml @@ -11,3 +11,8 @@ variables: parameter: --certificate-renew action: store_true persist: false + certificates_renew_ca: + help: Regenerate the CA certificate. Does not apply to custom certificates. + parameter: --certificate-renew-ca + action: store_true + persist: false diff --git a/src/roles/certificates/defaults/main.yml b/src/roles/certificates/defaults/main.yml index 3ea553564..7b3488033 100644 --- a/src/roles/certificates/defaults/main.yml +++ b/src/roles/certificates/defaults/main.yml @@ -12,3 +12,4 @@ certificates_algorithm_size: 4096 certificates_ca_validity_days: 7300 certificates_validity_days: 7300 certificates_renew: false +certificates_renew_ca: false diff --git a/src/roles/certificates/tasks/ca.yml b/src/roles/certificates/tasks/ca.yml index b098ad885..e0a65b08e 100644 --- a/src/roles/certificates/tasks/ca.yml +++ b/src/roles/certificates/tasks/ca.yml @@ -23,63 +23,71 @@ state: directory mode: '0755' -- name: 'Create CA key password file' - ansible.builtin.copy: - content: "{{ certificates_ca_password }}" - dest: "{{ certificates_ca_directory_keys }}/ca.pwd" - owner: root - group: root - mode: '0600' - no_log: true +- name: 'Check if CA certificate exists' + ansible.builtin.stat: + path: "{{ certificates_ca_directory_certs }}/ca.crt" + register: _certificates_ca_cert -- name: 'Create CA private key' - community.crypto.openssl_privatekey: - path: "{{ certificates_ca_directory_keys }}/ca.key" - type: "{{ certificates_algorithm_type }}" - size: "{{ certificates_algorithm_size }}" - passphrase: "{{ certificates_ca_password }}" - owner: root - group: root - mode: '0600' +- name: 'Generate CA' + when: not _certificates_ca_cert.stat.exists or certificates_renew_ca + block: + - name: 'Create CA key password file' + ansible.builtin.copy: + content: "{{ certificates_ca_password }}" + dest: "{{ certificates_ca_directory_keys }}/ca.pwd" + owner: root + group: root + mode: '0600' + no_log: true -- name: 'Create CA certificate signing request' - community.crypto.openssl_csr: - path: "{{ certificates_ca_directory_requests }}/ca.csr" - privatekey_path: "{{ certificates_ca_directory_keys }}/ca.key" - privatekey_passphrase: "{{ certificates_ca_password }}" - common_name: "{{ certificates_ca_subject }}" - use_common_name_for_san: false - basic_constraints: - - 'CA:TRUE' - basic_constraints_critical: true - key_usage: - - keyCertSign - - cRLSign - - digitalSignature - key_usage_critical: true - create_subject_key_identifier: true + - name: 'Create CA private key' + community.crypto.openssl_privatekey: + path: "{{ certificates_ca_directory_keys }}/ca.key" + type: "{{ certificates_algorithm_type }}" + size: "{{ certificates_algorithm_size }}" + passphrase: "{{ certificates_ca_password }}" + owner: root + group: root + mode: '0600' -- name: 'Create self-signed CA certificate' - community.crypto.x509_certificate: - path: "{{ certificates_ca_directory_certs }}/ca.crt" - csr_path: "{{ certificates_ca_directory_requests }}/ca.csr" - privatekey_path: "{{ certificates_ca_directory_keys }}/ca.key" - privatekey_passphrase: "{{ certificates_ca_password }}" - provider: selfsigned - selfsigned_not_after: "+{{ certificates_ca_validity_days }}d" + - name: 'Create CA certificate signing request' + community.crypto.openssl_csr: + path: "{{ certificates_ca_directory_requests }}/ca.csr" + privatekey_path: "{{ certificates_ca_directory_keys }}/ca.key" + privatekey_passphrase: "{{ certificates_ca_password }}" + common_name: "{{ certificates_ca_subject }}" + use_common_name_for_san: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + key_usage: + - keyCertSign + - cRLSign + - digitalSignature + key_usage_critical: true + create_subject_key_identifier: true + + - name: 'Create self-signed CA certificate' + community.crypto.x509_certificate: + path: "{{ certificates_ca_directory_certs }}/ca.crt" + csr_path: "{{ certificates_ca_directory_requests }}/ca.csr" + privatekey_path: "{{ certificates_ca_directory_keys }}/ca.key" + privatekey_passphrase: "{{ certificates_ca_password }}" + provider: selfsigned + selfsigned_not_after: "+{{ certificates_ca_validity_days }}d" -- name: 'Copy CA as server CA certificate' - ansible.builtin.copy: - src: "{{ certificates_ca_directory_certs }}/ca.crt" - dest: "{{ certificates_ca_directory_certs }}/server-ca.crt" - remote_src: true - force: false - mode: '0444' + - name: 'Copy CA as server CA certificate' + ansible.builtin.copy: + src: "{{ certificates_ca_directory_certs }}/ca.crt" + dest: "{{ certificates_ca_directory_certs }}/server-ca.crt" + remote_src: true + force: false + mode: '0444' -- name: 'Create CA bundle' - ansible.builtin.copy: - src: "{{ certificates_ca_directory_certs }}/ca.crt" - dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" - remote_src: true - force: false - mode: '0444' + - name: 'Create CA bundle' + ansible.builtin.copy: + src: "{{ certificates_ca_directory_certs }}/ca.crt" + dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" + remote_src: true + force: false + mode: '0444' From 0b9fe721464435b1846bca09fd7cf196e569960d Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Wed, 6 May 2026 16:24:09 -0400 Subject: [PATCH 3/3] 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_foreman_installer role copies certs from /root/ssl-build/ into /var/lib/foremanctl/certs/, persists the CA passphrase to a dedicated file, 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. Separate I/O from the migrate_answers module so it only transforms and returns mapped parameters. The playbook handles writing to stdout, output files, and the parameters file. Migration is preview-by-default and requires --apply to perform changes. Update integration tests to read control-node state files from OBSAH_STATE rather than hardcoding paths or checking the remote server. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test.yml | 81 +++++++++- .../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/iop.md | 8 +- docs/migration-guide.md | 64 +++++--- docs/user/certificates.md | 40 ++--- .../_certificate_source/metadata.obsah.yaml | 3 +- src/playbooks/deploy/metadata.obsah.yaml | 3 - src/playbooks/migrate/metadata.obsah.yaml | 24 ++- src/playbooks/migrate/migrate.yaml | 40 +++-- src/plugins/modules/migrate_answers.py | 47 +----- .../defaults/main.yml | 2 + .../migrate_foreman_installer/tasks/main.yml | 153 ++++++++++++++++++ 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 | 71 ++++++++ tests/unit/migrate_test.py | 34 ++-- 22 files changed, 510 insertions(+), 190 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 bd32d16e1..449db289b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,7 +58,6 @@ jobs: matrix: certificate_source: - default - - installer security: - none database: @@ -68,9 +67,6 @@ jobs: - centos/stream10 iop: - enabled - exclude: - - certificate_source: installer - box: centos/stream10 include: - certificate_source: default security: fapolicyd @@ -116,10 +112,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: | @@ -309,6 +301,78 @@ 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 preview + run: | + mkdir -p .var/lib/foremanctl + ./foremanctl migrate + - name: Run migration + run: | + ./foremanctl migrate --apply + - 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 + - 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() @@ -316,6 +380,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/iop.md b/docs/iop.md index f29847ac2..e767f7395 100644 --- a/docs/iop.md +++ b/docs/iop.md @@ -122,18 +122,12 @@ Set in the playbook vars or inventory to match your Foreman deployment: ### Certificates -Gateway certificates are configured per certificate source: +Gateway certificates use the default certificate paths: -**Default certificates** (`certificate_source: default`): - Server: `/var/lib/foremanctl/certs/certs/localhost.crt` - Client: `/var/lib/foremanctl/certs/certs/localhost-client.crt` - CA: `/var/lib/foremanctl/certs/certs/ca.crt` -**Installer certificates** (`certificate_source: installer`): -- Server: `/root/ssl-build/localhost/localhost-iop-core-gateway-server.crt` -- Client: `/root/ssl-build/localhost/localhost-iop-core-gateway-client.crt` -- CA: `/root/ssl-build/katello-default-ca.crt` - ### Container Images All IOP images default to `quay.io/iop/:foreman-3.18`. Each role exposes `iop__container_image` and `iop__container_tag` variables to override. diff --git a/docs/migration-guide.md b/docs/migration-guide.md index acccd3dcb..b07cc4f32 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -4,7 +4,7 @@ When upgrading from foreman-installer to foremanctl, the `foremanctl migrate` command helps convert your existing configuration to the new format. -This guide explains how to migrate your foreman-installer answer files to foremanctl configuration files. +By default, `foremanctl migrate` previews the migration without making any changes. Use `--apply` to perform the actual migration. ## Prerequisites @@ -25,46 +25,69 @@ Before migrating, ensure the following: ## Migration Workflow -1. **Generate the migrated configuration**: +1. **Preview the migration** (no changes are made): ```bash - foremanctl migrate --output /var/lib/foremanctl/parameters.yaml + foremanctl migrate ``` 2. **Review the output** for any warnings about unmapped parameters -3. **Use the migrated configuration** with foremanctl: +3. **Apply the migration** when satisfied: + ```bash + foremanctl migrate --apply + ``` + +4. **Deploy using foremanctl**: ```bash foremanctl deploy ``` - (foremanctl automatically loads configuration from `/var/lib/foremanctl/parameters.yaml`) ## Command Usage -### Basic Migration +### Preview Migration -Migrate from the default location (reads the currently active scenario): +Preview the migrated configuration without making any changes: ```bash -foremanctl migrate --output /var/lib/foremanctl/parameters.yaml +foremanctl migrate ``` +This shows: +- Mapped answer file parameters and their new values +- Unmappable parameters that need manual review +- Certificate state detected on the system + +### Apply Migration + +Perform the actual migration: +```bash +foremanctl migrate --apply +``` + +This: +- Writes migrated parameters to the foremanctl configuration +- Normalizes installer certificates into `/var/lib/foremanctl/certs/` +- Backs up the original `/root/ssl-build/` directory to `/root/ssl-build.bak/` + ### Custom Answer File Migrate from a specific answer file: ```bash -foremanctl migrate --answer-file /path/to/custom-answers.yaml --output /var/lib/foremanctl/parameters.yaml +foremanctl migrate --answer-file /path/to/custom-answers.yaml +foremanctl migrate --apply --answer-file /path/to/custom-answers.yaml ``` -### Output to stdout +### Write to a Custom Path -Preview the migrated configuration without writing a file: +Write the migrated parameters to a specific file for inspection: ```bash -foremanctl migrate +foremanctl migrate --output /tmp/migrated.yaml ``` ## Command Options +- `--apply` - Perform the migration. Without this flag, only previews what would happen. - `--answer-file PATH` - Path to the foreman-installer answer file. If not specified, reads the currently active scenario and extracts the answer file path from it. -- `--output PATH` - Path for the migrated configuration (default: stdout) +- `--output PATH` - Path for the migrated configuration. If not specified and `--apply` is used, writes to the foremanctl configuration. > [!NOTE] > Unlike other `foremanctl` commands, migrate does not persist parameters between runs. Each migration is independent. @@ -117,20 +140,9 @@ These parameters need to be manually reviewed and added to the new configuration ## Using the Migrated Configuration -Once you've generated and reviewed the migrated configuration: - -1. **Save it to the foremanctl parameters file**: - ```bash - # Either generate directly to the parameters file - foremanctl migrate --output /var/lib/foremanctl/parameters.yaml - - # Or copy after review - foremanctl migrate --output /tmp/migrated.yaml - # Review /tmp/migrated.yaml - cp /tmp/migrated.yaml /var/lib/foremanctl/parameters.yaml - ``` +Once you've applied the migration: -2. **Deploy using foremanctl**: +1. **Deploy using foremanctl**: ```bash foremanctl deploy ``` diff --git a/docs/user/certificates.md b/docs/user/certificates.md index 045a6943c..b6fe9c0c7 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 `/var/lib/foremanctl/certs/` - 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 --apply +foremanctl deploy ``` +The `migrate --apply` command copies certificates from `/root/ssl-build/` into `/var/lib/foremanctl/certs/`, persists the CA passphrase so foremanctl can issue new certificates, and backs up the original directory to `/root/ssl-build.bak/`. Run `foremanctl migrate` without `--apply` to preview the migration first. 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: `/var/lib/foremanctl/certs/certs/server-ca.crt` (custom CA that signed server cert) - Client Certificate: `/var/lib/foremanctl/certs/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 @@ -140,7 +138,6 @@ Both flags are **not persisted** in foremanctl’s answers file (one-shot). - 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 @@ -183,10 +180,9 @@ For `certificate_source: custom_server`: 2. **Custom Server Certificates**: Copy the custom server cert, key, and CA bundle from user-provided paths to `/var/lib/foremanctl/certs/` (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 --apply` command includes a `migrate_foreman_installer` role that normalizes `foreman-installer` certificates into the canonical `/var/lib/foremanctl/certs/` 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 @@ -202,19 +198,11 @@ client_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqd All certificate sources use the same paths. For custom server certificates, `server_ca_certificate` points to the custom CA that signed the server certificate, and `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`: -1. **Variable Loading**: Loads certificate variables based on `certificate_source` +1. **Variable Loading**: Loads certificate variables from `src/vars/certificates.yml` 2. **Certificate Generation**: Runs `certificates` role when `certificate_source == 'default'` 3. **Certificate Validation**: Runs `certificate_checks` role for all sources 4. **Service Configuration**: Passes certificate paths to dependent roles 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/metadata.obsah.yaml b/src/playbooks/migrate/metadata.obsah.yaml index b937f954e..d88fe9c6a 100644 --- a/src/playbooks/migrate/metadata.obsah.yaml +++ b/src/playbooks/migrate/metadata.obsah.yaml @@ -1,15 +1,29 @@ --- help: | - Migrate foreman-installer answer file to foremanctl configuration format. + Migrate from a foreman-installer deployment to foremanctl. - This command reads foreman-installer answer files and converts them to the - new foremanctl configuration format. Unmappable parameters are reported - as warnings but do not cause the command to fail. + By default, previews the migration without making any changes. + Use --apply to perform the actual migration. + + Without --apply, shows: + - Mapped answer file parameters and their new values + - Unmappable parameters that need manual review + - Certificate state detected on the system + + With --apply, performs: + - Writes migrated parameters to the foremanctl configuration + - Normalizes installer certificates to the foremanctl layout + - Backs up the original /root/ssl-build/ directory variables: + migrate_apply: + help: Perform the migration. Without this flag, only previews what would happen. + parameter: --apply + action: store_true + persist: false answer_file: help: Path to the foreman-installer answer file to migrate. If not specified, attempts to read from the default location. persist: false output: - help: Path where the migrated configuration should be written. If not specified, outputs to stdout. + help: Path where the migrated configuration should be written. If not specified and --apply is used, writes to the foremanctl configuration. persist: false diff --git a/src/playbooks/migrate/migrate.yaml b/src/playbooks/migrate/migrate.yaml index 6fa2ec577..8518f92b9 100644 --- a/src/playbooks/migrate/migrate.yaml +++ b/src/playbooks/migrate/migrate.yaml @@ -1,29 +1,49 @@ --- -- name: Migrate foreman-installer answer file to foremanctl format +- name: Migrate from foreman-installer to foremanctl hosts: - quadlet - gather_facts: false + gather_facts: true + become: true + vars_files: + - "../../vars/certificates.yml" + roles: + - role: migrate_foreman_installer + when: migrate_apply | default(false) tasks: - - name: Run migration + - name: Run answer file migration migrate_answers: answer_file: "{{ answer_file | default(omit) }}" - output: "{{ output | default(omit) }}" - working_directory: "{{ lookup('env', 'PWD') }}" register: migration_result - name: Display migrated configuration to stdout - when: migration_result.output_content | default('') != '' + when: output is not defined ansible.builtin.debug: - msg: "{{ migration_result.output_content }}" + msg: "{{ migration_result.mapped | to_nice_yaml }}" + + - name: Write migrated configuration to output file + when: output is defined + ansible.builtin.copy: + content: "{{ migration_result.mapped | to_nice_yaml }}" + dest: "{{ output }}" + mode: '0600' + delegate_to: localhost + become: false + + - name: Write migrated configuration to parameters file + when: migrate_apply | default(false) + ansible.builtin.copy: + content: "{{ migration_result.mapped | to_nice_yaml }}" + dest: "{{ obsah_state_path }}/parameters.yaml" + mode: '0600' + delegate_to: localhost + become: false - name: Display migration results ansible.builtin.debug: msg: - - "Migration completed successfully!" + - "{{ 'Migration completed successfully!' if (migrate_apply | default(false)) else 'Migration preview (use --apply to perform the migration):' }}" - "Mapped parameters: {{ migration_result.mapped_count }}" - "Unmappable parameters: {{ migration_result.unmappable_count }}" - "{{ _unmappable_warning if migration_result.unmappable | length > 0 else '' }}" - - "{{ _output_file_msg if migration_result.output_file is defined else '' }}" 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 }}" diff --git a/src/plugins/modules/migrate_answers.py b/src/plugins/modules/migrate_answers.py index 920538813..d420eaa08 100755 --- a/src/plugins/modules/migrate_answers.py +++ b/src/plugins/modules/migrate_answers.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -import os - import yaml from ansible.module_utils.basic import AnsibleModule @@ -26,11 +24,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 } @@ -158,27 +158,9 @@ def apply_mappings(old_config): } -def write_output(data, output_path=None, working_directory=None): - """Write migrated configuration to file or return as string.""" - yaml_content = yaml.dump(data, default_flow_style=False, sort_keys=True) - - if output_path: - if working_directory and not os.path.isabs(output_path): - absolute_path = os.path.join(working_directory, output_path) - else: - absolute_path = os.path.abspath(output_path) - with open(absolute_path, 'w') as f: - f.write(yaml_content) - return absolute_path - else: - return yaml_content - - def run_module(): module_args = dict( answer_file=dict(type='str', required=False, default=None), - output=dict(type='str', required=False, default=None), - working_directory=dict(type='str', required=False, default=None), ) result = dict( @@ -186,7 +168,7 @@ def run_module(): mapped_count=0, unmappable_count=0, unmappable=[], - output_content='', + mapped={}, ) module = AnsibleModule( @@ -207,25 +189,12 @@ def run_module(): result['mapped_count'] = len(migration_result['mapped']) result['unmappable_count'] = len(migration_result['unmappable']) result['unmappable'] = migration_result['unmappable'] + result['mapped'] = migration_result['mapped'] - # Issue warnings for unmappable parameters if migration_result['unmappable']: for param in migration_result['unmappable']: module.warn(f"Parameter '{param}' could not be mapped and will need manual review") - if not module.check_mode: - output_path = module.params.get('output') - working_directory = module.params.get('working_directory') - - if output_path: - absolute_path = write_output(migration_result['mapped'], output_path, working_directory) - result['output_file'] = absolute_path - result['changed'] = True - else: - # Output to stdout - store in result so Ansible displays it - yaml_content = write_output(migration_result['mapped'], output_path, working_directory) - result['output_content'] = yaml_content - module.exit_json(**result) except (FileNotFoundError, PermissionError, ValueError) as e: 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..7fa5dca4b --- /dev/null +++ b/src/roles/migrate_foreman_installer/defaults/main.yml @@ -0,0 +1,2 @@ +--- +migrate_foreman_installer_ca_directory: /var/lib/foremanctl/certs 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..d2258402b --- /dev/null +++ b/src/roles/migrate_foreman_installer/tasks/main.yml @@ -0,0 +1,153 @@ +--- +- 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: "{{ obsah_state_path }}/parameters.yaml" + regexp: '^certificates_source:' + line: "certificates_source: custom_server" + create: true + mode: '0600' + delegate_to: localhost + 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.copy: + dest: "{{ obsah_state_path }}/certificates-ca-password" + content: "{{ migrate_foreman_installer_installer_ca_password.content | b64decode | trim }}" + mode: '0600' + no_log: true + delegate_to: localhost + become: false + + - 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 9cd865463..000000000 --- a/src/vars/installer_certificates.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -ca_key_password: "/root/ssl-build/katello-default-ca.pwd" # noqa: no-static-secrets -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..34455644f --- /dev/null +++ b/tests/migration_test.py @@ -0,0 +1,71 @@ +import os + +import pytest +import yaml + + +@pytest.fixture(scope="module") +def obsah_state_path(): + return os.environ.get("OBSAH_STATE", "/var/lib/foremanctl") + + +@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"/var/lib/foremanctl/certs/{subdir}") + assert d.exists + assert d.is_directory + assert d.mode == 0o755 + + +def test_ca_password_file(server, migrated_environment): + f = server.file("/var/lib/foremanctl/certs/private/ca.pwd") + assert f.exists + assert f.mode == 0o600 + + +def test_ca_password_persisted(migrated_environment, obsah_state_path): + password_file = os.path.join(obsah_state_path, "certificates-ca-password") + assert os.path.exists(password_file) + assert oct(os.stat(password_file).st_mode & 0o777) == oct(0o600) + with open(password_file) as f: + assert len(f.read().strip()) > 0 + + +def test_default_certs_no_custom_source(migrated_environment, obsah_state_path): + parameters_file = os.path.join(obsah_state_path, "parameters.yaml") + assert os.path.exists(parameters_file) + with open(parameters_file) as f: + params = yaml.safe_load(f) + assert "certificates_source" not in params + + +def test_answers_migration_database_mode(migrated_environment, obsah_state_path): + parameters_file = os.path.join(obsah_state_path, "parameters.yaml") + assert os.path.exists(parameters_file) + with open(parameters_file) as f: + params = yaml.safe_load(f) + assert params.get("database_mode") == "internal" + + +def test_answers_migration_admin_username(migrated_environment, obsah_state_path): + parameters_file = os.path.join(obsah_state_path, "parameters.yaml") + assert os.path.exists(parameters_file) + with open(parameters_file) as f: + params = yaml.safe_load(f) + assert params.get("foreman_initial_admin_username") == "admin" diff --git a/tests/unit/migrate_test.py b/tests/unit/migrate_test.py index d95bec7ba..c40eaae38 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 = { @@ -167,21 +186,6 @@ def test_load_invalid_yaml(self): finally: os.unlink(temp_file) - def test_write_output_to_file(self): - """Test writing output to a file""" - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - temp_file = f.name - - try: - test_data = {'database_host': 'localhost', 'database_port': 5432} - migrate_answers.write_output(test_data, temp_file) - - with open(temp_file, 'r') as f: - result = yaml.safe_load(f) - assert result == test_data - finally: - os.unlink(temp_file) - class TestTransformations: """Test individual transformation functions"""