From 3534b632ac015b892d63d5a8c6297a051e75a531 Mon Sep 17 00:00:00 2001 From: Roman Podoliaka Date: Sat, 28 Sep 2024 21:18:45 +0100 Subject: [PATCH] postgres: add support for restoring backups Only the "custom" format of pg_restore is supported, and the backup file must be located on the host where the ansible playbook is executed. The intended use cases are: * Migrating to new PostgreSQL/Ubuntu versions in production (the former notoriously requires downtime and either restoring a database backup, or running the migration tool to update the storage format offline). * Testing of database backups that we create today. --- .github/workflows/ci.yaml | 30 ++++++++++++++++++- roles/postgres/meta/main.yml | 5 ++++ roles/postgres/tasks/main.yml | 6 ++++ roles/postgres/tasks/restore.yml | 35 ++++++++++++++++++++++ testdata/xsnippet-api_20241003-030004.pgc | Bin 0 -> 11941 bytes 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 roles/postgres/tasks/restore.yml create mode 100644 testdata/xsnippet-api_20241003-030004.pgc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d7a4a1..4b56b57 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,6 +60,14 @@ jobs: popd ansible: + strategy: + matrix: + db_backup: + # Clean install + - '' + # Restore a database backup + - 'testdata/xsnippet-api_20241003-030004.pgc' + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -96,8 +104,28 @@ jobs: - name: Run the playbook run: | + read -r -d '' extra_vars << 'EOF' || true + { + "volume_device": "${{ steps.volume-device.outputs.uri }}", + "postgres_users": [ + { + "database": "{{ xsnippet_api_user }}", + "username": "{{ xsnippet_api_user }}", + "backup_schedule": "*-*-* 3:00:00", + "backup_restore": "${{ matrix.db_backup }}" + } + ] + } + EOF + ansible-playbook \ -vvv \ - -e volume_device="${{ steps.volume-device.outputs.uri }}" \ + -e "${extra_vars}" \ --inventory inventories/ci \ site.yml + + - name: Verify that the database backup has been restored correctly + if: matrix.db_backup != '' + run: | + # Expect at least one full page of results + test "$(curl http://127.0.0.1:8080/v1/snippets | jq length)" == "20" diff --git a/roles/postgres/meta/main.yml b/roles/postgres/meta/main.yml index f6dabc5..cf4f31f 100644 --- a/roles/postgres/meta/main.yml +++ b/roles/postgres/meta/main.yml @@ -28,6 +28,11 @@ argument_specs: description: | The time of when database backups should be triggered. Uses the systemd calendar event expression syntax (see man 7 systemd.time). If not set, backups will not be created. + backup_restore: + type: str + required: false + description: | + Path to a database backup to be restored. default: [] description: | The list of database/username pairs to create. diff --git a/roles/postgres/tasks/main.yml b/roles/postgres/tasks/main.yml index 039dc4d..2985618 100644 --- a/roles/postgres/tasks/main.yml +++ b/roles/postgres/tasks/main.yml @@ -57,6 +57,12 @@ become: true become_user: postgres +- name: Restore database backups if necessary + ansible.builtin.include_tasks: + file: restore.yml + with_items: "{{ postgres_users }}" + when: item.backup_restore is defined and item.backup_restore + - name: Install the script for backup rotation ansible.builtin.copy: src: 'rotate.py' diff --git a/roles/postgres/tasks/restore.yml b/roles/postgres/tasks/restore.yml new file mode 100644 index 0000000..ee05b8f --- /dev/null +++ b/roles/postgres/tasks/restore.yml @@ -0,0 +1,35 @@ +- name: Restore database backups if necessary + become: true + become_user: postgres + block: + - name: Check if we need to restore a database backup + community.postgresql.postgresql_query: + db: "{{ item.database }}" + # pg_tables is a system view that is implicitly created in every + # database. The information in is local to that particular database. + query: "SELECT * FROM pg_catalog.pg_tables WHERE tableowner = %s" + positional_args: + - "{{ item.username }}" + register: existing_tables + + - name: Copy and restore a database backup (only if the database is empty) + when: existing_tables.rowcount == 0 + block: + - name: Create a temporary backup directory + ansible.builtin.tempfile: + state: directory + suffix: backup + register: backup_tmp_dir + + - name: Copy the database backup + ansible.builtin.copy: + src: "{{ item.backup_restore }}" + dest: "{{ [backup_tmp_dir.path, item.backup_restore | basename] | path_join }}" + mode: 'u=rw,g=r,o=' + + - name: Restore the database backup + community.postgresql.postgresql_db: + name: "{{ item.database }}" + state: "restore" + target: "{{ [backup_tmp_dir.path, item.backup_restore | basename] | path_join }}" + target_opts: "--single-transaction --exit-on-error" diff --git a/testdata/xsnippet-api_20241003-030004.pgc b/testdata/xsnippet-api_20241003-030004.pgc new file mode 100644 index 0000000000000000000000000000000000000000..bc42b524551e27e8abe0542bf2553bf5f4d28a4b GIT binary patch literal 11941 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 h9l5fanvaj>2dwiiJbd_Q{G+eF`ihlo=;? literal 0 HcmV?d00001