diff --git a/src/playbooks/backup/backup.yaml b/src/playbooks/backup/backup.yaml new file mode 100644 index 000000000..6fa4f266f --- /dev/null +++ b/src/playbooks/backup/backup.yaml @@ -0,0 +1,251 @@ +--- +- name: Backup Foreman databases and configuration + hosts: quadlet + become: true + gather_facts: true + vars_files: + - "../../vars/defaults.yml" + - "../../vars/flavors/{{ flavor }}.yml" + - "../../vars/foreman.yml" + - "../../vars/database.yml" + - "../../vars/database_iop.yml" + + vars: + backup_timestamp: "{{ ansible_date_time.iso8601_basic_short }}" + backup_dir_full: "{{ backup_dir }}/foreman-backup-{{ backup_timestamp }}" + service_stopped: false + postgresql_started: false + foreman_server_fqdn: "{{ ansible_fqdn }}" + # Timeout and retry settings + task_wait_retries: 60 + task_wait_delay: 10 + postgresql_ready_retries: 10 + postgresql_ready_delay: 2 + postgresql_stop_retries: 30 + postgresql_stop_delay: 1 + + pre_tasks: + - name: Validate unsupported parameters + ansible.builtin.assert: + that: + - not online | default(false) + - not incremental | default(false) + - tar_volume_size is not defined + fail_msg: "Unsupported parameter used. Online backup, incremental backup, and tar volume size are not yet implemented." + quiet: true + + - name: Ensure backup directory exists + ansible.builtin.file: + path: "{{ backup_dir }}" + state: directory + mode: '0755' + + - name: Test write permissions + ansible.builtin.file: + path: "{{ backup_dir }}/.write_test" + state: touch + mode: '0644' + register: write_test + changed_when: false + + - name: Remove write test file + ansible.builtin.file: + path: "{{ backup_dir }}/.write_test" + state: absent + when: write_test is succeeded + changed_when: false + + tasks: + - name: Perform backup operations + block: + - name: Create timestamped backup directory + ansible.builtin.file: + path: "{{ backup_dir_full }}" + state: directory + mode: '0755' + + - name: Run preflight checks + ansible.builtin.include_tasks: + file: tasks/preflight.yaml + + - name: Stop Foreman services + ansible.builtin.systemd: + name: foreman.target + state: stopped + + - name: Mark services as stopped + ansible.builtin.set_fact: + service_stopped: true + + - name: Wait for PostgreSQL to fully stop + ansible.builtin.systemd: + name: postgresql.service + register: postgres_status + until: postgres_status.status.ActiveState == 'inactive' + retries: "{{ postgresql_stop_retries }}" + delay: "{{ postgresql_stop_delay }}" + when: database_mode == 'internal' + changed_when: false + + - name: Start PostgreSQL for dumps + ansible.builtin.systemd: + name: postgresql.service + state: started + when: database_mode == 'internal' + + - name: Mark PostgreSQL as started + ansible.builtin.set_fact: + postgresql_started: true + when: database_mode == 'internal' + + - name: Wait for PostgreSQL readiness + ansible.builtin.command: + cmd: pg_isready -h {{ database_host }} -p {{ database_port }} + register: pg_ready + retries: "{{ postgresql_ready_retries }}" + delay: "{{ postgresql_ready_delay }}" + until: pg_ready.rc == 0 + changed_when: false + + - name: Initialize databases_config with Foreman database + ansible.builtin.set_fact: + databases_config: + - name: foreman + database: "{{ foreman_database_name }}" + host: "{{ foreman_database_host }}" + port: "{{ foreman_database_port }}" + user: "{{ foreman_database_user }}" + password: "{{ foreman_database_password }}" + no_log: true + + - name: Add Katello databases (Candlepin and Pulp) + ansible.builtin.set_fact: + databases_config: "{{ databases_config + katello_databases }}" + vars: + katello_databases: + - name: candlepin + database: "{{ candlepin_database_name }}" + host: "{{ candlepin_database_host }}" + port: "{{ candlepin_database_port }}" + user: "{{ candlepin_database_user }}" + password: "{{ candlepin_database_password }}" + - name: pulp + database: "{{ pulp_database_name }}" + host: "{{ pulp_database_host }}" + port: "{{ pulp_database_port }}" + user: "{{ pulp_database_user }}" + password: "{{ pulp_database_password }}" + when: "'katello' in enabled_features" + no_log: true + + - name: Add IOP databases + ansible.builtin.set_fact: + databases_config: "{{ databases_config + iop_databases }}" + vars: + iop_databases: + - name: iop_advisor + database: "{{ iop_advisor_database_name }}" + host: "{{ iop_advisor_database_host }}" + port: "{{ iop_advisor_database_port }}" + user: "{{ iop_advisor_database_user }}" + password: "{{ iop_advisor_database_password }}" + - name: iop_inventory + database: "{{ iop_inventory_database_name }}" + host: "{{ iop_inventory_database_host }}" + port: "{{ iop_inventory_database_port }}" + user: "{{ iop_inventory_database_user }}" + password: "{{ iop_inventory_database_password }}" + - name: iop_remediations + database: "{{ iop_remediation_database_name }}" + host: "{{ iop_remediation_database_host }}" + port: "{{ iop_remediation_database_port }}" + user: "{{ iop_remediation_database_user }}" + password: "{{ iop_remediation_database_password }}" + - name: iop_vmaas + database: "{{ iop_vmaas_database_name }}" + host: "{{ iop_vmaas_database_host }}" + port: "{{ iop_vmaas_database_port }}" + user: "{{ iop_vmaas_database_user }}" + password: "{{ iop_vmaas_database_password }}" + - name: iop_vulnerability + database: "{{ iop_vulnerability_database_name }}" + host: "{{ iop_vulnerability_database_host }}" + port: "{{ iop_vulnerability_database_port }}" + user: "{{ iop_vulnerability_database_user }}" + password: "{{ iop_vulnerability_database_password }}" + when: "'iop' in enabled_features" + no_log: true + + - name: Build database names list for display + ansible.builtin.set_fact: + databases_to_backup: "{{ databases_config | map(attribute='database') | list }}" + + - name: Dump databases + ansible.builtin.include_tasks: + file: tasks/database_dumps.yaml + + - name: Backup foremanctl state + ansible.builtin.include_tasks: + file: tasks/foremanctl_state.yaml + + - name: Backup pulp content + ansible.builtin.include_tasks: + file: tasks/pulp_content.yaml + when: not skip_pulp_content | default(false) + + - name: Generate backup metadata + ansible.builtin.include_tasks: + file: tasks/metadata.yaml + + - name: Stop PostgreSQL + ansible.builtin.systemd: + name: postgresql.service + state: stopped + when: + - database_mode == 'internal' + - postgresql_started | default(false) + + - name: Mark PostgreSQL as stopped + ansible.builtin.set_fact: + postgresql_started: false + when: database_mode == 'internal' + + - name: Start Foreman services + ansible.builtin.systemd: + name: foreman.target + state: started + + - name: Mark services as started + ansible.builtin.set_fact: + service_stopped: false + + - name: Display backup completion + ansible.builtin.debug: + msg: | + Backup completed successfully. + Location: {{ backup_dir_full }} + Databases: {{ databases_to_backup | join(', ') }} + + rescue: + - name: Restore PostgreSQL on failure + ansible.builtin.systemd: + name: postgresql.service + state: stopped + when: + - database_mode == 'internal' + - postgresql_started | default(false) + failed_when: false + + - name: Restore Foreman services on failure + ansible.builtin.systemd: + name: foreman.target + state: started + when: service_stopped | default(false) + failed_when: false + + - name: Report failure + ansible.builtin.fail: + msg: | + Backup failed: {{ ansible_failed_result.msg | default('Unknown error') }} + Services have been restarted. + Partial backup may exist at: {{ backup_dir_full }} diff --git a/src/playbooks/backup/metadata.obsah.yaml b/src/playbooks/backup/metadata.obsah.yaml new file mode 100644 index 000000000..87f97546d --- /dev/null +++ b/src/playbooks/backup/metadata.obsah.yaml @@ -0,0 +1,34 @@ +--- +help: | + Create offline backup of Foreman databases and configuration + +variables: + backup_dir: + parameter: backup_dir + help: Directory where backup files will be stored + type: AbsolutePath + persist: false + + online: + help: Perform online backup without stopping services (not yet implemented) + action: store_true + persist: false + + skip_pulp_content: + help: Skip Pulp content directory backup (not yet implemented) + action: store_true + persist: false + + incremental: + help: Perform incremental backup (not yet implemented) + action: store_true + persist: false + + tar_volume_size: + help: Split tar archives at specified size in MB (not yet implemented, for Pulp content only) + persist: false + + wait_for_tasks: + help: Wait for running tasks to complete instead of failing immediately + action: store_true + persist: false diff --git a/src/playbooks/backup/tasks/database_dumps.yaml b/src/playbooks/backup/tasks/database_dumps.yaml new file mode 100644 index 000000000..ab02f2b57 --- /dev/null +++ b/src/playbooks/backup/tasks/database_dumps.yaml @@ -0,0 +1,36 @@ +--- +# Dump all detected databases using pg_dump -Fc (PostgreSQL custom format, compressed) + +- name: Dump databases + ansible.builtin.command: + cmd: > + pg_dump + -h {{ item.host }} + -p {{ item.port }} + -U {{ item.user }} + -Fc + -f {{ backup_dir_full }}/{{ item.name }}.dump + {{ item.database }} + environment: + PGPASSWORD: "{{ item.password }}" + loop: "{{ databases_config }}" + changed_when: true + no_log: true + +- name: Calculate total backup size + ansible.builtin.find: + paths: "{{ backup_dir_full }}" + patterns: "*.dump" + register: backup_files + +- name: Calculate total size + ansible.builtin.set_fact: + total_size_bytes: "{{ backup_files.files | map(attribute='size') | sum }}" + +- name: Display backup summary + ansible.builtin.debug: + msg: | + Database dumps completed: + - Total files: {{ backup_files.matched }} + - Total size: {{ total_size_bytes | int | human_readable }} + - Location: {{ backup_dir_full }} diff --git a/src/playbooks/backup/tasks/foremanctl_state.yaml b/src/playbooks/backup/tasks/foremanctl_state.yaml new file mode 100644 index 000000000..7792e5350 --- /dev/null +++ b/src/playbooks/backup/tasks/foremanctl_state.yaml @@ -0,0 +1,25 @@ +--- +- name: Check if foremanctl state directory exists + ansible.builtin.stat: + path: "{{ obsah_state_path }}" + register: foremanctl_state_dir + +- name: Backup foremanctl state directory + community.general.archive: + path: "{{ obsah_state_path }}" + dest: "{{ backup_dir_full }}/foremanctl-state.tar.gz" + format: gz + mode: '0644' + when: foremanctl_state_dir.stat.exists + register: state_backup + +- name: Verify foremanctl state backup + ansible.builtin.stat: + path: "{{ backup_dir_full }}/foremanctl-state.tar.gz" + register: state_stat + when: foremanctl_state_dir.stat.exists + +- name: Display foremanctl state backup info + ansible.builtin.debug: + msg: "Backed up foremanctl state ({{ state_stat.stat.size | human_readable }})" + when: foremanctl_state_dir.stat.exists diff --git a/src/playbooks/backup/tasks/metadata.yaml b/src/playbooks/backup/tasks/metadata.yaml new file mode 100644 index 000000000..ac37b47d8 --- /dev/null +++ b/src/playbooks/backup/tasks/metadata.yaml @@ -0,0 +1,92 @@ +--- +# Generate backup metadata before stopping services +# This metadata helps with restore operations and compatibility checking + +- name: Set hostname from facts + ansible.builtin.set_fact: + backup_hostname: "{{ ansible_fqdn }}" + +- name: Set OS version from facts + ansible.builtin.set_fact: + backup_os_version: "{{ ansible_distribution }} {{ ansible_distribution_version }}" + +- name: Gather package facts + ansible.builtin.package_facts: + +- name: Set foremanctl version + ansible.builtin.set_fact: + foremanctl_version: >- + {{ + ansible_facts.packages['foremanctl'][0].version ~ + ('-' ~ ansible_facts.packages['foremanctl'][0].release if ansible_facts.packages['foremanctl'][0].release | default('') else '') + if 'foremanctl' in ansible_facts.packages + else 'unknown' + }} + +- name: Query container images + containers.podman.podman_image_info: + register: container_images_result + failed_when: false + +- name: Set container images raw data + ansible.builtin.set_fact: + container_images_raw: "{{ container_images_result.images }}" + when: container_images_result is succeeded + +- name: Build detailed container image list + ansible.builtin.set_fact: + container_images_detailed: >- + {{ + container_images_detailed | default([]) + + [{ + 'name': (item.RepoTags | first) if item.RepoTags | default([]) | length > 0 else '', + 'digest': (item.RepoDigests | first) if item.RepoDigests | default([]) | length > 0 else '', + 'id': item.Id, + 'created': item.Created + }] + }} + loop: "{{ container_images_raw | default([]) }}" + when: container_images_raw is defined + +- name: Set enabled features list + ansible.builtin.set_fact: + enabled_features_list: "{{ enabled_features | default([]) }}" + +- name: Check if pulp content was backed up + ansible.builtin.stat: + path: "{{ backup_dir_full }}/pulp-content.tar.gz" + register: pulp_content_backup_check + failed_when: false + +- name: Build backup metadata + ansible.builtin.set_fact: + backup_metadata: + hostname: "{{ backup_hostname }}" + os_version: "{{ backup_os_version }}" + foremanctl_version: "{{ foremanctl_version }}" + online: false + incremental: false + timestamp: "{{ backup_timestamp }}" + databases: "{{ databases_to_backup }}" + iop_enabled: "{{ 'iop' in (enabled_features_list | map('string') | list) }}" + enabled_features: "{{ enabled_features_list }}" + database_mode: "{{ database_mode }}" + container_images: "{{ container_images_detailed | default([]) }}" + backed_up_components: >- + {{ + [ + 'databases', + 'container_images', + 'foremanctl_state' + ] + (['pulp_content'] if pulp_content_backup_check.stat.exists | default(false) else []) + }} + +- name: Write metadata file + ansible.builtin.copy: + content: "{{ backup_metadata | to_nice_yaml }}" + dest: "{{ backup_dir_full }}/metadata.yml" + mode: '0644' + +- name: Display metadata location + ansible.builtin.debug: + msg: "Backup metadata written to {{ backup_dir_full }}/metadata.yml" diff --git a/src/playbooks/backup/tasks/preflight.yaml b/src/playbooks/backup/tasks/preflight.yaml new file mode 100644 index 000000000..164f0a12e --- /dev/null +++ b/src/playbooks/backup/tasks/preflight.yaml @@ -0,0 +1,123 @@ +--- +# Preflight checks for backup operation +# - Check for running Foreman tasks +# - Check for running Pulp tasks +# - Run amcheck on databases (if available and local) + +- name: Get PostgreSQL admin password from podman secret + ansible.builtin.command: podman secret inspect postgresql-admin-password --showsecret + register: postgresql_admin_password_secret + when: database_mode == 'internal' + changed_when: false + failed_when: false + no_log: true + +- name: Set PostgreSQL admin password fact + ansible.builtin.set_fact: + postgresql_admin_password: "{{ (postgresql_admin_password_secret.stdout | from_json)[0].SecretData }}" + when: + - database_mode == 'internal' + - postgresql_admin_password_secret is succeeded + no_log: true + +- name: Check for running Foreman tasks + ansible.builtin.uri: + url: "https://{{ foreman_server_fqdn }}/foreman_tasks/api/tasks?search=state%3Drunning&per_page=1" + method: GET + user: "{{ foreman_initial_admin_username }}" + password: "{{ foreman_initial_admin_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: foreman_tasks_check + failed_when: false + changed_when: false + no_log: true + +- name: Set Foreman running tasks count + ansible.builtin.set_fact: + foreman_running_tasks: "{{ foreman_tasks_check.json.subtotal | default(0) | int }}" + when: foreman_tasks_check.status == 200 + +- name: Wait for Foreman tasks to complete (if --wait-for-tasks) + ansible.builtin.uri: + url: "https://{{ foreman_server_fqdn }}/foreman_tasks/api/tasks?search=state%3Drunning&per_page=1" + method: GET + user: "{{ foreman_initial_admin_username }}" + password: "{{ foreman_initial_admin_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: foreman_tasks_wait + until: foreman_tasks_wait.json.subtotal | default(0) == 0 + retries: "{{ task_wait_retries }}" + delay: "{{ task_wait_delay }}" + when: + - wait_for_tasks | default(false) + - foreman_running_tasks | default(0) | int > 0 + changed_when: false + no_log: true + +- name: Fail if Foreman tasks are running (without --wait-for-tasks) + ansible.builtin.fail: + msg: | + There are {{ foreman_running_tasks }} running Foreman task(s). + Please wait for these to complete or use --wait-for-tasks flag. + when: + - not wait_for_tasks | default(false) + - foreman_running_tasks | default(0) | int > 0 + +- name: Check for running Pulp tasks + community.postgresql.postgresql_query: + db: "{{ pulp_database_name }}" + login_host: "{{ pulp_database_host }}" + login_port: "{{ pulp_database_port }}" + login_user: "{{ pulp_database_user }}" + login_password: "{{ pulp_database_password }}" + query: "SELECT COUNT(*) as count FROM core_task WHERE state IN ('running', 'waiting')" + register: pulp_tasks_check + failed_when: false + changed_when: false + no_log: true + +- name: Set Pulp running tasks count + ansible.builtin.set_fact: + pulp_running_tasks: "{{ pulp_tasks_check.query_result[0].count | default(0) | int }}" + when: pulp_tasks_check is succeeded + +- name: Wait for Pulp tasks to complete (if --wait-for-tasks) + community.postgresql.postgresql_query: + db: "{{ pulp_database_name }}" + login_host: "{{ pulp_database_host }}" + login_port: "{{ pulp_database_port }}" + login_user: "{{ pulp_database_user }}" + login_password: "{{ pulp_database_password }}" + query: "SELECT COUNT(*) as count FROM core_task WHERE state IN ('running', 'waiting')" + register: pulp_tasks_wait + until: pulp_tasks_wait.query_result[0].count | default(0) == 0 + retries: "{{ task_wait_retries }}" + delay: "{{ task_wait_delay }}" + when: + - wait_for_tasks | default(false) + - pulp_running_tasks | default(0) | int > 0 + changed_when: false + no_log: true + +- name: Fail if Pulp tasks are running (without --wait-for-tasks) + ansible.builtin.fail: + msg: | + There are {{ pulp_running_tasks }} running Pulp task(s). + Please wait for these to complete or use --wait-for-tasks flag. + when: + - not wait_for_tasks | default(false) + - pulp_running_tasks | default(0) | int > 0 + +- name: Run database index integrity checks + ansible.builtin.include_role: + name: "{{ item }}" + loop: + - check_db_index_foreman + - check_db_index_candlepin + - check_db_index_pulp + - check_db_index_iop + when: database_mode == 'internal' diff --git a/src/playbooks/backup/tasks/pulp_content.yaml b/src/playbooks/backup/tasks/pulp_content.yaml new file mode 100644 index 000000000..51be57afc --- /dev/null +++ b/src/playbooks/backup/tasks/pulp_content.yaml @@ -0,0 +1,144 @@ +--- +- name: Determine Pulp storage path from pulp_volumes + ansible.builtin.set_fact: + pulp_storage_path: "{{ (pulp_volumes | default(['/var/lib/pulp:/var/lib/pulp']) | first).split(':') | first }}" + when: pulp_volumes is defined + +- name: Set default Pulp storage path + ansible.builtin.set_fact: + pulp_storage_path: /var/lib/pulp + when: pulp_volumes is not defined + +- name: Check if pulp media directory exists + ansible.builtin.stat: + path: "{{ pulp_storage_path }}/media" + register: pulp_media_stat + +- name: Count files in pulp media directory + ansible.builtin.find: + paths: "{{ pulp_storage_path }}/media" + recurse: true + file_type: file + register: pulp_media_files + when: pulp_media_stat.stat.exists + +- name: Calculate pulp media directory size + ansible.builtin.command: + cmd: du -sb {{ pulp_storage_path }}/media + register: pulp_media_size + changed_when: false + failed_when: false + when: + - pulp_media_stat.stat.exists + - pulp_media_files.matched > 0 + +- name: Display pulp content backup start + ansible.builtin.debug: + msg: >- + Backing up {{ pulp_media_files.matched }} files + ({{ (pulp_media_size.stdout.split()[0] | int / 1024 / 1024) | round(2) }} MB) + from {{ pulp_storage_path }}/media + when: + - pulp_media_stat.stat.exists + - pulp_media_files.matched > 0 + +- name: Check if Pulp encryption keys exist + ansible.builtin.stat: + path: "{{ pulp_storage_path }}/{{ item }}" + loop: + - database_fields.symmetric.key + - django_secret_key + register: pulp_key_files + +- name: Backup pulp content directory with encryption keys # noqa: command-instead-of-module + ansible.builtin.command: + cmd: > + tar -czf {{ backup_dir_full }}/pulp-content.tar.gz + -C {{ pulp_storage_path }} + --exclude=media/exports + --exclude=media/imports + --exclude=media/sync_imports + media + database_fields.symmetric.key + django_secret_key + register: pulp_content_archive + changed_when: true + when: + - pulp_media_stat.stat.exists + - pulp_media_files.matched > 0 + +- name: Backup encryption keys when media is empty # noqa: command-instead-of-module + ansible.builtin.command: + cmd: > + tar -czf {{ backup_dir_full }}/pulp-content.tar.gz + -C {{ pulp_storage_path }} + --ignore-failed-read + database_fields.symmetric.key + django_secret_key + register: pulp_content_archive + changed_when: true + when: + - not skip_pulp_content | default(false) + - pulp_media_stat.stat.exists + - pulp_media_files.matched == 0 + - pulp_key_files.results | selectattr('stat.exists', 'defined') | selectattr('stat.exists', 'equalto', true) | list | length > 0 + +- name: Verify pulp content archive was created + ansible.builtin.stat: + path: "{{ backup_dir_full }}/pulp-content.tar.gz" + register: pulp_content_archive_stat + when: >- + (pulp_media_stat.stat.exists and pulp_media_files.matched > 0) or + (not skip_pulp_content | default(false) and + pulp_media_stat.stat.exists and + pulp_media_files.matched == 0 and + pulp_key_files.results | selectattr('stat.exists', 'defined') | selectattr('stat.exists', 'equalto', true) | list | length > 0) + +- name: Count backed up encryption keys + ansible.builtin.set_fact: + backed_up_keys_count: "{{ pulp_key_files.results | selectattr('stat.exists', 'defined') | selectattr('stat.exists', 'equalto', true) | list | length }}" + when: pulp_content_archive_stat.stat.exists | default(false) + +- name: Display pulp content backup completion + ansible.builtin.debug: + msg: >- + Pulp content backup completed: {{ backup_dir_full }}/pulp-content.tar.gz + ({{ (pulp_content_archive_stat.stat.size / 1024 / 1024) | round(2) }} MB compressed, + {{ (100 - (pulp_content_archive_stat.stat.size / (pulp_media_size.stdout.split()[0] | int) * 100)) | round(1) }}% compression, + {{ backed_up_keys_count }} encryption keys included) + when: + - pulp_media_stat.stat.exists + - pulp_media_files.matched > 0 + - pulp_content_archive_stat.stat.exists + +- name: Display encryption keys backup completion + ansible.builtin.debug: + msg: >- + Pulp encryption keys backup completed: {{ backup_dir_full }}/pulp-content.tar.gz + ({{ backed_up_keys_count }} encryption keys, + {{ (pulp_content_archive_stat.stat.size / 1024) | round(2) }} KB) + when: + - pulp_media_stat.stat.exists + - pulp_media_files.matched == 0 + - pulp_content_archive_stat.stat.exists + +- name: Display skip message - flag set + ansible.builtin.debug: + msg: "Skipping pulp content backup (--skip-pulp-content flag set)" + when: skip_pulp_content | default(false) + +- name: Display skip message - directory empty and no keys + ansible.builtin.debug: + msg: "Skipping pulp content backup (no files in {{ pulp_storage_path }}/media and no encryption keys found)" + when: + - pulp_media_stat.stat.exists + - pulp_media_files.matched == 0 + - not skip_pulp_content | default(false) + - not (pulp_content_archive_stat.stat.exists | default(false)) + +- name: Display skip message - directory missing + ansible.builtin.debug: + msg: "Skipping pulp content backup ({{ pulp_storage_path }}/media does not exist)" + when: + - not pulp_media_stat.stat.exists + - not skip_pulp_content | default(false) diff --git a/src/roles/check_db_index_candlepin/tasks/main.yml b/src/roles/check_db_index_candlepin/tasks/main.yml new file mode 100644 index 000000000..ff4059d67 --- /dev/null +++ b/src/roles/check_db_index_candlepin/tasks/main.yml @@ -0,0 +1,54 @@ +--- +# Check Candlepin database indexes using PostgreSQL amcheck extension +# This check verifies the logical consistency of B-tree indexes to detect corruption + +- name: Check if amcheck extension is available + community.postgresql.postgresql_query: + db: postgres + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: SELECT COUNT(*) as count FROM pg_available_extensions WHERE name = 'amcheck' + register: check_db_index_candlepin_amcheck_available + when: database_mode == 'internal' + failed_when: false + changed_when: false + no_log: true + +- name: Run amcheck on Candlepin database + community.postgresql.postgresql_query: + db: "{{ candlepin_database_name }}" + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: | + CREATE EXTENSION IF NOT EXISTS amcheck; + SELECT bt_index_check(c.oid, true) + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relkind = 'i' AND n.nspname = 'public'; + when: + - database_mode == 'internal' + - check_db_index_candlepin_amcheck_available is succeeded + - check_db_index_candlepin_amcheck_available.query_result[0].count | default(0) > 0 + register: check_db_index_candlepin_db_index_check + failed_when: false + changed_when: false + no_log: true + +- name: Report Candlepin database index check status + ansible.builtin.debug: + msg: "Candlepin database index check: {{ 'PASSED' if check_db_index_candlepin_db_index_check is succeeded else 'FAILED - indexes may be corrupted' }}" + when: + - database_mode == 'internal' + - check_db_index_candlepin_amcheck_available is succeeded + - check_db_index_candlepin_amcheck_available.query_result[0].count | default(0) > 0 + +- name: Skip message when amcheck not available + ansible.builtin.debug: + msg: "Candlepin database index check: SKIPPED - amcheck extension not available" + when: + - database_mode == 'internal' + - check_db_index_candlepin_amcheck_available is failed or check_db_index_candlepin_amcheck_available.query_result[0].count | default(0) == 0 diff --git a/src/roles/check_db_index_foreman/tasks/main.yml b/src/roles/check_db_index_foreman/tasks/main.yml new file mode 100644 index 000000000..670c6e0f3 --- /dev/null +++ b/src/roles/check_db_index_foreman/tasks/main.yml @@ -0,0 +1,54 @@ +--- +# Check Foreman database indexes using PostgreSQL amcheck extension +# This check verifies the logical consistency of B-tree indexes to detect corruption + +- name: Check if amcheck extension is available + community.postgresql.postgresql_query: + db: postgres + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: SELECT COUNT(*) as count FROM pg_available_extensions WHERE name = 'amcheck' + register: check_db_index_foreman_amcheck_available + when: database_mode == 'internal' + failed_when: false + changed_when: false + no_log: true + +- name: Run amcheck on Foreman database + community.postgresql.postgresql_query: + db: "{{ foreman_database_name }}" + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: | + CREATE EXTENSION IF NOT EXISTS amcheck; + SELECT bt_index_check(c.oid, true) + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relkind = 'i' AND n.nspname = 'public'; + when: + - database_mode == 'internal' + - check_db_index_foreman_amcheck_available is succeeded + - check_db_index_foreman_amcheck_available.query_result[0].count | default(0) > 0 + register: check_db_index_foreman_db_index_check + failed_when: false + changed_when: false + no_log: true + +- name: Report Foreman database index check status + ansible.builtin.debug: + msg: "Foreman database index check: {{ 'PASSED' if check_db_index_foreman_db_index_check is succeeded else 'FAILED - indexes may be corrupted' }}" + when: + - database_mode == 'internal' + - check_db_index_foreman_amcheck_available is succeeded + - check_db_index_foreman_amcheck_available.query_result[0].count | default(0) > 0 + +- name: Skip message when amcheck not available + ansible.builtin.debug: + msg: "Foreman database index check: SKIPPED - amcheck extension not available" + when: + - database_mode == 'internal' + - check_db_index_foreman_amcheck_available is failed or check_db_index_foreman_amcheck_available.query_result[0].count | default(0) == 0 diff --git a/src/roles/check_db_index_iop/tasks/main.yml b/src/roles/check_db_index_iop/tasks/main.yml new file mode 100644 index 000000000..c82aea230 --- /dev/null +++ b/src/roles/check_db_index_iop/tasks/main.yml @@ -0,0 +1,83 @@ +--- +# Check IOP database indexes using PostgreSQL amcheck extension +# This check verifies the logical consistency of B-tree indexes to detect corruption +# IOP has 5 databases: advisor, inventory, remediations, vmaas, vulnerability + +- name: Check if IOP feature is enabled + ansible.builtin.set_fact: + check_db_index_iop_feature_enabled: "{{ 'iop' in (enabled_features | default([]) | map('string') | list) }}" + +- name: Check if amcheck extension is available + community.postgresql.postgresql_query: + db: postgres + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: SELECT COUNT(*) as count FROM pg_available_extensions WHERE name = 'amcheck' + register: check_db_index_iop_amcheck_available + when: + - database_mode == 'internal' + - check_db_index_iop_feature_enabled | default(false) + failed_when: false + changed_when: false + no_log: true + +- name: Run amcheck on IOP databases + community.postgresql.postgresql_query: + db: "{{ iop_db.db }}" + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: | + CREATE EXTENSION IF NOT EXISTS amcheck; + SELECT bt_index_check(c.oid, true) + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relkind = 'i' AND n.nspname = 'public'; + loop: + - { db: "{{ iop_advisor_database_name }}", name: "IOP Advisor" } + - { db: "{{ iop_inventory_database_name }}", name: "IOP Inventory" } + - { db: "{{ iop_remediation_database_name }}", name: "IOP Remediations" } + - { db: "{{ iop_vmaas_database_name }}", name: "IOP VMaaS" } + - { db: "{{ iop_vulnerability_database_name }}", name: "IOP Vulnerability" } + loop_control: + loop_var: iop_db + when: + - database_mode == 'internal' + - check_db_index_iop_feature_enabled | default(false) + - check_db_index_iop_amcheck_available is succeeded + - check_db_index_iop_amcheck_available.query_result[0].count | default(0) > 0 + register: check_db_index_iop_db_index_checks + failed_when: false + changed_when: false + no_log: true + +- name: Report IOP database index check status + ansible.builtin.debug: + msg: "{{ iop_check.iop_db.name }} database index check: {{ 'PASSED' if iop_check is succeeded else 'FAILED - indexes may be corrupted' }}" + loop: "{{ check_db_index_iop_db_index_checks.results | default([]) }}" + loop_control: + loop_var: iop_check + label: "{{ iop_check.iop_db.name }}" + when: + - database_mode == 'internal' + - check_db_index_iop_feature_enabled | default(false) + - check_db_index_iop_amcheck_available is succeeded + - check_db_index_iop_amcheck_available.query_result[0].count | default(0) > 0 + +- name: Skip message when IOP not enabled + ansible.builtin.debug: + msg: "IOP database index checks: SKIPPED - IOP feature not enabled" + when: + - database_mode == 'internal' + - not (check_db_index_iop_feature_enabled | default(false)) + +- name: Skip message when amcheck not available + ansible.builtin.debug: + msg: "IOP database index checks: SKIPPED - amcheck extension not available" + when: + - database_mode == 'internal' + - check_db_index_iop_feature_enabled | default(false) + - check_db_index_iop_amcheck_available is failed or check_db_index_iop_amcheck_available.query_result[0].count | default(0) == 0 diff --git a/src/roles/check_db_index_pulp/tasks/main.yml b/src/roles/check_db_index_pulp/tasks/main.yml new file mode 100644 index 000000000..b8d49f981 --- /dev/null +++ b/src/roles/check_db_index_pulp/tasks/main.yml @@ -0,0 +1,54 @@ +--- +# Check Pulp database indexes using PostgreSQL amcheck extension +# This check verifies the logical consistency of B-tree indexes to detect corruption + +- name: Check if amcheck extension is available + community.postgresql.postgresql_query: + db: postgres + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: SELECT COUNT(*) as count FROM pg_available_extensions WHERE name = 'amcheck' + register: check_db_index_pulp_amcheck_available + when: database_mode == 'internal' + failed_when: false + changed_when: false + no_log: true + +- name: Run amcheck on Pulp database + community.postgresql.postgresql_query: + db: "{{ pulp_database_name }}" + login_host: "{{ database_host }}" + login_port: "{{ database_port }}" + login_user: postgres + login_password: "{{ postgresql_admin_password }}" + query: | + CREATE EXTENSION IF NOT EXISTS amcheck; + SELECT bt_index_check(c.oid, true) + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relkind = 'i' AND n.nspname = 'public'; + when: + - database_mode == 'internal' + - check_db_index_pulp_amcheck_available is succeeded + - check_db_index_pulp_amcheck_available.query_result[0].count | default(0) > 0 + register: check_db_index_pulp_db_index_check + failed_when: false + changed_when: false + no_log: true + +- name: Report Pulp database index check status + ansible.builtin.debug: + msg: "Pulp database index check: {{ 'PASSED' if check_db_index_pulp_db_index_check is succeeded else 'FAILED - indexes may be corrupted' }}" + when: + - database_mode == 'internal' + - check_db_index_pulp_amcheck_available is succeeded + - check_db_index_pulp_amcheck_available.query_result[0].count | default(0) > 0 + +- name: Skip message when amcheck not available + ansible.builtin.debug: + msg: "Pulp database index check: SKIPPED - amcheck extension not available" + when: + - database_mode == 'internal' + - check_db_index_pulp_amcheck_available is failed or check_db_index_pulp_amcheck_available.query_result[0].count | default(0) == 0 diff --git a/src/roles/checks/tasks/main.yml b/src/roles/checks/tasks/main.yml index 90dbf9e1d..53a8f30e6 100644 --- a/src/roles/checks/tasks/main.yml +++ b/src/roles/checks/tasks/main.yml @@ -6,6 +6,10 @@ - check_hostname - check_database_connection - check_system_requirements + - check_db_index_foreman + - check_db_index_candlepin + - check_db_index_pulp + - check_db_index_iop - name: Report status of checks ansible.builtin.fail: