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/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/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 bc9405619..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 @@ -125,13 +123,21 @@ 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 - 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,16 +180,14 @@ 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 -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,24 +196,13 @@ 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 - -**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" -``` +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. #### 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/_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/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/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/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' 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/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/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 9bf66ec3b..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") @@ -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)) 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"""