diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e98679216..d5db30268 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -372,6 +372,83 @@ jobs: limit-access-to-actor: true wait-timeout-minutes: 5 + foreman-proxy-content-tests: + strategy: + fail-fast: false + matrix: + certificate_source: + - default + - custom_server + runs-on: ubuntu-24.04 + name: "Tests Proxy Deployment" + 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 + with: + configure_dns: true + - name: Install Ansible + run: pip install --upgrade ansible-core + - name: Setup environment + run: ./setup-environment + - name: Start VMs + run: | + ./forge vms start --vms "quadlet proxy" + - name: Run image pull + run: | + ./foremanctl pull-images + - name: Create custom certificates + if: matrix.certificate_source == 'custom_server' + run: | + ./forge custom-certs + ./forge custom-certs --hostname proxy.example.com + - name: Deploy Katello on quadlet + run: | + ./foremanctl deploy \ + --certificate-source=${{ matrix.certificate_source }} \ + ${{ matrix.certificate_source == 'custom_server' && '--certificate-server-certificate /root/custom-certificates/certs/quadlet.example.com.crt --certificate-server-key /root/custom-certificates/private/quadlet.example.com.key --certificate-server-ca-certificate /root/custom-certificates/certs/ca.crt' || '' }} \ + --initial-admin-password=changeme \ + --tuning development \ + --add-feature foreman-proxy + - name: Remove old parameters.yaml + run: rm -f .var/lib/foremanctl/parameters.yaml + - name: Generate certificates bundle on quadlet + run: | + ./foremanctl certificate-bundle proxy.example.com \ + --certificate-source=${{ matrix.certificate_source }} \ + ${{ matrix.certificate_source == 'custom_server' && '--certificate-server-certificate /root/custom-certificates/certs/proxy.example.com.crt --certificate-server-key /root/custom-certificates/private/proxy.example.com.key' || '' }} + - name: Fetch certificates bundle from quadlet + run: | + ./forge fetch-bundle proxy.example.com + - name: Deploy content proxy + run: | + ./foremanctl deploy-proxy \ + --flavor foreman-proxy-content \ + --certificate-bundle $(pwd)/.var/lib/foremanctl/proxy.example.com.tar.gz \ + --foreman-fqdn quadlet.example.com + - name: Run tests + run: | + ./forge test --pytest-args="--server-hostname=proxy" + - name: Generate sos reports + if: ${{ always() }} + run: ./forge sos + - name: Archive sos reports + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: sosreport-proxy-content + 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() @@ -380,6 +457,7 @@ jobs: - devel-tests - upgrade - migration + - foreman-proxy-content-tests - ansible-lint - python-lint runs-on: ubuntu-latest diff --git a/Vagrantfile b/Vagrantfile index cce0a0cc1..b69ff07e2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -48,6 +48,16 @@ Vagrant.configure("2") do |config| end end + config.vm.define "proxy" do |override| + override.vm.box = "centos/stream9" + override.vm.hostname = "proxy.#{DOMAIN}" + + override.vm.provider "libvirt" do |libvirt, provider| + libvirt.memory = 4096 + libvirt.cpus = 4 + end + end + # Load user-local box definitions from boxes.yaml (gitignored) boxes_yaml = File.join(__dir__, 'boxes.yaml') if File.exist?(boxes_yaml) diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index a1485f6b6..a4214e10e 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -12,6 +12,7 @@ - "../../../src/vars/base.yaml" vars: httpd_foreman_backend: "http://localhost:3000" + pulp_register_foreman_proxy: false pre_tasks: - name: Set development postgresql databases ansible.builtin.set_fact: diff --git a/development/playbooks/fetch-bundle/fetch-bundle.yaml b/development/playbooks/fetch-bundle/fetch-bundle.yaml new file mode 100644 index 000000000..144c4f08d --- /dev/null +++ b/development/playbooks/fetch-bundle/fetch-bundle.yaml @@ -0,0 +1,14 @@ +--- +- name: Fetch generated bundle from server + hosts: + - quadlet + become: true + vars_files: + - "../../../src/vars/base.yaml" + - "../../../src/vars/certificates.yml" + tasks: + - name: Fetch bundle + ansible.builtin.fetch: + src: "{{ certificates_ca_directory }}/bundles/{{ hostname }}.tar.gz" + dest: "{{ obsah_state_path }}/{{ hostname }}.tar.gz" + flat: true diff --git a/development/playbooks/fetch-bundle/metadata.obsah.yaml b/development/playbooks/fetch-bundle/metadata.obsah.yaml new file mode 100644 index 000000000..f2d2d635c --- /dev/null +++ b/development/playbooks/fetch-bundle/metadata.obsah.yaml @@ -0,0 +1,8 @@ +--- +help: | + Fetch a certificate bundle + +variables: + hostname: + parameter: hostname + help: Hostname to fetch the certificate bundle for. diff --git a/docs/developer/deployment.md b/docs/developer/deployment.md index cffaba067..cc8f67bc2 100644 --- a/docs/developer/deployment.md +++ b/docs/developer/deployment.md @@ -1,5 +1,47 @@ # Deployment Design +## Deployment Types + +foremanctl supports two deployment types: **server** and **proxy**. Each has its own sub-command, flavor, and set of services. + +### Server + +Deploys a Foreman server. This is the primary deployment type and the default entry point. + + +```bash +./foremanctl deploy +``` + +### Proxy + +Deploys a Foreman Proxy node that connects to a Foreman server. + +Before running the proxy deployment, a certificate bundle must be generated on the Foreman server and copied to the proxy VM: + +1. On the **Foreman server**, generate a certificate bundle for the proxy hostname: + + ```bash + ./foremanctl certificate-bundle proxy.example.com + ``` + + This produces a tar archive at a path like `/var/lib/foremanctl/certs/bundles/.tar.gz`. + +2. Copy the bundle to the **proxy VM**: + + ```bash + scp /var/lib/foremanctl/certs/bundles/proxy.example.com.tar.gz root@proxy.example.com:/root/proxy.example.com.tar.gz + ``` + +3. On the **proxy VM**, run the deployment: + + ```bash + ./foremanctl deploy-proxy \ + --flavor foreman-proxy-content \ + --certificate-bundle /root/proxy.example.com.tar.gz \ + --foreman-fqdn quadlet.example.com + ``` + ## Deployment Paths ### Happy Path diff --git a/docs/developer/testing.md b/docs/developer/testing.md index bb8cdb832..c2c15dbb2 100644 --- a/docs/developer/testing.md +++ b/docs/developer/testing.md @@ -172,6 +172,40 @@ def test_ingress_http_endpoint(server): # ... ``` +### Flavor-specific tests + +Some assertions only apply to a particular deployment flavor (for example, Katello expects `candlepin` and `pulp` databases; but a content proxy does not). Put flavor specific tests under `tests/flavor//`. Tests outside that tree always run regardless of flavor. + +#### How tests are selected + +During collection, `pytest_collection_modifyitems` in `tests/conftest.py` reads the active flavor from the foremanctl parameters file (`.var/lib/foremanctl/parameters.yaml`). It defaults to `katello`. + +For each collected test: + +- If the test file is **not** under `tests/flavor/`, it is kept. +- If it **is** under `tests/flavor/`, it is kept only when it lives in `tests/flavor//`; tests in sibling flavor directories are deselected. + +So a Katello deployment runs `tests/flavor/katello/` plus all shared tests, but skips `tests/flavor/foreman-proxy-content/`. After `./foremanctl deploy-proxy --flavor foreman-proxy-content`, the opposite applies. + +This is separate from [feature guarding](#feature-guarding): feature marks skip tests at runtime based on enabled features; flavor selection happens at collection time from the deployed flavor. + +#### Layout + +``` +tests/ + postgresql_test.py # shared — always collected + flavor/ + katello/ # runs only when flavor is katello + postgresql_test.py + images_test.py + foreman-proxy-content/ # runs only when flavor is foreman-proxy-content + pulp_test.py + httpd_test.py + postgresql_test.py +``` + +Use feature marks when behavior depends on an optional add-on feature; use `tests/flavor//` when the whole deployment type differs (server vs content proxy, different services, different defaults). + ### API test The `foremanapi` fixture is an [apypie](https://github.com/Apipie/apypie) `ForemanApi` client that connects to the deployed Foreman instance(authenticated as `admin`/`changeme`). It maps directly to the Foreman REST API — each method takes a resource name that corresponds to an API endpoint: @@ -232,5 +266,6 @@ def test_dynflow_service_instances(server, instance): | Client registration or content workflows | `tests/client_test.py` | | CLI flags, playbooks, or feature management | `tests/features_test.py` or `tests/playbooks_test.py` | | A standalone script (no deployment needed) | `tests/unit/_test.py` | +| Behavior specific to a deployment flavor | `tests/flavor//_test.py` | When adding a new file, follow the conventions of the nearest existing test file. diff --git a/pytest.toml b/pytest.toml new file mode 100644 index 000000000..3ffcdc954 --- /dev/null +++ b/pytest.toml @@ -0,0 +1,2 @@ +[pytest] +addopts = ["--import-mode=importlib"] diff --git a/src/features.d/content.yaml b/src/features.d/content.yaml new file mode 100644 index 000000000..dcb5d2ce3 --- /dev/null +++ b/src/features.d/content.yaml @@ -0,0 +1,26 @@ +--- +content/rpm: + description: RPM content type for Pulp + internal: true + dependencies: + - pulp +content/deb: + description: Debian content type for Pulp + internal: true + dependencies: + - pulp +content/container: + description: Container content type for Pulp + internal: true + dependencies: + - pulp +content/ansible: + description: Ansible content type for Pulp + internal: true + dependencies: + - pulp +content/python: + description: Python content type for Pulp + internal: true + dependencies: + - pulp diff --git a/src/features.yaml b/src/features.yaml index daf451bdc..4f82adcb4 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -5,6 +5,12 @@ foreman-proxy: description: Base Foreman Proxy hammer: description: Foreman CLI +pulp: + description: Pulp content management service + internal: true +candlepin: + description: Candlepin subscription management service + internal: true katello: description: Content and Subscription Management plugin for Foreman foreman: @@ -12,6 +18,8 @@ katello: hammer: katello dependencies: - tasks + - pulp + - candlepin google: description: Google Compute Engine plugin for Foreman hammer: foreman_google diff --git a/src/filter_plugins/foremanctl.py b/src/filter_plugins/foremanctl.py index dca8b790e..238658e82 100644 --- a/src/filter_plugins/foremanctl.py +++ b/src/filter_plugins/foremanctl.py @@ -117,6 +117,11 @@ def has_feature(features, feature): or feature in get_dependencies(list(features))) +def databases_for_features(databases, enabled_features): + """Return databases whose feature gate matches enabled_features.""" + return [db for db in databases if has_feature(enabled_features, db['feature'])] + + def to_postgresql_databases(databases): return [{'name': db['database'], 'owner': db['user']} for db in databases] @@ -138,6 +143,7 @@ def filters(self): 'list_all_features': list_all_features, 'invalid_features': invalid_features, 'has_feature': has_feature, + 'databases_for_features': databases_for_features, 'to_postgresql_databases': to_postgresql_databases, 'to_postgresql_users': to_postgresql_users, } diff --git a/src/playbooks/_certificates_custom/metadata.obsah.yaml b/src/playbooks/_certificates_custom/metadata.obsah.yaml new file mode 100644 index 000000000..0b1ef0857 --- /dev/null +++ b/src/playbooks/_certificates_custom/metadata.obsah.yaml @@ -0,0 +1,26 @@ +--- +variables: + certificates_cnames: + help: Additional DNS name to include in Subject Alternative Names for certificates. Can be specified multiple times. + action: append_unique + type: FQDN + parameter: --certificate-cname + certificates_custom_server_certificate: + help: Path to a custom server certificate to use instead of the auto-generated one. + type: AbsolutePath + parameter: --certificate-server-certificate + persist: false + certificates_custom_server_key: + help: Path to the private key for the custom server certificate. + type: AbsolutePath + parameter: --certificate-server-key + persist: false + certificates_custom_server_ca_certificate: + help: Path to the CA certificate that signed the custom server certificate. + type: AbsolutePath + parameter: --certificate-server-ca-certificate + persist: false + +constraints: + required_together: + - [certificates_custom_server_certificate, certificates_custom_server_key, certificates_custom_server_ca_certificate] diff --git a/src/playbooks/_flavor_features/metadata.obsah.yaml b/src/playbooks/_flavor_features/metadata.obsah.yaml index 4a7ba405d..532a92ddf 100644 --- a/src/playbooks/_flavor_features/metadata.obsah.yaml +++ b/src/playbooks/_flavor_features/metadata.obsah.yaml @@ -1,9 +1,5 @@ --- variables: - flavor: - help: Base flavor to use in this deployment. - choices: - - katello features: parameter: --add-feature help: Additional features to enable in this deployment. diff --git a/src/playbooks/_flavors/foreman-proxy-content/metadata.obsah.yaml b/src/playbooks/_flavors/foreman-proxy-content/metadata.obsah.yaml new file mode 100644 index 000000000..073e70a34 --- /dev/null +++ b/src/playbooks/_flavors/foreman-proxy-content/metadata.obsah.yaml @@ -0,0 +1,4 @@ +--- +include: + - _pulp + - _foreman_proxy diff --git a/src/playbooks/_flavors/katello/metadata.obsah.yaml b/src/playbooks/_flavors/katello/metadata.obsah.yaml new file mode 100644 index 000000000..e96ba2d79 --- /dev/null +++ b/src/playbooks/_flavors/katello/metadata.obsah.yaml @@ -0,0 +1,30 @@ +--- +variables: + external_authentication: + help: External authentication method to use + choices: + - ipa + - ipa_with_api + external_authentication_pam_service: + help: Name of the PAM service to use for IPA authentication + pulp_import_paths: + help: Extra file path that Pulp can use for content imports. Argument may be used more than once. + action: append_unique + type: AbsolutePath + parameter: --content-import-path + pulp_export_paths: + help: Extra file path that Pulp can use for content exports. Argument may be used more than once. + action: append_unique + type: AbsolutePath + parameter: --content-export-path + +include: + - _certificate_source + - _certificate_validity + - _certificates_custom + - _database_mode + - _database_connection + - _foreman + - _foreman_proxy + - _pulp + - _tuning diff --git a/src/playbooks/_foreman/metadata.obsah.yaml b/src/playbooks/_foreman/metadata.obsah.yaml new file mode 100644 index 000000000..fb089a1d5 --- /dev/null +++ b/src/playbooks/_foreman/metadata.obsah.yaml @@ -0,0 +1,16 @@ +--- +variables: + foreman_initial_admin_username: + help: Initial username for the admin user. + parameter: --initial-admin-username + foreman_initial_admin_password: + help: Initial password for the admin user. + parameter: --initial-admin-password + foreman_initial_organization: + help: Name of an initial organization. + parameter: --initial-organization + foreman_initial_location: + help: Name of an initial location. + parameter: --initial-location + foreman_puma_workers: + help: Number of workers for Puma. diff --git a/src/playbooks/_foreman_proxy/metadata.obsah.yaml b/src/playbooks/_foreman_proxy/metadata.obsah.yaml new file mode 100644 index 000000000..c0d3da1c2 --- /dev/null +++ b/src/playbooks/_foreman_proxy/metadata.obsah.yaml @@ -0,0 +1,12 @@ +--- +variables: + foreman_proxy_bmc_ipmi_implementation: + parameter: --bmc-ipmi-implementation + help: IPMI implementation to use for BMC. + choices: + - freeipmi + - ipmitool + foreman_proxy_bmc_redfish_verify_ssl: + parameter: --bmc-redfish-verify-ssl + help: Verify SSL certificates for Redfish BMC connections. + type: Boolean diff --git a/src/playbooks/_pulp/metadata.obsah.yaml b/src/playbooks/_pulp/metadata.obsah.yaml index 6b6a4a301..6a928621c 100644 --- a/src/playbooks/_pulp/metadata.obsah.yaml +++ b/src/playbooks/_pulp/metadata.obsah.yaml @@ -1,12 +1,4 @@ --- variables: - pulp_import_paths: - help: Extra file path that Pulp can use for content imports. Argument may be used more than once. - action: append_unique - type: AbsolutePath - parameter: --content-import-path - pulp_export_paths: - help: Extra file path that Pulp can use for content exports. Argument may be used more than once. - action: append_unique - type: AbsolutePath - parameter: --content-export-path + pulp_worker_count: + help: Number of Pulp workers. Defaults to 8 or the number of CPU cores, whichever is smaller. diff --git a/src/playbooks/deploy-proxy/deploy-proxy.yaml b/src/playbooks/deploy-proxy/deploy-proxy.yaml new file mode 100644 index 000000000..6ea0967e8 --- /dev/null +++ b/src/playbooks/deploy-proxy/deploy-proxy.yaml @@ -0,0 +1,34 @@ +--- +- name: Setup proxy machine + hosts: + - proxy + become: true + vars_files: + - "../../vars/defaults.yml" + - "../../vars/flavors/{{ flavor }}.yml" + - "../../vars/certificates.yml" + - "../../vars/images.yml" + - "../../vars/tuning/{{ tuning }}.yml" + - "../../vars/database.yml" + - "../../vars/foreman.yml" + - "../../vars/base.yaml" + roles: + - role: pre_install + - role: checks + vars: + checks_databases: "{{ all_databases }}" + - role: certificates + - role: certificate_checks + vars: + certificate_checks_certificate: "{{ server_certificate }}" + certificate_checks_key: "{{ server_key }}" + certificate_checks_ca: "{{ server_ca_certificate }}" + - role: postgresql + when: + - database_mode == 'internal' + - valkey + - httpd + - pulp + - systemd_target + - foreman_proxy + - post_install diff --git a/src/playbooks/deploy-proxy/metadata.obsah.yaml b/src/playbooks/deploy-proxy/metadata.obsah.yaml new file mode 100644 index 000000000..32afe749e --- /dev/null +++ b/src/playbooks/deploy-proxy/metadata.obsah.yaml @@ -0,0 +1,27 @@ +--- +help: | + Install node + +variables: + flavor: + help: Base flavor to use in this deployment. + choices: + - foreman-proxy-content + certificates_bundle: + help: Path to the certificate bundle tar file. + type: AbsolutePath + parameter: --certificate-bundle + persist: false + foreman_name: + parameter: --foreman-fqdn + help: FQDN of the Foreman server this proxy connects to. + foreman_proxy_oauth_consumer_key: + parameter: --oauth-consumer-key + help: OAuth key to be used for communication with Foreman. + foreman_proxy_oauth_consumer_secret: + parameter: --oauth-consumer-secret + help: OAuth secret to be used for communication with Foreman. + +include: + - _flavor_features + - _flavors/foreman-proxy-content diff --git a/src/playbooks/deploy/deploy.yaml b/src/playbooks/deploy/deploy.yaml index 1454ddab1..bd3304cb7 100644 --- a/src/playbooks/deploy/deploy.yaml +++ b/src/playbooks/deploy/deploy.yaml @@ -1,5 +1,5 @@ --- -- name: Setup quadlet demo machine +- name: Setup machine hosts: - quadlet become: true @@ -18,7 +18,6 @@ vars: checks_databases: "{{ all_databases }}" - role: certificates - when: "certificates_source in ['default', 'custom_server']" - role: certificate_checks vars: certificate_checks_certificate: "{{ server_certificate }}" @@ -30,8 +29,8 @@ - valkey - candlepin - httpd - - pulp - foreman + - pulp - role: systemd_target - role: iop_core when: diff --git a/src/playbooks/deploy/metadata.obsah.yaml b/src/playbooks/deploy/metadata.obsah.yaml index 2b04775e3..a378460c1 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -3,22 +3,10 @@ help: | Install variables: - foreman_initial_admin_username: - help: Initial username for the admin user. - parameter: --initial-admin-username - foreman_initial_admin_password: - help: Initial password for the admin user. - parameter: --initial-admin-password - foreman_initial_organization: - help: Name of an initial organization. - parameter: --initial-organization - foreman_initial_location: - help: Name of an initial location. - parameter: --initial-location - foreman_puma_workers: - help: Number of workers for Puma. - pulp_worker_count: - help: Number of Pulp workers. Defaults to 8 or the number of CPU cores, whichever is smaller. + flavor: + help: Base flavor to use in this deployment. + choices: + - katello external_authentication: help: External authentication method to use choices: @@ -26,46 +14,7 @@ variables: - ipa_with_api external_authentication_pam_service: help: Name of the PAM service to use for IPA authentication - certificates_cnames: - help: Additional DNS name to include in Subject Alternative Names for certificates. Can be specified multiple times. - action: append_unique - type: FQDN - parameter: --certificate-cname - certificates_custom_server_certificate: - help: Path to a custom server certificate to use instead of the auto-generated one. - type: AbsolutePath - parameter: --certificate-server-certificate - persist: false - certificates_custom_server_key: - help: Path to the private key for the custom server certificate. - type: AbsolutePath - parameter: --certificate-server-key - persist: false - certificates_custom_server_ca_certificate: - help: Path to the CA certificate that signed the custom server certificate. - type: AbsolutePath - parameter: --certificate-server-ca-certificate - persist: false - foreman_proxy_bmc_ipmi_implementation: - parameter: --bmc-ipmi-implementation - help: IPMI implementation to use for BMC. - choices: - - freeipmi - - ipmitool - foreman_proxy_bmc_redfish_verify_ssl: - parameter: --bmc-redfish-verify-ssl - help: Verify SSL certificates for Redfish BMC connections. - type: Boolean - -constraints: - required_together: - - [certificates_custom_server_certificate, certificates_custom_server_key, certificates_custom_server_ca_certificate] include: - - _certificate_source - - _certificate_validity - - _database_mode - - _database_connection - - _tuning - _flavor_features - - _pulp + - _flavors/katello diff --git a/src/playbooks/health/health.yaml b/src/playbooks/health/health.yaml index 011aca516..23787790f 100644 --- a/src/playbooks/health/health.yaml +++ b/src/playbooks/health/health.yaml @@ -13,14 +13,7 @@ ansible.builtin.include_role: name: checks tasks_from: execute_check - loop: - - check_hostname - - check_services - - check_foreman_api - - check_database_connection - - check_foreman_tasks - - check_host_facts_count - - check_duplicate_permissions + loop: "{{ health_checks_to_execute }}" when: >- (item != 'check_foreman_tasks' or not (health_skip_check_foreman_tasks_param | default(false) | bool)) # Add additional skips here diff --git a/src/playbooks/pull-images/pull-images.yaml b/src/playbooks/pull-images/pull-images.yaml index 6b84d1a13..41baea33f 100644 --- a/src/playbooks/pull-images/pull-images.yaml +++ b/src/playbooks/pull-images/pull-images.yaml @@ -11,15 +11,28 @@ roles: - role: pre_install post_tasks: - - name: Deploy core image units + - name: Deploy Foreman image units ansible.builtin.include_role: - name: "{{ item }}" + name: foreman + tasks_from: image.yaml + when: enabled_features | has_feature('foreman') + + - name: Deploy Candlepin image units + ansible.builtin.include_role: + name: candlepin + tasks_from: image.yaml + when: enabled_features | has_feature('candlepin') + + - name: Deploy Pulp image units + ansible.builtin.include_role: + name: pulp + tasks_from: image.yaml + when: enabled_features | has_feature('pulp') + + - name: Deploy Valkey image units + ansible.builtin.include_role: + name: valkey tasks_from: image.yaml - loop: - - foreman - - candlepin - - pulp - - valkey - name: Deploy database image units ansible.builtin.include_role: @@ -31,7 +44,7 @@ ansible.builtin.include_role: name: foreman_proxy tasks_from: image.yaml - when: "'foreman-proxy' in enabled_features" + when: enabled_features | has_feature('foreman-proxy') - name: Deploy IOP image units ansible.builtin.include_role: @@ -52,7 +65,7 @@ - iop_advisor_frontend - iop_inventory_frontend - iop_vulnerability_frontend - when: "'iop' in enabled_features" + when: enabled_features | has_feature('iop') - name: Pull images ansible.builtin.include_role: diff --git a/src/requirements.txt b/src/requirements.txt index 52dd0c3e9..34c9b8e62 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,3 +1,3 @@ -obsah >= 1.8.1 +obsah >= 1.9.1 requests requests-oauthlib diff --git a/src/roles/certificate_bundle/tasks/main.yml b/src/roles/certificate_bundle/tasks/main.yml index ce3c15827..9b352ee45 100644 --- a/src/roles/certificate_bundle/tasks/main.yml +++ b/src/roles/certificate_bundle/tasks/main.yml @@ -61,7 +61,7 @@ src: "{{ certificate_bundle_server_key }}" dest: "{{ certificate_bundle_build_directory.path }}/certs/private/{{ certificate_bundle_hostname }}.key" remote_src: true - mode: '0600' + mode: '0440' - name: Copy client certificate ansible.builtin.copy: @@ -75,7 +75,7 @@ src: "{{ certificate_bundle_client_key }}" dest: "{{ certificate_bundle_build_directory.path }}/certs/private/{{ certificate_bundle_hostname }}-client.key" remote_src: true - mode: '0600' + mode: '0440' - name: Create tarball community.general.archive: diff --git a/src/roles/certificates/defaults/main.yml b/src/roles/certificates/defaults/main.yml index d460eee7e..6a79cb9bd 100644 --- a/src/roles/certificates/defaults/main.yml +++ b/src/roles/certificates/defaults/main.yml @@ -17,3 +17,4 @@ certificates_ca_validity_days: 7300 certificates_validity_days: 7300 certificates_renew: false certificates_renew_ca: false +certificates_generate: true diff --git a/src/roles/certificates/tasks/ca.yml b/src/roles/certificates/tasks/ca.yml index 145ccb1c4..418b84b35 100644 --- a/src/roles/certificates/tasks/ca.yml +++ b/src/roles/certificates/tasks/ca.yml @@ -5,7 +5,8 @@ register: _certificates_ca_cert - name: 'Generate CA' - when: not _certificates_ca_cert.stat.exists or certificates_renew_ca + when: + - (not _certificates_ca_cert.stat.exists or certificates_renew_ca) block: - name: 'Create CA key password file' ansible.builtin.copy: diff --git a/src/roles/certificates/tasks/extract.yml b/src/roles/certificates/tasks/extract.yml new file mode 100644 index 000000000..d370973c4 --- /dev/null +++ b/src/roles/certificates/tasks/extract.yml @@ -0,0 +1,18 @@ +--- +- name: Check path to certificate tar file exists + ansible.builtin.stat: + path: "{{ certificates_bundle }}" + register: _certificates_tar_file_path + delegate_to: localhost + +- name: Fail if path to certificate tar file does not exist + ansible.builtin.fail: + msg: "Path to certificate tar file not found: {{ certificates_bundle }}" + when: not _certificates_tar_file_path.stat.exists + +- name: Extract certificate bundle + ansible.builtin.unarchive: + src: "{{ certificates_bundle }}" + dest: "{{ certificates_ca_directory }}" + extra_opts: + - --strip-components=1 diff --git a/src/roles/certificates/tasks/main.yml b/src/roles/certificates/tasks/main.yml index a3a5e22ff..ac793d261 100644 --- a/src/roles/certificates/tasks/main.yml +++ b/src/roles/certificates/tasks/main.yml @@ -29,23 +29,38 @@ state: directory mode: '0755' -- name: 'Generate CA certificate' - ansible.builtin.include_tasks: ca.yml - when: certificates_ca - -- name: Issue host certificates - ansible.builtin.include_tasks: host.yml - when: certificates_hostnames is defined - with_items: "{{ certificates_hostnames }}" - loop_control: - loop_var: certificates_hostname - -- name: Create CA bundle with internal CA and custom server CA - ansible.builtin.assemble: - src: "{{ certificates_ca_directory_certs }}" - dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" - regexp: '(ca|server-ca)\.crt$' - mode: '0444' +- name: 'Generate certificates' when: - - certificates_source == 'custom_server' - - certificates_custom_server_certificate is defined + - certificates_bundle is not defined + - certificates_generate + block: + - name: 'Generate CA certificate' + ansible.builtin.include_tasks: ca.yml + when: + - certificates_ca + + - name: Issue host certificates + ansible.builtin.include_tasks: host.yml + when: + - certificates_hostnames is defined + with_items: "{{ certificates_hostnames }}" + loop_control: + loop_var: certificates_hostname + + - name: Create CA bundle with internal CA and custom server CA + ansible.builtin.assemble: + src: "{{ certificates_ca_directory_certs }}" + dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" + regexp: '(ca|server-ca)\.crt$' + mode: '0444' + when: + - certificates_source == 'custom_server' + - certificates_custom_server_certificate is defined + +- name: 'Consume certs' + when: + - certificates_bundle is defined + - not certificates_generate + block: + - name: Extract proxy certificate bundle + ansible.builtin.include_tasks: extract.yml diff --git a/src/roles/check_services/tasks/main.yaml b/src/roles/check_services/tasks/main.yaml index 3b3d995bf..ba8c0a211 100644 --- a/src/roles/check_services/tasks/main.yaml +++ b/src/roles/check_services/tasks/main.yaml @@ -1,5 +1,5 @@ --- -- name: Get all Foreman services as JSON +- name: Get all Foreman target services as JSON ansible.builtin.shell: cmd: systemctl list-units --output=json --all $(systemctl list-dependencies --plain foreman.target) register: check_services_all_json @@ -26,7 +26,7 @@ list }} -- name: Verify all Foreman services are active +- name: Verify all Foreman target services are active ansible.builtin.assert: that: - check_services_not_running_list | length == 0 diff --git a/src/roles/checks/defaults/main.yml b/src/roles/checks/defaults/main.yml index bdebaac21..9e405bb44 100644 --- a/src/roles/checks/defaults/main.yml +++ b/src/roles/checks/defaults/main.yml @@ -1,2 +1,3 @@ --- checks_databases: [] +checks_to_execute: [] diff --git a/src/roles/checks/tasks/main.yml b/src/roles/checks/tasks/main.yml index 48ae0872c..18765bfe3 100644 --- a/src/roles/checks/tasks/main.yml +++ b/src/roles/checks/tasks/main.yml @@ -1,11 +1,7 @@ --- - name: Execute checks ansible.builtin.include_tasks: execute_check.yml - loop: - - check_features - - check_hostname - - check_database_connection - - check_system_requirements + loop: "{{ checks_to_execute }}" - name: Run database index integrity checks ansible.builtin.include_role: diff --git a/src/roles/foreman/tasks/main.yaml b/src/roles/foreman/tasks/main.yaml index dd5282760..b74cc3656 100644 --- a/src/roles/foreman/tasks/main.yaml +++ b/src/roles/foreman/tasks/main.yaml @@ -325,12 +325,3 @@ register: foreman_tasks_status when: - "'katello' in foreman_status.json['results']" - -- name: Configure Foreman Proxy - theforeman.foreman.smart_proxy: - name: "{{ ansible_facts['fqdn'] }}-pulp" - url: "https://{{ ansible_facts['fqdn'] }}/pulp/api/v3/smart_proxy" - server_url: "{{ foreman_url }}" - oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" - oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" - ca_path: "{{ foreman_ca_certificate }}" diff --git a/src/roles/foreman_proxy/defaults/main.yaml b/src/roles/foreman_proxy/defaults/main.yaml index 4791507b1..abd664b17 100644 --- a/src/roles/foreman_proxy/defaults/main.yaml +++ b/src/roles/foreman_proxy/defaults/main.yaml @@ -7,8 +7,7 @@ foreman_proxy_https_port: 8443 foreman_proxy_url: "https://{{ foreman_proxy_name }}:{{ foreman_proxy_https_port }}" # Settings -foreman_proxy_trusted_hosts: - - "{{ foreman_proxy_name }}" +foreman_proxy_trusted_hosts: [] foreman_proxy_base_features: - logs @@ -22,3 +21,5 @@ foreman_proxy_foreman_server_url: "https://{{ ansible_facts['fqdn'] }}" # BMC settings foreman_proxy_bmc_ipmi_implementation: ipmitool foreman_proxy_bmc_redfish_verify_ssl: true + +foreman_proxy_with_pulp_mirror: "{{ pulp_mirror | default(false) }}" diff --git a/src/roles/foreman_proxy/templates/settings.yml.j2 b/src/roles/foreman_proxy/templates/settings.yml.j2 index e7dbe4fbb..e8dbd421f 100644 --- a/src/roles/foreman_proxy/templates/settings.yml.j2 +++ b/src/roles/foreman_proxy/templates/settings.yml.j2 @@ -1,7 +1,10 @@ :settings_directory: /etc/foreman-proxy/settings.d :foreman_url: {{ foreman_proxy_foreman_server_url }} -:trusted_hosts: {{ foreman_proxy_trusted_hosts }} +:trusted_hosts: +{% for host in foreman_proxy_trusted_hosts %} + - {{ host }} +{% endfor %} :https_port: {{ foreman_proxy_https_port }} :ssl_ca_file: /etc/foreman-proxy/ssl_ca.pem diff --git a/src/roles/httpd/defaults/main.yml b/src/roles/httpd/defaults/main.yml index 7f4f7b35a..aab561ba4 100644 --- a/src/roles/httpd/defaults/main.yml +++ b/src/roles/httpd/defaults/main.yml @@ -20,3 +20,7 @@ httpd_ipa_manage_sssd: true httpd_ipa_keytab: /etc/httpd/conf/http.keytab httpd_ipa_pam_service: "{{ external_authentication_pam_service | default('foreman') }}" httpd_ipa_gssapi_local_name: true + +httpd_with_foreman: "{{ 'foreman' in enabled_features }}" +httpd_with_pulpcore: "{{ not httpd_with_foreman and not httpd_with_pulp_mirror }}" +httpd_with_pulp_mirror: "{{ pulp_mirror | default(false) }}" diff --git a/src/roles/httpd/tasks/main.yml b/src/roles/httpd/tasks/main.yml index bbc3d633d..1c79c4d6b 100644 --- a/src/roles/httpd/tasks/main.yml +++ b/src/roles/httpd/tasks/main.yml @@ -70,24 +70,29 @@ - name: Configure external authentication ansible.builtin.include_tasks: "external_auth/{{ httpd_external_authentication }}.yml" - when: httpd_external_authentication in ['ipa', 'ipa_with_api'] + when: + - httpd_with_foreman + - httpd_external_authentication in ['ipa', 'ipa_with_api'] - name: Remove external authentication configuration if not enabled ansible.builtin.include_tasks: external_auth/cleanup.yml - when: httpd_external_authentication is none + when: + - httpd_with_foreman + - httpd_external_authentication is none -- name: Configure foreman vhost + +- name: Configure vhost ansible.builtin.template: src: foreman-vhost.conf.j2 - dest: /etc/httpd/conf.d/foreman.conf + dest: /etc/httpd/conf.d/{{ 'foreman' if httpd_with_foreman else 'pulpcore' }}.conf mode: "0644" notify: - Restart httpd -- name: Configure foreman-ssl vhost +- name: Configure SSL vhost ansible.builtin.template: src: foreman-ssl-vhost.conf.j2 - dest: /etc/httpd/conf.d/foreman-ssl.conf + dest: /etc/httpd/conf.d/{{ 'foreman-ssl' if httpd_with_foreman else 'pulpcore-ssl' }}.conf mode: "0644" notify: - Restart httpd diff --git a/src/roles/httpd/templates/foreman-ssl-vhost.conf.j2 b/src/roles/httpd/templates/foreman-ssl-vhost.conf.j2 index 923d09365..1288b2251 100644 --- a/src/roles/httpd/templates/foreman-ssl-vhost.conf.j2 +++ b/src/roles/httpd/templates/foreman-ssl-vhost.conf.j2 @@ -5,12 +5,13 @@ IncludeOptional "/etc/httpd/conf.d/05-foreman-ssl.d/*.conf" ## Logging - ErrorLog "/var/log/httpd/foreman-ssl_error_ssl.log" + ErrorLog "/var/log/httpd/{{ 'foreman-ssl_error_ssl' if httpd_with_foreman else 'pulpcore_error' }}.log" ServerSignature Off - CustomLog "/var/log/httpd/foreman-ssl_access_ssl.log" combined + CustomLog "/var/log/httpd/{{ 'foreman-ssl_access_ssl' if httpd_with_foreman else 'pulpcore_access' }}.log" combined ## Request header rules ## as per http://httpd.apache.org/docs/2.4/mod/mod_headers.html#requestheader +{% if httpd_with_foreman %} RequestHeader set X-FORWARDED-PROTO "https" RequestHeader set SSL-CLIENT-S-DN "%{SSL_CLIENT_S_DN}s" RequestHeader set SSL-CLIENT-CERT "%{SSL_CLIENT_CERT}s" @@ -33,6 +34,9 @@ RequestHeader unset REMOTE-USER_GROUPS RequestHeader unset REMOTE_USER-GROUPS RequestHeader unset REMOTE_USER_GROUPS +{% else %} + RequestHeader set X_RHSM_SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" +{% endif %} ## SSL directives SSLEngine on @@ -46,6 +50,13 @@ # SSL Proxy directives SSLProxyEngine On + SSLProxyCACertificateFile "{{ httpd_ssl_dir }}/certs/katello-default-ca.crt" +{% if not httpd_with_foreman %} + ProxyPass /katello/api/v2/repositories/ {{ httpd_foreman_url }}/katello/api/v2/repositories/ + + ProxyPassReverse {{ httpd_foreman_url }}/katello/api/v2/repositories/ + +{% endif %} ProxyPass /pulp_ansible/galaxy/ {{ httpd_pulp_api_backend }}/pulp_ansible/galaxy/ ProxyPassReverse /pulp_ansible/galaxy/ {{ httpd_pulp_api_backend }}/pulp_ansible/galaxy/ @@ -54,6 +65,9 @@ RequestHeader unset REMOTE_USER RequestHeader unset REMOTE-USER RequestHeader set REMOTE-USER "admin" "expr=%{SSL_CLIENT_S_DN_CN} == '{{ ansible_facts['fqdn'] }}'" + {% for httpd_pulp_trusted_host in httpd_pulp_trusted_hosts %} + RequestHeader set REMOTE-USER "admin" "expr=%{SSL_CLIENT_S_DN_CN} == '{{ httpd_pulp_trusted_host }}'" + {% endfor %} ProxyPass {{ httpd_pulp_api_backend }}/v2/ ProxyPassReverse {{ httpd_pulp_api_backend }}/v2/ @@ -83,6 +97,9 @@ RequestHeader unset REMOTE_USER RequestHeader unset REMOTE-USER RequestHeader set REMOTE-USER "admin" "expr=%{SSL_CLIENT_S_DN_CN} == '{{ ansible_facts['fqdn'] }}'" + {% for httpd_pulp_trusted_host in httpd_pulp_trusted_hosts %} + RequestHeader set REMOTE-USER "admin" "expr=%{SSL_CLIENT_S_DN_CN} == '{{ httpd_pulp_trusted_host }}'" + {% endfor %} ProxyPass {{ httpd_pulp_api_backend }}/pulp/api/v3 timeout=600 ProxyPassReverse {{ httpd_pulp_api_backend }}/pulp/api/v3 @@ -99,7 +116,8 @@ ## Proxy rules ProxyRequests Off - ProxyPreserveHost On + ProxyPreserveHost {{ 'On' if httpd_with_foreman else 'Off' }} +{% if httpd_with_foreman %} ProxyAddHeaders On ProxyPass /pulp ! ProxyPass /pub ! @@ -108,6 +126,16 @@ ProxyPass /server-status ! ProxyPass / {{ httpd_foreman_backend }}/ retry=0 timeout=900 upgrade=websocket ProxyPassReverse / {{ httpd_foreman_backend }}/ +{% else %} + ProxyPass /rhsm {{ httpd_foreman_url }}/rhsm disablereuse=on retry=0 + ProxyPassReverse /rhsm {{ httpd_foreman_url }}/rhsm + ProxyPass /redhat_access {{ httpd_foreman_url }}/redhat_access disablereuse=on retry=0 + ProxyPassReverse /redhat_access {{ httpd_foreman_url }}/redhat_access + ProxyPass /api/lightspeed {{ httpd_foreman_url }}/api/lightspeed disablereuse=on retry=0 + ProxyPassReverse /api/lightspeed {{ httpd_foreman_url }}/api/lightspeed + ProxyPass /api/registration_commands {{ httpd_foreman_url }}/api/registration_commands disablereuse=on retry=0 + ProxyPassReverse /api/registration_commands {{ httpd_foreman_url }}/api/registration_commands +{% endif %} AddDefaultCharset UTF-8 diff --git a/src/roles/httpd/templates/foreman-vhost.conf.j2 b/src/roles/httpd/templates/foreman-vhost.conf.j2 index 90065c018..cd0840afd 100644 --- a/src/roles/httpd/templates/foreman-vhost.conf.j2 +++ b/src/roles/httpd/templates/foreman-vhost.conf.j2 @@ -5,13 +5,14 @@ IncludeOptional "/etc/httpd/conf.d/05-foreman.d/*.conf" ## Logging - ErrorLog "/var/log/httpd/foreman_error.log" + ErrorLog "/var/log/httpd/{{ 'foreman_error' if httpd_with_foreman else 'mirror_error' }}.log" ServerSignature Off - CustomLog "/var/log/httpd/foreman_access.log" combined + CustomLog "/var/log/httpd/{{ 'foreman_access' if httpd_with_foreman else 'mirror_access' }}.log" combined ## Request header rules ## as per http://httpd.apache.org/docs/2.4/mod/mod_headers.html#requestheader RequestHeader set X-FORWARDED-PROTO "http" +{% if httpd_with_foreman %} RequestHeader set SSL-CLIENT-S-DN "" RequestHeader set SSL-CLIENT-CERT "" RequestHeader set SSL-CLIENT-VERIFY "" @@ -33,6 +34,7 @@ RequestHeader unset REMOTE-USER_GROUPS RequestHeader unset REMOTE_USER-GROUPS RequestHeader unset REMOTE_USER_GROUPS +{% endif %} RequestHeader unset X-CLIENT-CERT @@ -50,7 +52,7 @@ ProxyPassReverse {{ httpd_pulp_content_backend }}/pulp/content - Alias /pub /var/www/html/pub + Alias /pub {{ httpd_pub_dir }} Options +FollowSymLinks +Indexes @@ -59,6 +61,7 @@ ## Proxy rules ProxyRequests Off +{% if httpd_with_foreman %} ProxyPreserveHost On ProxyAddHeaders On ProxyPass /pulp ! @@ -68,6 +71,7 @@ ProxyPass /server-status ! ProxyPass / {{ httpd_foreman_backend }}/ retry=0 timeout=900 upgrade=websocket ProxyPassReverse / {{ httpd_foreman_backend }}/ +{% endif %} AddDefaultCharset UTF-8 diff --git a/src/roles/post_install/tasks/foreman-proxy-content.yaml b/src/roles/post_install/tasks/foreman-proxy-content.yaml new file mode 100644 index 000000000..45685822f --- /dev/null +++ b/src/roles/post_install/tasks/foreman-proxy-content.yaml @@ -0,0 +1,8 @@ +--- +- name: Mark installation as complete + ansible.builtin.copy: + dest: "{{ post_install_done_flag }}" + content: '' + mode: '0640' + delegate_to: localhost + become: false diff --git a/src/roles/post_install/tasks/katello.yaml b/src/roles/post_install/tasks/katello.yaml new file mode 100644 index 000000000..6327e77ac --- /dev/null +++ b/src/roles/post_install/tasks/katello.yaml @@ -0,0 +1,16 @@ +--- +- name: Admin credentials + ansible.builtin.debug: + msg: + - "{{ _post_install_url_msg }}" + - "{{ _post_install_cred_msg if (post_install_done_flag is not exists) else '' }}" + vars: + _post_install_url_msg: "Foreman is running at {{ foreman_url }}" + _post_install_cred_msg: "Admin credentials: {{ foreman_initial_admin_username }}:{{ foreman_initial_admin_password }}" +- name: Mark installation as complete + ansible.builtin.copy: + dest: "{{ post_install_done_flag }}" + content: '' + mode: '0640' + delegate_to: localhost + become: false diff --git a/src/roles/post_install/tasks/main.yaml b/src/roles/post_install/tasks/main.yaml index 6327e77ac..e8b049b5e 100644 --- a/src/roles/post_install/tasks/main.yaml +++ b/src/roles/post_install/tasks/main.yaml @@ -1,16 +1,3 @@ --- -- name: Admin credentials - ansible.builtin.debug: - msg: - - "{{ _post_install_url_msg }}" - - "{{ _post_install_cred_msg if (post_install_done_flag is not exists) else '' }}" - vars: - _post_install_url_msg: "Foreman is running at {{ foreman_url }}" - _post_install_cred_msg: "Admin credentials: {{ foreman_initial_admin_username }}:{{ foreman_initial_admin_password }}" -- name: Mark installation as complete - ansible.builtin.copy: - dest: "{{ post_install_done_flag }}" - content: '' - mode: '0640' - delegate_to: localhost - become: false +- name: Run post install tasks + ansible.builtin.include_tasks: "{{ flavor }}.yaml" diff --git a/src/roles/pulp/defaults/main.yaml b/src/roles/pulp/defaults/main.yaml index f27b74d83..70931702f 100644 --- a/src/roles/pulp/defaults/main.yaml +++ b/src/roles/pulp/defaults/main.yaml @@ -24,9 +24,12 @@ pulp_content_origin: "http://{{ ansible_facts['fqdn'] }}:24816" pulp_pulp_url: "http://{{ ansible_facts['fqdn'] }}:24817" pulp_enable_analytics: false +pulp_mirror: false +pulp_register_foreman_proxy: true +pulp_rhsm_url: "https://{{ ansible_facts['fqdn'] }}/rhsm" -pulp_default_import_paths: ["/var/lib/pulp/sync_imports", "/var/lib/pulp/imports"] -pulp_default_export_paths: ["/var/lib/pulp/exports"] +pulp_default_import_paths: "{{ pulp_mirror | ternary([], ['/var/lib/pulp/sync_imports', '/var/lib/pulp/imports']) }}" +pulp_default_export_paths: "{{ pulp_mirror | ternary([], ['/var/lib/pulp/exports']) }}" pulp_import_paths: [] pulp_export_paths: [] @@ -70,6 +73,8 @@ pulp_settings_other_env: PULP_REST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSES: >- ['rest_framework.authentication.SessionAuthentication', 'pulpcore.app.authentication.PulpRemoteUserAuthentication'] PULP_SMART_PROXY_PULP_URL: "{{ pulp_pulp_url }}" + PULP_SMART_PROXY_MIRROR: "{{ pulp_mirror }}" + PULP_SMART_PROXY_RHSM_URL: "{{ pulp_rhsm_url }}" PULP_API_WORKERS: "{{ pulp_api_service_worker_count }}" PULP_CONTENT_WORKERS: "{{ pulp_content_service_worker_count }}" PULP_TOKEN_AUTH_DISABLED: "true" diff --git a/src/roles/pulp/tasks/main.yaml b/src/roles/pulp/tasks/main.yaml index a83a6f17c..c39e955fa 100644 --- a/src/roles/pulp/tasks/main.yaml +++ b/src/roles/pulp/tasks/main.yaml @@ -25,6 +25,7 @@ state: directory mode: "0755" loop: "{{ pulp_default_import_paths + pulp_import_paths }}" + when: not pulp_mirror - name: Create export directories ansible.builtin.file: @@ -32,6 +33,7 @@ state: directory mode: "0755" loop: "{{ pulp_default_export_paths + pulp_export_paths }}" + when: not pulp_mirror - name: Create DB password secret containers.podman.podman_secret: @@ -311,3 +313,13 @@ when: - pulp_existing_workers | length > 0 - (item | regex_replace('^' + pulp_worker_container_name + '@(\\d+)\\.service$', '\\1') | int) > (pulp_worker_count | int) + +- name: Configure Foreman Proxy + theforeman.foreman.smart_proxy: + name: "{{ ansible_facts['fqdn'] }}-pulp" + url: "https://{{ ansible_facts['fqdn'] }}/pulp/api/v3/smart_proxy" + server_url: "{{ pulp_foreman_url }}" + oauth1_consumer_key: "{{ pulp_foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ pulp_foreman_oauth_consumer_secret }}" + ca_path: "{{ pulp_foreman_ca_certificate }}" + when: pulp_register_foreman_proxy diff --git a/src/vars/base.yaml b/src/vars/base.yaml index de4d8d610..ed3023aa9 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -28,11 +28,19 @@ httpd_client_ca_certificate: "{{ client_ca_certificate }}" httpd_server_certificate: "{{ server_certificate }}" httpd_server_key: "{{ server_key }}" httpd_enabled_pulp_snippets: "{{ ['pypi'] if 'pulp_python' in pulp_plugins else [] }}" +httpd_foreman_url: "{{ foreman_url }}" +httpd_pulp_trusted_hosts: + - "{{ foreman_name }}" pulp_storage_path: /var/lib/pulp pulp_content_origin: "https://{{ ansible_facts['fqdn'] }}" pulp_pulp_url: "https://{{ ansible_facts['fqdn'] }}" pulp_plugins: "{{ enabled_features | select('contains', 'content/') | map('replace', 'content/', 'pulp_') | list }}" +pulp_rhsm_url: "{{ foreman_proxy_foreman_server_url }}/rhsm" +pulp_foreman_url: "{{ foreman_proxy_foreman_server_url }}" +pulp_foreman_ca_certificate: "{{ foreman_ca_certificate }}" +pulp_foreman_oauth_consumer_key: "{{ foreman_proxy_oauth_consumer_key }}" +pulp_foreman_oauth_consumer_secret: "{{ foreman_proxy_oauth_consumer_secret }}" hammer_ca_certificate: "{{ server_ca_certificate }}" hammer_plugins: "{{ enabled_features | features_to_hammer_plugins }}" @@ -43,6 +51,8 @@ foreman_proxy_foreman_server_url: "{{ foreman_url }}" foreman_proxy_foreman_ca_certificate: "{{ foreman_ca_certificate }}" foreman_proxy_oauth_consumer_key: "{{ foreman_oauth_consumer_key }}" foreman_proxy_oauth_consumer_secret: "{{ foreman_oauth_consumer_secret }}" +foreman_proxy_trusted_hosts: + - "{{ foreman_name }}" iop_core_foreman_url: "{{ foreman_url }}" iop_core_foreman_oauth_consumer_key: "{{ foreman_oauth_consumer_key }}" diff --git a/src/vars/database.yml b/src/vars/database.yml index e788fba22..7efcfb7d8 100644 --- a/src/vars/database.yml +++ b/src/vars/database.yml @@ -91,7 +91,7 @@ databases: password: "{{ candlepin_database_password }}" ssl_mode: "{{ candlepin_database_ssl_mode }}" ssl_ca: "{{ candlepin_database_ssl_ca }}" - feature: katello + feature: candlepin - name: pulp database: "{{ pulp_database_name }}" host: "{{ pulp_database_host }}" @@ -100,7 +100,7 @@ databases: password: "{{ pulp_database_password }}" ssl_mode: "{{ pulp_database_ssl_mode }}" ssl_ca: "{{ pulp_database_ssl_ca }}" - feature: katello + feature: pulp - name: iop_advisor database: "{{ iop_advisor_database_name }}" host: "{{ iop_advisor_database_host }}" @@ -138,7 +138,7 @@ databases: feature: iop all_databases: >- - {{ databases | selectattr('feature', 'in', enabled_features) | list }} + {{ databases | databases_for_features(enabled_features) }} postgresql_databases: >- {{ all_databases | to_postgresql_databases }} diff --git a/src/vars/flavors/foreman-proxy-content.yml b/src/vars/flavors/foreman-proxy-content.yml new file mode 100644 index 000000000..36668f4cc --- /dev/null +++ b/src/vars/flavors/foreman-proxy-content.yml @@ -0,0 +1,21 @@ +flavor_features: + - foreman-proxy + - content/rpm + - content/deb + - content/container + - content/ansible + - content/python + +pulp_mirror: true + +checks_to_execute: + - check_features + - check_hostname + - check_database_connection + +health_checks_to_execute: + - check_hostname + - check_database_connection + - check_services + +certificates_generate: false diff --git a/src/vars/flavors/katello.yml b/src/vars/flavors/katello.yml index ca5cd86d0..cdc309169 100644 --- a/src/vars/flavors/katello.yml +++ b/src/vars/flavors/katello.yml @@ -7,3 +7,18 @@ flavor_features: - content/deb - content/python - content/rpm + +checks_to_execute: + - check_features + - check_hostname + - check_database_connection + - check_system_requirements + +health_checks_to_execute: + - check_hostname + - check_database_connection + - check_services + - check_foreman_api + - check_foreman_tasks + - check_host_facts_count + - check_duplicate_permissions diff --git a/tests/candlepin_test.py b/tests/candlepin_test.py index a61b4f920..5d6b3c8e6 100644 --- a/tests/candlepin_test.py +++ b/tests/candlepin_test.py @@ -1,3 +1,8 @@ +import pytest + +pytestmark = pytest.mark.feature('katello') + + def assert_secret_content(server, secret_name, secret_value): secret = server.run(f'podman secret inspect --format {"{{.SecretData}}"} --showsecret {secret_name}') assert secret.succeeded diff --git a/tests/certificate_bundle_test.py b/tests/certificate_bundle_test.py index c6368b1b3..d4d56517d 100644 --- a/tests/certificate_bundle_test.py +++ b/tests/certificate_bundle_test.py @@ -2,6 +2,9 @@ import pytest +pytestmark = pytest.mark.feature('katello') + + HOSTNAME = 'proxy.example.com' TARBALL = f'/var/lib/foremanctl/certs/bundles/{HOSTNAME}.tar.gz' diff --git a/tests/certificates_test.py b/tests/certificates_test.py index e65a813b5..e331e68fe 100644 --- a/tests/certificates_test.py +++ b/tests/certificates_test.py @@ -11,6 +11,8 @@ def certificate_info(server, certificate): @pytest.mark.parametrize("certificate_type", ['ca_certificate', 'server_ca_certificate', 'server_certificate', 'client_certificate', 'localhost_certificate']) def test_certificate_expiry(server, certificates, certificate_type): + if certificate_type == 'localhost_certificate' and not server.file(certificates[certificate_type]).exists: + pytest.skip("localhost certificate not present in proxy deployment") openssl_data = certificate_info(server, certificates[certificate_type]) not_after = dateutil.parser.parse(openssl_data['notAfter']) now = datetime.datetime.now(tz=not_after.tzinfo) @@ -78,6 +80,8 @@ def test_client_certificate_chain_verifies(server, certificates): def test_localhost_certificate_issued_by_internal_ca(server, certificates, custom_certificates): + if not server.file(certificates['localhost_certificate']).exists: + pytest.skip("localhost certificate not present in proxy deployment") localhost_info = certificate_info(server, certificates['localhost_certificate']) ca_info = certificate_info(server, certificates['ca_certificate']) assert localhost_info['issuer'] == ca_info['subject'], \ diff --git a/tests/client_test.py b/tests/client_test.py index 1bcef15c0..6116ac8b6 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -1,3 +1,8 @@ +import pytest + +pytestmark = pytest.mark.feature('katello') + + def test_foreman_content_view(client_environment, activation_key, organization, foremanapi, client): client.run('dnf install -y subscription-manager') rcmd = foremanapi.create('registration_commands', {'organization_id': organization['id'], 'insecure': True, 'activation_keys': [activation_key['name']], 'force': True}) diff --git a/tests/conftest.py b/tests/conftest.py index b6f954aa1..c425dabf5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,8 @@ SSH_CONFIG = './.tmp/ssh-config' OBSAH_STATE = os.environ.get('OBSAH_STATE', '.var/lib/foremanctl') PARAMETERS_FILE = os.path.join(OBSAH_STATE, 'parameters.yaml') +FLAVOR_TESTS_DIR = py.path.local(__file__).dirpath() / 'flavor' +FOREMAN_PROXY_PORT = 8443 class UserParameters: @@ -44,19 +46,34 @@ def enabled_features(self): return set(feature for feature, status, _desc in self.features if status == 'enabled') +def pytest_addoption(parser): + parser.addoption("--server-hostname", action="store", default="quadlet", help="Hostname of the server VM to test against") + + +def flavor(): + with open(PARAMETERS_FILE) as f: + params = yaml.safe_load(f) + return params.get('flavor', 'katello') + + @pytest.fixture(scope="module") def enabled_features(pytestconfig): return pytestconfig.user_parameters.enabled_features +@pytest.fixture(scope="module") +def available_features(pytestconfig): + return pytestconfig.user_parameters.available_features + + @pytest.fixture(scope="module") def fixture_dir(): return py.path.local(__file__).realpath() / '..' / 'fixtures' @pytest.fixture(scope="module") -def server_hostname(): - return 'quadlet' +def server_hostname(pytestconfig): + return pytestconfig.getoption("server_hostname") @pytest.fixture(scope="module") @@ -247,6 +264,25 @@ def pytest_configure(config): config.user_parameters = UserParameters(config) +def pytest_collection_modifyitems(config, items): + active_flavor = flavor() + active_flavor_dir = FLAVOR_TESTS_DIR / active_flavor + + deselected = [] + selected = [] + for item in items: + test_path = py.path.local(item.fspath) + if test_path.relto(FLAVOR_TESTS_DIR): + if not test_path.relto(active_flavor_dir): + deselected.append(item) + continue + selected.append(item) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = selected + + def pytest_runtest_setup(item): feature_markers = set(mark.args[0] for mark in item.iter_markers(name="feature")) if feature_markers: @@ -280,3 +316,31 @@ def local_request(ssh_config, server_fqdn): session.mount(f"http://{server_fqdn}", adapter) session.mount(f"https://{server_fqdn}", adapter) return session + + +@pytest.fixture(scope="module") +def proxy_base_url(server_fqdn): + return f"https://{server_fqdn}:{FOREMAN_PROXY_PORT}" + + +@pytest.fixture(scope="module") +def curl_request(server, certificates, server_fqdn): + def _request(path, base_url=None, method=None, data=None, headers=None, return_body=False): + url = f"{base_url or f'https://{server_fqdn}'}/{path}" + curl_opts = ( + f"--cacert {certificates['server_ca_certificate']} " + f"--cert {certificates['client_certificate']} " + f"--key {certificates['client_key']} " + f"--silent " + ) + if not return_body: + curl_opts += "--write-out '%{http_code}' --output /dev/null " + if method: + curl_opts += f"-X {method} " + if data: + curl_opts += f"-d '{data}' " + if headers: + for key, value in headers.items(): + curl_opts += f"--header '{key}: {value}' " + return server.run(f"curl {curl_opts}{url}") + return _request diff --git a/tests/features_test.py b/tests/features_test.py index 70cb23935..0063aa179 100644 --- a/tests/features_test.py +++ b/tests/features_test.py @@ -1,7 +1,7 @@ import subprocess -def test_foremanctl_features(): +def test_foremanctl_features(available_features): command = ['./foremanctl', 'features'] result = subprocess.run(command, capture_output=True, text=True) @@ -10,18 +10,18 @@ def test_foremanctl_features(): for noise in ['PLAY [', 'TASK [', 'ok:', 'changed:', 'PLAY RECAP']: assert noise not in result.stdout, f"Ansible output not suppressed: found '{noise}'" - for feature in ['foreman', 'foreman-proxy', 'azure-rm']: + for feature in available_features: assert feature in result.stdout, f"Expected feature '{feature}' in output" -def test_foremanctl_features_list_enabled(): +def test_foremanctl_features_list_enabled(enabled_features): command = ['./foremanctl', 'features', '--list-enabled'] result = subprocess.run(command, capture_output=True, text=True) assert result.returncode == 0 - assert 'enabled' in result.stdout - assert 'available' not in result.stdout + for feature in enabled_features: + assert feature in result.stdout, f"Expected feature '{feature}' in output" def test_invalid_feature_rejected(): diff --git a/tests/flavor/__init__.py b/tests/flavor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/flavor/foreman-proxy-content/__init__.py b/tests/flavor/foreman-proxy-content/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/flavor/foreman-proxy-content/httpd_test.py b/tests/flavor/foreman-proxy-content/httpd_test.py new file mode 100644 index 000000000..a545a0ff2 --- /dev/null +++ b/tests/flavor/foreman-proxy-content/httpd_test.py @@ -0,0 +1,31 @@ +CURL_CMD = "curl --silent --output /dev/null" + + +def test_pulpcore_vhost_exists(server): + conf = server.file("/etc/httpd/conf.d/pulpcore.conf") + assert conf.exists + assert conf.is_file + + +def test_https_pulp_api_with_client_cert(curl_request): + cmd = curl_request("pulp/api/v3/smart_proxy/v2/features") + assert cmd.succeeded + assert cmd.stdout == '200' + + +def test_https_rhsm_proxy(curl_request): + cmd = curl_request("rhsm") + assert cmd.succeeded + assert cmd.stdout not in ('404', '502', '503') + + +def test_https_pulp_content_proxy(curl_request): + cmd = curl_request("pulp/content/") + assert cmd.succeeded + assert cmd.stdout == '200' + + +def test_https_pulpcore_registry(curl_request): + cmd = curl_request("pulpcore_registry/v2/") + assert cmd.succeeded + assert cmd.stdout not in ('404', '502', '503') diff --git a/tests/flavor/foreman-proxy-content/postgresql_test.py b/tests/flavor/foreman-proxy-content/postgresql_test.py new file mode 100644 index 000000000..d7bff4c93 --- /dev/null +++ b/tests/flavor/foreman-proxy-content/postgresql_test.py @@ -0,0 +1,13 @@ + +def test_postgresql_databases(database): + result = database.run("podman exec postgresql psql -U postgres -c '\\l'") + assert "pulp" in result.stdout + assert "foreman" not in result.stdout + assert "candlepin" not in result.stdout + + +def test_postgresql_users(database): + result = database.run("podman exec postgresql psql -U postgres -c '\\du'") + assert "pulp" in result.stdout + assert "foreman" not in result.stdout + assert "candlepin" not in result.stdout diff --git a/tests/flavor/foreman-proxy-content/pulp_test.py b/tests/flavor/foreman-proxy-content/pulp_test.py new file mode 100644 index 000000000..240e08568 --- /dev/null +++ b/tests/flavor/foreman-proxy-content/pulp_test.py @@ -0,0 +1,52 @@ +import json + +import pytest + + +@pytest.fixture(scope="module") +def pulp_smart_proxy_features(curl_request): + cmd = curl_request("pulp/api/v3/smart_proxy/v2/features", return_body=True) + assert cmd.succeeded, f"Failed to query smart_proxy features: {cmd.stderr}" + return json.loads(cmd.stdout) + + +@pytest.fixture(scope="module") +def pulp_settings(server): + py = ( + 'from django.conf import settings; import json; ' + 'print(json.dumps({"import": list(settings.ALLOWED_IMPORT_PATHS), ' + '"export": list(settings.ALLOWED_EXPORT_PATHS)}))' + ) + result = server.run(f"podman exec pulp-api pulpcore-manager shell -c '{py}'") + assert result.succeeded, f"Failed to read Pulp settings: {result.stderr}" + return json.loads(result.stdout) + + +def test_import_paths_restricted(pulp_settings): + assert [] == pulp_settings['import'] + assert '/var/lib/pulp/imports' not in pulp_settings['import'] + + +def test_no_imports_or_exports_directories(server): + assert not server.file("/var/lib/pulp/exports").exists + assert not server.file("/var/lib/pulp/imports").exists + + +def test_pulp_smart_proxy_mirror_mode(pulp_smart_proxy_features): + settings = pulp_smart_proxy_features['pulpcore'].get('settings', {}) + assert settings.get('mirror') is True + assert 'client_certificate' in settings.get('client_authentication', []) + + +def test_pulp_smart_proxy_features(pulp_smart_proxy_features): + features = pulp_smart_proxy_features + assert 'pulpcore' in features + capabilities = features['pulpcore'].get('capabilities', []) + for expected in ('core', 'smart_proxy', 'rpm', 'deb', 'ansible', 'python', 'container', 'file', 'certguard'): + assert expected in capabilities, f"Missing capability: {expected}" + + +def test_pulp_api_status(curl_request): + cmd = curl_request("pulp/api/v3/status/") + assert cmd.succeeded + assert cmd.stdout == '200' diff --git a/tests/flavor/katello/__init__.py b/tests/flavor/katello/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/flavor/katello/images_test.py b/tests/flavor/katello/images_test.py new file mode 100644 index 000000000..c111eeafe --- /dev/null +++ b/tests/flavor/katello/images_test.py @@ -0,0 +1,32 @@ +import pytest + +FLAVOR_CORE_IMAGES = [ + "pulp", + "candlepin", + "foreman" +] + + +@pytest.fixture(params=FLAVOR_CORE_IMAGES) +def core_image(request): + return request.param + + +def test_image_file_exists(server, core_image): + image_file = server.file(f"/etc/containers/systemd/{core_image}.image") + assert image_file.exists and image_file.is_file + + +def test_image_dropin_directory_exists(server, core_image): + dropin_dir = server.file(f"/etc/containers/systemd/{core_image}.image.d") + assert dropin_dir.exists and dropin_dir.is_directory + + +def test_image_service_exists(server, core_image): + service = server.service(f"{core_image}-image") + assert service.exists + + +def test_image_registry_auth_file(server, core_image): + f = server.file(f"/etc/containers/systemd/{core_image}.image") + assert "REGISTRY_AUTH_FILE" in f.content_string diff --git a/tests/flavor/katello/postgresql_test.py b/tests/flavor/katello/postgresql_test.py new file mode 100644 index 000000000..bab61dbdf --- /dev/null +++ b/tests/flavor/katello/postgresql_test.py @@ -0,0 +1,13 @@ + +def test_postgresql_databases(database): + result = database.run("podman exec postgresql psql -U postgres -c '\\l'") + assert "foreman" in result.stdout + assert "candlepin" in result.stdout + assert "pulp" in result.stdout + + +def test_postgresql_users(database): + result = database.run("podman exec postgresql psql -U postgres -c '\\du'") + assert "pulp" in result.stdout + assert "foreman" in result.stdout + assert "candlepin" in result.stdout diff --git a/tests/foreman_api_test.py b/tests/foreman_api_test.py index 4429dbbf3..d2b784447 100644 --- a/tests/foreman_api_test.py +++ b/tests/foreman_api_test.py @@ -1,54 +1,10 @@ -def test_foreman_organization(organization): - assert organization - - -def test_foreman_product(product): - assert product - - -def test_foreman_yum_repository(yum_repository, foremanapi, local_request): - assert yum_repository - foremanapi.resource_action('repositories', 'sync', {'id': yum_repository['id']}) - repo_url = yum_repository['full_path'] - assert local_request.get(f'{repo_url}/repodata/repomd.xml', verify=False) - assert local_request.get(f'{repo_url}/Packages/b/bear-4.1-1.noarch.rpm', verify=False) - - -def test_foreman_file_repository(file_repository, foremanapi, local_request): - assert file_repository - foremanapi.resource_action('repositories', 'sync', {'id': file_repository['id']}) - repo_url = file_repository['full_path'] - assert local_request.get(f'{repo_url}/1.iso', verify=False) +import pytest +pytestmark = pytest.mark.feature('foreman') -def test_foreman_container_repository(container_repository, foremanapi): - assert container_repository - foremanapi.resource_action('repositories', 'sync', {'id': container_repository['id']}) - -def test_foreman_lifecycle_environment(lifecycle_environment): - assert lifecycle_environment - - -def test_foreman_content_view(content_view, yum_repository, foremanapi): - assert content_view - foremanapi.update('content_views', {'id': content_view['id'], 'repository_ids': [yum_repository['id']]}) - foremanapi.resource_action('content_views', 'publish', {'id': content_view['id']}) - # do something with the published view - versions = foremanapi.list('content_view_versions', params={'content_view_id': content_view['id']}) - for version in versions: - current_environment_ids = {environment['id'] for environment in version['environments']} - for environment_id in current_environment_ids: - foremanapi.resource_action('content_views', 'remove_from_environment', params={'id': content_view['id'], 'environment_id': environment_id}) - foremanapi.delete('content_view_versions', version) - - -def test_foreman_manifest(organization, foremanapi, fixture_dir): - manifest_path = fixture_dir / 'manifest.zip' - with open(manifest_path, 'rb') as manifest_file: - files = {'content': (str(manifest_path), manifest_file, 'application/zip')} - params = {'organization_id': organization['id']} - foremanapi.resource_action('subscriptions', 'upload', params, files=files) +def test_foreman_organization(organization): + assert organization def test_foreman_initial_organization(foremanapi): diff --git a/tests/foreman_compute_resources_test.py b/tests/foreman_compute_resources_test.py index 6ed61b1de..db43d9985 100644 --- a/tests/foreman_compute_resources_test.py +++ b/tests/foreman_compute_resources_test.py @@ -1,5 +1,7 @@ import pytest +pytestmark = pytest.mark.feature('foreman') + @pytest.mark.parametrize("compute_resource", ['AzureRm', 'EC2', 'GCE', 'Libvirt', 'Openstack', 'Vmware']) def test_foreman_compute_resources(foremanapi, compute_resource): diff --git a/tests/foreman_plugins_test.py b/tests/foreman_plugins_test.py index b2c787ccb..bab7d595f 100644 --- a/tests/foreman_plugins_test.py +++ b/tests/foreman_plugins_test.py @@ -1,5 +1,7 @@ import pytest +pytestmark = pytest.mark.feature('foreman') + @pytest.mark.parametrize("foreman_plugin", ['foreman_azure_rm', 'foreman_google']) def test_foreman_compute_resources(foremanapi, foreman_plugin): diff --git a/tests/foreman_proxy_test.py b/tests/foreman_proxy_test.py index 71ad8cbc0..1189074d0 100644 --- a/tests/foreman_proxy_test.py +++ b/tests/foreman_proxy_test.py @@ -3,28 +3,26 @@ import pytest -FOREMAN_PROXY_PORT = 8443 +from tests.conftest import FOREMAN_PROXY_PORT @pytest.fixture(scope="module") -def proxy_v2_features(server, certificates, server_fqdn): - cmd = server.run( - f"curl --cacert {certificates['server_ca_certificate']} " - f"--cert {certificates['client_certificate']} " - f"--key {certificates['client_key']} " - f"--silent https://{server_fqdn}:{FOREMAN_PROXY_PORT}/v2/features" - ) +def proxy_v2_features(curl_request, proxy_base_url): + cmd = curl_request("v2/features", base_url=proxy_base_url, return_body=True) assert cmd.succeeded, f"Failed to query /v2/features: {cmd.stderr}" return json.loads(cmd.stdout) -def test_foreman_proxy_features(server, certificates, server_fqdn, enabled_features): - cmd = server.run(f"curl --cacert {certificates['server_ca_certificate']} --silent https://{server_fqdn}:{FOREMAN_PROXY_PORT}/features") +def test_foreman_proxy_features(curl_request, proxy_base_url, enabled_features): + cmd = curl_request("features", base_url=proxy_base_url, return_body=True) assert cmd.succeeded features = json.loads(cmd.stdout) assert "logs" in features - assert "script" in features - assert "dynflow" in features + if 'remote-execution' in enabled_features: + assert "script" in features + assert "dynflow" in features + else: + assert "script" not in features if 'bmc' in enabled_features: assert "bmc" in features else: @@ -42,15 +40,13 @@ def test_foreman_proxy_port(server): @pytest.mark.xfail(reason='Fails until report feature is available') -def test_foreman_proxy_client_auth_to_foreman(server, certificates, server_fqdn): +def test_foreman_proxy_client_auth_to_foreman(curl_request): test_report = {"config_report": {"host": "test.example.com", "reported_at": datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}} - cmd = server.run( - f"curl --cacert {certificates['server_ca_certificate']} " - f"--cert {certificates['client_certificate']} " - f"--key {certificates['client_key']} " - f"--output /dev/null --write-out '%{{http_code}}' " - f"--data '{json.dumps(test_report)}' --header 'Content-Type: application/json' " - f"https://{server_fqdn}/api/v2/config_reports" + cmd = curl_request( + "api/v2/config_reports", + method="POST", + data=json.dumps(test_report), + headers={"Content-Type": "application/json"}, ) assert cmd.succeeded assert cmd.stdout == '201' diff --git a/tests/foreman_test.py b/tests/foreman_test.py index 174dd1f66..8dc443b8c 100644 --- a/tests/foreman_test.py +++ b/tests/foreman_test.py @@ -2,6 +2,8 @@ import pytest +pytestmark = pytest.mark.feature('foreman') + FOREMAN_HOST = 'localhost' FOREMAN_PORT = 3000 diff --git a/tests/hammer_test.py b/tests/hammer_test.py index 456f0ba62..6fa4a1997 100644 --- a/tests/hammer_test.py +++ b/tests/hammer_test.py @@ -1,3 +1,8 @@ +import pytest + +pytestmark = pytest.mark.feature('hammer') + + def test_hammer_ping(server): hammer = server.run("hammer ping") assert hammer.succeeded diff --git a/tests/health_test.py b/tests/health_test.py index 0087b2fcc..57ed1af53 100644 --- a/tests/health_test.py +++ b/tests/health_test.py @@ -34,6 +34,7 @@ def test_health_checks_pass(): ) +@pytest.mark.feature('foreman') def test_foreman_tasks_check_detects_errors(errored_foreman_task): """Verify foreman tasks check detects errored tasks and fails""" diff --git a/tests/httpd_test.py b/tests/httpd_test.py index c086e683c..b0ec165b3 100644 --- a/tests/httpd_test.py +++ b/tests/httpd_test.py @@ -1,3 +1,5 @@ +import pytest + HTTP_HOST = 'localhost' HTTP_PORT = 80 HTTPS_PORT = 443 @@ -21,12 +23,14 @@ def test_https_port(server): assert httpd.port(HTTPS_PORT).is_reachable -def test_http_foreman_ping(server, server_fqdn): - cmd = server.run(f"{CURL_CMD} --write-out '%{{redirect_url}}' http://{server_fqdn}/api/v2/ping") +@pytest.mark.feature('foreman') +def test_http_foreman_ping(curl_request): + cmd = curl_request("api/v2/ping") assert cmd.succeeded - assert cmd.stdout == f'https://{server_fqdn}/api/v2/ping' + assert cmd.stdout == '200' +@pytest.mark.feature('foreman') def test_https_foreman_ping(server, certificates, server_fqdn): cmd = server.run(f"{CURL_CMD} --cacert {certificates['server_ca_certificate']} --write-out '%{{http_code}}' https://{server_fqdn}/api/v2/ping") assert cmd.succeeded @@ -57,14 +61,14 @@ def test_https_pulp_content(server, certificates, server_fqdn): assert "Index of /pulp/content/" in cmd.stdout -def test_https_pulp_auth(server, certificates, server_fqdn): - cmd = server.run(f"{CURL_CMD} --cacert {certificates['server_ca_certificate']} --write-out '%{{http_code}}' --cert {certificates['client_certificate']} --key {certificates['client_key']} https://{server_fqdn}/pulp/api/v3/users/") +def test_https_pulp_auth(curl_request): + cmd = curl_request("pulp/api/v3/users/") assert cmd.succeeded assert cmd.stdout == '200' -def test_https_pypi_endpoint(server, certificates, server_fqdn): - cmd = server.run(f"curl --cacert {certificates['server_ca_certificate']} https://{server_fqdn}/pypi/test/") +def test_https_pypi_endpoint(curl_request): + cmd = curl_request("pypi/test/", return_body=True) assert cmd.succeeded # Verify route proxies to Pulp's Python plugin by checking for PythonDistribution in response # (Rails or unconfigured routes would return different errors) @@ -78,38 +82,40 @@ def test_pub_directory_exists(server): assert pub_dir.mode == 0o755 -def test_http_pub_directory_accessible(server, server_fqdn): - cmd = server.run(f"{CURL_CMD} --write-out '%{{http_code}}' http://{server_fqdn}/pub/") +def test_http_pub_directory_accessible(curl_request): + cmd = curl_request("pub/") assert cmd.succeeded assert cmd.stdout == '200' -def test_https_pub_directory_accessible(server, certificates, server_fqdn): - cmd = server.run(f"{CURL_CMD} --cacert {certificates['server_ca_certificate']} --write-out '%{{http_code}}' https://{server_fqdn}/pub/") +def test_https_pub_directory_accessible(curl_request): + cmd = curl_request("pub/") assert cmd.succeeded assert cmd.stdout == '200' -def test_http_pub_server_ca_certificate_downloadable(server, server_fqdn): - cmd = server.run(f"{CURL_CMD} --write-out '%{{http_code}}' http://{server_fqdn}/pub/katello-server-ca.crt") +def test_http_pub_server_ca_certificate_downloadable(curl_request): + cmd = curl_request("pub/katello-server-ca.crt") assert cmd.succeeded assert cmd.stdout == '200' -def test_https_pub_server_ca_certificate_downloadable(server, certificates, server_fqdn): - cmd = server.run(f"{CURL_CMD} --cacert {certificates['server_ca_certificate']} --write-out '%{{http_code}}' https://{server_fqdn}/pub/katello-server-ca.crt") +def test_https_pub_server_ca_certificate_downloadable(curl_request): + cmd = curl_request("pub/katello-server-ca.crt") assert cmd.succeeded assert cmd.stdout == '200' +@pytest.mark.feature('foreman') def test_http_foreman_login(server, server_fqdn): cmd = server.run(f"{CURL_CMD} --write-out '%{{http_code}}' http://{server_fqdn}/users/login") assert cmd.succeeded assert cmd.stdout == '301' -def test_https_foreman_login(server, certificates, server_fqdn): - cmd = server.run(f"{CURL_CMD} --cacert {certificates['server_ca_certificate']} --write-out '%{{http_code}}' https://{server_fqdn}/users/login") +@pytest.mark.feature('foreman') +def test_https_foreman_login(curl_request): + cmd = curl_request("users/login") assert cmd.succeeded assert cmd.stdout == '200' @@ -135,6 +141,7 @@ def test_httpd_config_syntax(server): assert cmd.succeeded +@pytest.mark.feature('foreman') def test_httpd_headers_use_dashes(server): cmd = server.run("grep -rPn 'RequestHeader\\s+set\\s+\\S*_\\S*\\s' /etc/httpd/conf.d/foreman.conf /etc/httpd/conf.d/foreman-ssl.conf /etc/httpd/conf.d/05-foreman.d/ /etc/httpd/conf.d/05-foreman-ssl.d/ 2>/dev/null") assert cmd.stdout.strip() == '', f"HTTP header names should use dashes, not underscores:\n{cmd.stdout}" diff --git a/tests/images_test.py b/tests/images_test.py index 52e5cea69..e128e7145 100644 --- a/tests/images_test.py +++ b/tests/images_test.py @@ -1,10 +1,8 @@ import pytest CORE_IMAGES = [ - "foreman", - "candlepin", - "pulp", "valkey", + 'pulp' ] diff --git a/tests/katello_api_test.py b/tests/katello_api_test.py new file mode 100644 index 000000000..57cd2ce1b --- /dev/null +++ b/tests/katello_api_test.py @@ -0,0 +1,52 @@ +import pytest + +pytestmark = pytest.mark.feature('katello') + + +def test_foreman_product(product): + assert product + + +def test_foreman_yum_repository(yum_repository, foremanapi, local_request): + assert yum_repository + foremanapi.resource_action('repositories', 'sync', {'id': yum_repository['id']}) + repo_url = yum_repository['full_path'] + assert local_request.get(f'{repo_url}/repodata/repomd.xml', verify=False) + assert local_request.get(f'{repo_url}/Packages/b/bear-4.1-1.noarch.rpm', verify=False) + + +def test_foreman_file_repository(file_repository, foremanapi, local_request): + assert file_repository + foremanapi.resource_action('repositories', 'sync', {'id': file_repository['id']}) + repo_url = file_repository['full_path'] + assert local_request.get(f'{repo_url}/1.iso', verify=False) + + +def test_foreman_container_repository(container_repository, foremanapi): + assert container_repository + foremanapi.resource_action('repositories', 'sync', {'id': container_repository['id']}) + + +def test_foreman_lifecycle_environment(lifecycle_environment): + assert lifecycle_environment + + +def test_foreman_content_view(content_view, yum_repository, foremanapi): + assert content_view + foremanapi.update('content_views', {'id': content_view['id'], 'repository_ids': [yum_repository['id']]}) + foremanapi.resource_action('content_views', 'publish', {'id': content_view['id']}) + # do something with the published view + versions = foremanapi.list('content_view_versions', params={'content_view_id': content_view['id']}) + for version in versions: + current_environment_ids = {environment['id'] for environment in version['environments']} + for environment_id in current_environment_ids: + foremanapi.resource_action('content_views', 'remove_from_environment', params={'id': content_view['id'], 'environment_id': environment_id}) + foremanapi.delete('content_view_versions', version) + + +def test_foreman_manifest(organization, foremanapi, fixture_dir): + manifest_path = fixture_dir / 'manifest.zip' + with open(manifest_path, 'rb') as manifest_file: + files = {'content': (str(manifest_path), manifest_file, 'application/zip')} + params = {'organization_id': organization['id']} + foremanapi.resource_action('subscriptions', 'upload', params, files=files) diff --git a/tests/postgresql_test.py b/tests/postgresql_test.py index 82187466a..55c0185b5 100644 --- a/tests/postgresql_test.py +++ b/tests/postgresql_test.py @@ -13,20 +13,6 @@ def test_postgresql_port(database): assert postgresql.port("5432").is_reachable -def test_postgresql_databases(database): - result = database.run("podman exec postgresql psql -U postgres -c '\\l'") - assert "foreman" in result.stdout - assert "candlepin" in result.stdout - assert "pulp" in result.stdout - - -def test_postgresql_users(database): - result = database.run("podman exec postgresql psql -U postgres -c '\\du'") - assert "foreman" in result.stdout - assert "candlepin" in result.stdout - assert "pulp" in result.stdout - - def test_postgresql_password_encryption(database): result = database.run("podman exec postgresql psql -U postgres -c 'SHOW password_encryption'") assert "scram-sha-256" in result.stdout @@ -35,7 +21,7 @@ def test_postgresql_password_encryption(database): reader = csv.reader(result.stdout.splitlines()) for row in reader: - assert ("SCRAM-SHA-256" in row[6]) + assert "SCRAM-SHA-256" in row[6] def test_postgresql_missing_with_external(server, database_mode): diff --git a/tests/target_lifecycle_test.py b/tests/target_lifecycle_test.py index 618f0873f..55d88cd0c 100644 --- a/tests/target_lifecycle_test.py +++ b/tests/target_lifecycle_test.py @@ -1,21 +1,16 @@ import time -FOREMAN_PING_RETRIES = 90 -FOREMAN_PING_DELAY = 10 +TARGET_ACTIVE_RETRIES = 90 +TARGET_ACTIVE_DELAY = 10 CURL_CMD = "curl --silent --output /dev/null" -def _wait_for_foreman(server, server_fqdn, certificates): - """Poll the Foreman HTTPS frontend until available or timeout.""" - for _ in range(FOREMAN_PING_RETRIES): - cmd = server.run( - f"{CURL_CMD} --cacert {certificates['server_ca_certificate']}" - f" --write-out '%{{http_code}}' https://{server_fqdn}/api/v2/ping" - ) - if cmd.stdout == '200': +def _wait_for_target_active(server, target="foreman.target"): + for _ in range(TARGET_ACTIVE_RETRIES): + if server.service(target).is_running: return - time.sleep(FOREMAN_PING_DELAY) - raise AssertionError("Foreman did not become available after target lifecycle operation") + time.sleep(TARGET_ACTIVE_DELAY) + raise AssertionError(f"{target} did not become active after lifecycle operation") def test_foreman_target_stop_start(server, server_fqdn, certificates): @@ -25,12 +20,12 @@ def test_foreman_target_stop_start(server, server_fqdn, certificates): result = server.run("systemctl start foreman.target") assert result.rc == 0, f"Failed to start foreman.target: {result.stderr}" - _wait_for_foreman(server, server_fqdn, certificates) + _wait_for_target_active(server, "foreman.target") assert server.service("foreman.target").is_running def test_foreman_target_restart(server, server_fqdn, certificates): result = server.run("systemctl restart foreman.target") assert result.rc == 0, f"Failed to restart foreman.target: {result.stderr}" - _wait_for_foreman(server, server_fqdn, certificates) + _wait_for_target_active(server, "foreman.target") assert server.service("foreman.target").is_running