From ca4a53f553ad9ca3b096acaa16ef83ee23ce7bc4 Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Fri, 7 Nov 2025 14:25:38 -0500 Subject: [PATCH] Add external database testing support Signed-off-by: Eric D. Helms --- .github/workflows/test.yml | 68 +++++++++++ Vagrantfile | 18 ++- .../remote-database/remote-database.yaml | 10 ++ development/playbooks/vms/vms.yaml | 2 +- development/requirements.yml | 2 +- docs/deployment.md | 107 +++++++++++++++++- tests/conftest.py | 13 +++ tests/postgresql_test.py | 33 ++++-- 8 files changed, 234 insertions(+), 19 deletions(-) create mode 100644 development/playbooks/remote-database/remote-database.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3eed81135..237dfb165 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -233,12 +233,80 @@ jobs: ## If no one connects after 5 minutes, shut down server. wait-timeout-minutes: 5 + tests-external-database: + strategy: + fail-fast: false + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - 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 database" + - name: Configure remote-database + run: | + ./forge remote-database + - name: Configure repositories + run: | + ./forge setup-repositories + - name: Run image pull + run: | + ./foremanctl pull-images + - name: Run deployment + run: | + ./foremanctl deploy --database-mode=external --database-host=database.example.com --foreman-initial-admin-password=changeme + - name: Add optional feature - hammer + run: | + ./foremanctl deploy --add-feature hammer + - name: Add optional feature - foreman-proxy + run: | + ./foremanctl deploy --add-feature foreman-proxy + - name: Run tests + run: | + ./forge test --pytest-args="--database-mode=external" + - name: Run smoker + run: | + ./forge smoker + - name: Archive smoker report + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: smoker-external-database + path: "/home/runner/smoker/report/" + - name: Generate sos reports + if: ${{ always() }} + run: ./forge sos + - name: Archive sos reports + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: sosreport-external-database + path: sos/ + - name: Setup upterm session + if: ${{ failure() }} + uses: owenthereal/action-upterm@v1 + with: + ## limits ssh access and adds the ssh public key for the user which triggered the workflow + limit-access-to-actor: true + ## If no one connects after 5 minutes, shut down server. + wait-timeout-minutes: 5 + # A dummy job that you can mark as a required check instead of each individual test test-suite: if: always() needs: - tests - devel-tests + - tests-external-database - upgrade - ansible-lint runs-on: ubuntu-latest diff --git a/Vagrantfile b/Vagrantfile index d89ea60f2..2da89c816 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -2,11 +2,15 @@ Vagrant.configure("2") do |config| config.vm.box = "centos/stream9" config.vm.synced_folder ".", "/vagrant" - config.vm.provision "ansible" do |ansible| + config.vm.provision("etc_hosts", type: 'ansible') do |ansible| ansible.playbook = "development/playbooks/etc_host.yml" ansible.compatibility_mode = "2.0" end + config.vm.provision('disk_resize', type: 'ansible') do |ansible_provisioner| + ansible_provisioner.playbook = 'development/playbooks/resize_disk.yaml' + end + config.vm.define "quadlet" do |override| override.vm.hostname = "quadlet.example.com" @@ -15,10 +19,6 @@ Vagrant.configure("2") do |config| libvirt.cpus = 4 libvirt.machine_virtual_size = 30 end - - override.vm.provision('disk_resize', type: 'ansible') do |ansible_provisioner| - ansible_provisioner.playbook = 'development/playbooks/resize_disk.yaml' - end end config.vm.define "client" do |override| @@ -28,4 +28,12 @@ Vagrant.configure("2") do |config| libvirt.memory = 1024 end end + + config.vm.define "database" do |override| + override.vm.hostname = "database.example.com" + + override.vm.provider "libvirt" do |libvirt, provider| + libvirt.memory = 2048 + end + end end diff --git a/development/playbooks/remote-database/remote-database.yaml b/development/playbooks/remote-database/remote-database.yaml new file mode 100644 index 000000000..bcb489eb5 --- /dev/null +++ b/development/playbooks/remote-database/remote-database.yaml @@ -0,0 +1,10 @@ +--- +- name: Setup remote database + hosts: + - database + become: true + vars_files: + - "../../../src/vars/database.yml" + roles: + - role: pre_install + - role: postgresql diff --git a/development/playbooks/vms/vms.yaml b/development/playbooks/vms/vms.yaml index 5cb155368..79ae47e48 100644 --- a/development/playbooks/vms/vms.yaml +++ b/development/playbooks/vms/vms.yaml @@ -9,7 +9,7 @@ block: - name: Run vagrant up ansible.builtin.command: - cmd: "vagrant up {{ vms | default() }}" + cmd: "vagrant up {{ vms | default('quadlet client') }}" args: chdir: "{{ inventory_dir }}/../" changed_when: false diff --git a/development/requirements.yml b/development/requirements.yml index e0ebc14f7..18130efb0 100644 --- a/development/requirements.yml +++ b/development/requirements.yml @@ -2,8 +2,8 @@ collections: - ansible.posix - community.crypto - - community.postgresql - community.general + - community.postgresql - name: containers.podman version: ">=1.16.4" - name: https://github.com/theforeman/forklift diff --git a/docs/deployment.md b/docs/deployment.md index 90ded4159..3efd1f8f4 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -27,7 +27,7 @@ For example, pre-pulling images to reduce the core deployment utility runtime. To allow deployments with different sets of functionality enabled, the deployment utility supports features and flavors. -- A feature is an abstract representation of "the deployed system can now do X", usually implemented by enabling a Foreman/Pulp/Hammer plugin (or a collection of these). +- A feature is an abstract representation of "the deployed system can now do X", usually implemented by enabling a Foreman/Pulp/Hammer plugin (or a collection of these). - A flavor is a set of features that are enabled by default and can not be disabled. This is to allow common deployment types like "vanilla foreman", "katello", "satellite" and similar. Additionally to the functionality offered by plugins, we define the following "base" features: @@ -108,3 +108,108 @@ The upgrade process is: 2. Stop services: `systemctl stop foreman.target` 3. Deploy updated containers: `foremanctl deploy` 4. Optional: Prune old container images: `podman image prune --all` + +## External Database Support + +The deployment utility supports connecting to an external PostgreSQL database instead of deploying a local database container. This allows for shared database infrastructure, managed database services, or dedicated database servers. + +### Prerequisites + +Before configuring external database support, ensure the following requirements are met: + +#### Database Server Requirements +- PostgreSQL server accessible from the application server +- Required databases: `foreman`, `candlepin`, and `pulp` +- Database users with appropriate permissions for database creation and table management +- Network connectivity on the configured database port (default: 5432) + +### External Database Configuration Parameters + +The external database configuration is managed entirely through `foremanctl` command line parameters: + +#### Global Database Parameters +- `--database-mode`: Set to `external` for external database deployment +- `--database-host`: Database server hostname or IP address +- `--database-port`: Database server port (default: 5432) +- `--database-ssl-mode`: SSL connection mode + - Options: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` + - Default: `disable` +- `--database-ssl-ca`: Path to SSL CA certificate file (required for `verify-ca` and `verify-full` modes) + +#### Per-Service Database Configuration +Each service (Foreman, Candlepin, Pulp) can be configured with separate database credentials: + +**Foreman Database:** +- `--foreman-database-name`: Database name (default: `foreman`) +- `--foreman-database-user`: Database user (default: `foreman`) +- `--foreman-database-password`: Database password + +**Candlepin Database:** +- `--candlepin-database-name`: Database name (default: `candlepin`) +- `--candlepin-database-user`: Database user (default: `candlepin`) +- `--candlepin-database-password`: Database password + +**Pulp Database:** +- `--pulp-database-name`: Database name (default: `pulp`) +- `--pulp-database-user`: Database user (default: `pulp`) +- `--pulp-database-password`: Database password + +### Configuration Workflow + +#### 1. Database Server Preparation + +```bash +CREATE USER "foreman" WITH PASSWORD 'Foreman_Password'; +CREATE DATABASE foreman OWNER foreman; +CREATE USER "candlepin" WITH PASSWORD 'Candlepin_Password'; +CREATE DATABASE candlepin OWNER candlepin; +CREATE USER "pulp" WITH PASSWORD 'Pulpcore_Password'; +CREATE DATABASE pulpcore OWNER pulp; +``` + +#### 2. Basic External Database Deployment + +For a basic external database setup with default settings: + +```bash +./foremanctl deploy \ + --database-mode=external \ + --database-host=database.example.com \ + --foreman-database-password=secure_foreman_password \ + --candlepin-database-password=secure_candlepin_password \ + --pulp-database-password=secure_pulp_password +``` + +#### 3. Advanced External Database Deployment + +For production deployments with custom credentials and SSL: + +```bash +./foremanctl deploy \ + --database-mode=external \ + --database-host=database.example.com \ + --database-port=5432 \ + --database-ssl-mode=require \ + --database-ssl-ca=/etc/ssl/certs/ca-cert.pem \ + --foreman-database-user=foreman_prod \ + --foreman-database-password=secure_foreman_password \ + --candlepin-database-user=candlepin_prod \ + --candlepin-database-password=secure_candlepin_password \ + --pulp-database-user=pulp_prod \ + --pulp-database-password=secure_pulp_password +``` + +#### 4. SSL-Enabled External Database + +For secure connections with certificate verification: + +```bash +./foremanctl deploy \ + --database-mode=external \ + --database-host=database.example.com \ + --database-ssl-mode=verify-full \ + --database-ssl-ca=/path/to/ca-certificate.pem + --foreman-database-password=secure_foreman_password \ + --candlepin-database-password=secure_candlepin_password \ + --pulp-database-password=secure_pulp_password +``` diff --git a/tests/conftest.py b/tests/conftest.py index 0cd91dc54..0ebfdac4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ def pytest_addoption(parser): parser.addoption("--certificate-source", action="store", default="default", choices=('default', 'installer'), help="Where to obtain certificates from") + parser.addoption("--database-mode", action="store", default="internal", choices=('internal', 'external'), help="Whether the database is internal or external") @pytest.fixture(scope="module") @@ -42,6 +43,10 @@ def certificates(pytestconfig, server_fqdn): return yaml.safe_load(template.render(context)) +@pytest.fixture(scope="module") +def database_mode(pytestconfig): + return pytestconfig.getoption("database_mode") + @pytest.fixture(scope="module") def server(server_hostname): yield testinfra.get_host(f'paramiko://{server_hostname}', sudo=True, ssh_config=SSH_CONFIG) @@ -52,6 +57,14 @@ def client(): yield testinfra.get_host('paramiko://client', sudo=True, ssh_config=SSH_CONFIG) +@pytest.fixture(scope="module") +def database(database_mode, server): + if database_mode == 'external': + yield testinfra.get_host('paramiko://database', sudo=True, ssh_config=SSH_CONFIG) + else: + yield server + + @pytest.fixture(scope="module") def ssh_config(server_hostname): config = paramiko.SSHConfig.from_path(SSH_CONFIG) diff --git a/tests/postgresql_test.py b/tests/postgresql_test.py index 1f6f485bd..fa84f6d0b 100644 --- a/tests/postgresql_test.py +++ b/tests/postgresql_test.py @@ -1,33 +1,44 @@ import csv +import pytest -def test_postgresql_service(server): - postgresql = server.service("postgresql") + +def test_postgresql_service(database): + postgresql = database.service("postgresql") assert postgresql.is_running -def test_postgresql_port(server): - postgresql = server.addr("localhost") + +def test_postgresql_port(database): + postgresql = database.addr("localhost") assert postgresql.port("5432").is_reachable -def test_postgresql_databases(server): - result = server.run("podman exec postgresql psql -U postgres -c '\\l'") +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(server): - result = server.run("podman exec postgresql psql -U postgres -c '\\du'") +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(server): - result = server.run("podman exec postgresql psql -U postgres -c 'SHOW password_encryption'") + +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 - result = server.run("echo 'COPY (select * from pg_shadow) TO STDOUT (FORMAT CSV);' | podman exec -i postgresql psql -U postgres") + result = database.run("echo 'COPY (select * from pg_shadow) TO STDOUT (FORMAT CSV);' | podman exec -i postgresql psql -U postgres") reader = csv.reader(result.stdout.splitlines()) for row in reader: assert ("SCRAM-SHA-256" in row[6]) + + +def test_postgresql_missing_with_external(server, database_mode): + if database_mode == 'internal': + pytest.skip("Test only applies if database_mode=external") + else: + assert not server.service("postgresql").exists