Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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|
Expand All @@ -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
10 changes: 10 additions & 0 deletions development/playbooks/remote-database/remote-database.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
- name: Setup remote database
hosts:
- database
become: true
vars_files:
- "../../../src/vars/database.yml"
roles:
- role: pre_install
- role: postgresql
2 changes: 1 addition & 1 deletion development/playbooks/vms/vms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion development/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 106 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
```
13 changes: 13 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
33 changes: 22 additions & 11 deletions tests/postgresql_test.py
Original file line number Diff line number Diff line change
@@ -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
Loading