From e35cc35de74ecdc4a550f4545db31a70433e6685 Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Sat, 13 Sep 2025 16:43:19 +0200 Subject: [PATCH] feat(pretix): add ansible role and playbook --- playbooks/pretix.yml | 32 +++++++ playbooks/pretix_postgresql_valkey.yml | 42 +++++++++ roles/pretix/README.md | 16 ++++ roles/pretix/defaults/main/config.yml | 85 +++++++++++++++++++ roles/pretix/defaults/main/main.yml | 14 +++ .../pretix/defaults/main/system_packages.yml | 22 +++++ roles/pretix/defaults/main/systemd.yml | 22 +++++ roles/pretix/defaults/main/user.yml | 7 ++ roles/pretix/defaults/main/virtualenv.yml | 11 +++ roles/pretix/meta/main.yml | 9 ++ roles/pretix/tasks/check.yml | 14 +++ roles/pretix/tasks/configure.yml | 9 ++ roles/pretix/tasks/deploy-systemd.yml | 32 +++++++ roles/pretix/tasks/deploy.yml | 5 ++ roles/pretix/tasks/main.yml | 16 ++++ roles/pretix/tasks/prepare-systemd.yml | 30 +++++++ roles/pretix/tasks/prepare.yml | 29 +++++++ roles/pretix/templates/pretix.service.j2 | 16 ++++ roles/pretix/vars/main.yml | 7 ++ 19 files changed, 418 insertions(+) create mode 100644 playbooks/pretix.yml create mode 100644 playbooks/pretix_postgresql_valkey.yml create mode 100644 roles/pretix/README.md create mode 100644 roles/pretix/defaults/main/config.yml create mode 100644 roles/pretix/defaults/main/main.yml create mode 100644 roles/pretix/defaults/main/system_packages.yml create mode 100644 roles/pretix/defaults/main/systemd.yml create mode 100644 roles/pretix/defaults/main/user.yml create mode 100644 roles/pretix/defaults/main/virtualenv.yml create mode 100644 roles/pretix/meta/main.yml create mode 100644 roles/pretix/tasks/check.yml create mode 100644 roles/pretix/tasks/configure.yml create mode 100644 roles/pretix/tasks/deploy-systemd.yml create mode 100644 roles/pretix/tasks/deploy.yml create mode 100644 roles/pretix/tasks/main.yml create mode 100644 roles/pretix/tasks/prepare-systemd.yml create mode 100644 roles/pretix/tasks/prepare.yml create mode 100644 roles/pretix/templates/pretix.service.j2 create mode 100644 roles/pretix/vars/main.yml diff --git a/playbooks/pretix.yml b/playbooks/pretix.yml new file mode 100644 index 0000000..86f4694 --- /dev/null +++ b/playbooks/pretix.yml @@ -0,0 +1,32 @@ +--- +- name: Install and configure pretix + hosts: "{{ pretix_hosts | default('pretix') }}" + become: "{{ pretix_become | default(true) }}" + gather_facts: "{{ pretix_gather_facts | default(false) }}" + roles: + - role: finallycoffee.databases.postgresql_client + vars: + postgresql_become: >-2 + {{ pretix_postgresql_client_become | default(pretix_become | default(true)) }} + postgresql_client_database: "{{ pretix_postgresql_database }}" + postgresql_client_username: "{{ pretix_postgresql_user }}" + postgresql_client_password: "{{ pretix_postgresql_password }}" + - role: finallycoffee.databases.valkey + vars: + valkey_instance: "pretix" + valkey_secret: "{{ pretix_redis_secret }}" + valkey_config_user: + - "default on +@all -DEBUG ~* >{{ pretix_redis_secret }}" + valkey_container_ports: + - "{{ pretix_redis_bind_addr }}:{{ valkey_config_port }}" + - role: pretix + vars: + pretix_config_database_name: "{{ pretix_postgresql_database }}" + pretix_config_database_user: "{{ pretix_postgresql_user }}" + pretix_config_database_password: "{{ pretix_postgresql_password }}" + vars: + pretix_postgresql_user: "pretix" + pretix_postgresql_password: ~ + pretix_postgresql_database: "pretix" + pretix_redis_secret: ~ + pretix_redis_bind_addr: "127.0.10.1:6739" diff --git a/playbooks/pretix_postgresql_valkey.yml b/playbooks/pretix_postgresql_valkey.yml new file mode 100644 index 0000000..f092ebe --- /dev/null +++ b/playbooks/pretix_postgresql_valkey.yml @@ -0,0 +1,42 @@ +--- +- import_playbook: finallycoffee.databases.postgresql_client + vars: + postgresql_hosts: "{{ pretix_hosts | default('pretix') }}" + postgresql_become: >-2 + {{ pretix_postgresql_client_become | default(pretix_become | default(true)) }} + postgresql_client_database: "{{ pretix_postgresql_database | default('pretix') }}" + postgresql_client_username: "{{ pretix_postgresql_user | default('pretix') }}" + postgresql_client_password: >-2 + {{ pretix_postgresql_password | mandatory(msg='pretix postgresql password is required') }} + +- import_playbook: finallycoffee.databases.valkey + vars: + valkey_hosts: "{{ pretix_hosts | default('pretix') }}" + valkey_instance: "pretix" + valkey_secret: "{{ pretix_redis_secret | mandatory(msg='pretix valkey secret is required') }}" + valkey_config_user: + - "default on +@all -DEBUG ~* >{{ pretix_redis_secret }}" + valkey_container_ports: + - "{{ pretix_redis_bind_addr | default('127.0.10.1:6739') }}:{{ valkey_config_port }}" + valkey_config_bind: + - "0.0.0.0" + - "-::" + +- name: Install and configure pretix + hosts: "{{ pretix_hosts | default('pretix') }}" + become: "{{ pretix_become | default(true) }}" + gather_facts: "{{ pretix_gather_facts | default(false) }}" + roles: + - role: finallycoffee.services.pretix + vars: + pretix_config_database_name: "{{ pretix_postgresql_database | default('pretix') }}" + pretix_config_database_user: "{{ pretix_postgresql_user | default('pretix') }}" + pretix_config_database_password: "{{ pretix_postgresql_password }}" + pretix_config_redis_location: >-2 + redis://:{{ pretix_redis_secret }}@{{ pretix_redis_bind_addr }}/0 + pretix_config_celery_backend: >-2 + redis://:{{ pretix_redis_secret }}@{{ pretix_redis_bind_addr }}/1 + pretix_config_celery_broker: >-2 + redis://:{{ pretix_redis_secret }}@{{ pretix_redis_bind_addr }}/2 + vars: + pretix_redis_bind_addr: "127.0.10.1:6739" diff --git a/roles/pretix/README.md b/roles/pretix/README.md new file mode 100644 index 0000000..0199be2 --- /dev/null +++ b/roles/pretix/README.md @@ -0,0 +1,16 @@ +# `finallycoffee.services.pretix` ansible role + + +## Troubleshooting + +### virtualenv + +By default, the virtualenv is located in `/var/lib/pretix/virtualenv`. +This can be controlled by setting `pretix_virtualenv_dir`. + +NOTE: To fix a broken virtualenv, try setting `pretix_virtualenv_state` to `forcereinstall` (see +[`ansible.builtin.pip` on docs.ansible.com](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/pip_module.html)). + +NOTE: To install pip packages or execute migrations in the virtualenv, ansible +needs to become the unprivilated `pretix_user` (default: `pretix`). This might +require having the `acl` system package installed. diff --git a/roles/pretix/defaults/main/config.yml b/roles/pretix/defaults/main/config.yml new file mode 100644 index 0000000..9ea1212 --- /dev/null +++ b/roles/pretix/defaults/main/config.yml @@ -0,0 +1,85 @@ +--- +pretix_config_instance_name: "My pretix installation" +pretix_config_url: "https://pretix.example.org" +pretix_config_currency: "EUR" +pretix_config_data_dir: "{{ pretix_data_dir }}" +pretix_config_trust_x_forwarded_for: "on" +pretix_config_trust_x_forwarded_proto: "on" + +pretix_config_wsgi_name: "pretix" +pretix_config_wsgi_workers: 4 +pretix_config_wsgi_max_requests: 100 +pretix_config_wsgi_log_level: "info" +pretix_config_wsgi_bind_addr: "127.0.0.1:8345" + +pretix_config_database_backend: postgresql +pretix_config_database_name: pretix +pretix_config_database_user: pretix +pretix_config_database_password: ~ +pretix_config_database_host: "" + +pretix_config_mail_host: ~ +pretix_config_mail_from: "tickets@example.org" +pretix_config_mail_user: ~ +pretix_config_mail_password: ~ +pretix_config_mail_tls: true +pretix_config_mail_ssl: false + +pretix_config_redis_location: ~ +pretix_config_redis_sessions: true + +pretix_config_celery_backend: ~ +pretix_config_celery_broker: ~ + +pretix_app_config: + url: "{{ pretix_config_url }}" + instance_name: "{{ pretix_config_instance_name }}" + datadir: "{{ pretix_config_data_dir }}" + trust_x_forwarded_for: "{{ pretix_config_trust_x_forwarded_for }}" + trust_x_forwarded_proto: "{{ pretix_config_trust_x_forwarded_proto }}" + currency: "{{ pretix_config_currency }}" + +pretix_database_config: + backend: "{{ pretix_config_database_backend }}" + name: "{{ pretix_config_database_name }}" + user: "{{ pretix_config_database_user }}" + password: "{{ pretix_config_database_password }}" + host: "{{ pretix_config_database_host }}" + +pretix_mail_minimal_config: + host: "{{ pretix_config_mail_host }}" + from: "{{ pretix_config_mail_from }}" +pretix_mail_config: >-2 + {{ pretix_mail_minimal_config + | combine({'user': pretix_config_mail_user} if pretix_config_mail_user else {}) + | combine({'password': pretix_config_mail_password} if pretix_config_mail_password else {}) + | combine({'ssl': pretix_config_mail_ssl | bool | ternary('on', 'off')} if pretix_config_mail_ssl else {}) + | combine({'tls': pretix_config_mail_tls | bool | ternary('on', 'off')} if pretix_config_mail_tls else {}) + }} + +pretix_redis_config: + location: "{{ pretix_config_redis_location }}" + sessions: "{{ pretix_config_redis_sessions | bool | ternary('true', 'false') }}" + +pretix_celery_config: + backend: "{{ pretix_config_celery_backend }}" + broker: "{{ pretix_config_celery_broker }}" + +pretix_config: {} +pretix_default_config: + pretix: "{{ pretix_app_config }}" + database: "{{ pretix_database_config }}" + mail: "{{ pretix_mail_config }}" + redis: "{{ pretix_redis_config }}" + celery: "{{ pretix_celery_config }}" + +pretix_config_merged: >-2 + {{ pretix_default_config | combine(pretix_config | default({}), recursive=True) }} + +pretix_config_file_content: |+2 + {% for kv in (pretix_config_merged | dict2items) %} + [{{ kv.key }}] + {% for entry in ((kv.value | default({}, true)) | dict2items) %} + {{ entry.key }}={{ entry.value }} + {% endfor %} + {% endfor %} diff --git a/roles/pretix/defaults/main/main.yml b/roles/pretix/defaults/main/main.yml new file mode 100644 index 0000000..3cd1b54 --- /dev/null +++ b/roles/pretix/defaults/main/main.yml @@ -0,0 +1,14 @@ +--- +pretix_version: "2025.7.1" +pretix_state: "present" +pretix_deployment_method: "systemd" + +pretix_config_file: "/etc/pretix/pretix.cfg" +pretix_config_file_owner: "{{ pretix_user_id }}" +pretix_config_file_group: "{{ pretix_group_id }}" +pretix_config_file_mode: "0640" +pretix_config_dir: "{{ pretix_config_file | dirname }}" +pretix_install_dir: "/var/lib/pretix" +pretix_virtualenv_dir: "{{ pretix_install_dir }}/virtualenv" +pretix_data_dir: "{{ pretix_install_dir }}/data" +pretix_media_dir: "{{ pretix_data_dir }}/media" diff --git a/roles/pretix/defaults/main/system_packages.yml b/roles/pretix/defaults/main/system_packages.yml new file mode 100644 index 0000000..483262a --- /dev/null +++ b/roles/pretix/defaults/main/system_packages.yml @@ -0,0 +1,22 @@ +--- +pretix_debian_packages: + - "git" + - "build-essential" + - "python3-dev" + - "python3-venv" + - "python3" + - "python3-pip" + - "libxml2-dev" + - "libxslt1-dev" + - "libffi-dev" + - "zlib1g-dev" + - "libssl-dev" + - "gettext" + - "libpq-dev" + - "libjpeg-dev" + - "libopenjp2-7-dev" + - "nodejs" + +pretix_packages: + "debian": + "12": "{{ pretix_debian_packages }}" diff --git a/roles/pretix/defaults/main/systemd.yml b/roles/pretix/defaults/main/systemd.yml new file mode 100644 index 0000000..6268d89 --- /dev/null +++ b/roles/pretix/defaults/main/systemd.yml @@ -0,0 +1,22 @@ +--- +pretix_systemd_unit_description: "pretix web service" +pretix_systemd_unit_after: "network.target" +pretix_systemd_unit_file_path: >-2 + /etc/systemd/system/{{ pretix_systemd_service_name }} + +pretix_systemd_service_name: "pretix.service" +pretix_systemd_service_user: "{{ pretix_user }}" +pretix_systemd_service_group: "{{ pretix_user }}" +pretix_systemd_service_environment: + VIRTUAL_ENV: "{{ pretix_virtualenv_dir }}" +pretix_systemd_service_working_directory: "{{ pretix_install_dir }}" +pretix_systemd_service_exec_start: >-2 + {{ pretix_virtualenv_dir }}/bin/gunicorn pretix.wsgi + --name {{ pretix_config_wsgi_name }} + --workers {{ pretix_config_wsgi_workers }} + --max-requests {{ pretix_config_wsgi_max_requests }} + --log-level={{ pretix_config_wsgi_log_level }} + --bind={{ pretix_config_wsgi_bind_addr }} +pretix_systemd_service_restart: "on-failure" + +pretix_systemd_install_wanted_by: "multi-user.target" diff --git a/roles/pretix/defaults/main/user.yml b/roles/pretix/defaults/main/user.yml new file mode 100644 index 0000000..5236d37 --- /dev/null +++ b/roles/pretix/defaults/main/user.yml @@ -0,0 +1,7 @@ +--- +pretix_user: "pretix" +pretix_user_system: true +pretix_user_create_home: false + +pretix_user_id: "{{ pretix_user_info.uid | default(pretix_user) }}" +pretix_group_id: "{{ pretix_user_info.group | default(pretix_user) }}" diff --git a/roles/pretix/defaults/main/virtualenv.yml b/roles/pretix/defaults/main/virtualenv.yml new file mode 100644 index 0000000..7d3a4a2 --- /dev/null +++ b/roles/pretix/defaults/main/virtualenv.yml @@ -0,0 +1,11 @@ +--- +pretix_virtualenv_state: "{{ pretix_state }}" +pretix_virtualenv_packages: + - "pip" + - "setuptools" + - "wheel" + - "gunicorn" + - "pretix=={{ pretix_version }}" + +pretix_virtualenv_site_packages: false +pretix_virtualenv_command: "python3 -m venv" diff --git a/roles/pretix/meta/main.yml b/roles/pretix/meta/main.yml new file mode 100644 index 0000000..bbd0558 --- /dev/null +++ b/roles/pretix/meta/main.yml @@ -0,0 +1,9 @@ +--- +allow_duplicates: true +dependencies: [] +galaxy_info: + role_name: pretix + description: Ansible role to deploy pretix (https://pretix.eu) + galaxy_tags: + - pretix + - ticketing diff --git a/roles/pretix/tasks/check.yml b/roles/pretix/tasks/check.yml new file mode 100644 index 0000000..04088f2 --- /dev/null +++ b/roles/pretix/tasks/check.yml @@ -0,0 +1,14 @@ +--- +- name: Ensure 'pretix_state' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported pretix_state '{{ pretix_state }}'. + Supported states are {{ pretix_states | join(', ') }} + when: pretix_state not in pretix_states + +- name: Ensure 'pretix_deployment_method' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported pretix_state '{{ pretix_deployment_method }}'. + Supported states are {{ pretix_deployment_methods | join(', ') }} + when: pretix_deployment_method not in pretix_deployment_methods diff --git a/roles/pretix/tasks/configure.yml b/roles/pretix/tasks/configure.yml new file mode 100644 index 0000000..830ac69 --- /dev/null +++ b/roles/pretix/tasks/configure.yml @@ -0,0 +1,9 @@ +--- +- name: Ensure configuration file is written + ansible.builtin.copy: + dest: "{{ pretix_config_file }}" + content: "{{ pretix_config_file_content }}" + owner: "{{ pretix_config_file_owner }}" + group: "{{ pretix_config_file_group }}" + mode: "{{ pretix_config_file_mode }}" + when: pretix_state == 'present' diff --git a/roles/pretix/tasks/deploy-systemd.yml b/roles/pretix/tasks/deploy-systemd.yml new file mode 100644 index 0000000..3728718 --- /dev/null +++ b/roles/pretix/tasks/deploy-systemd.yml @@ -0,0 +1,32 @@ +--- +- name: Ensure virtualenv in {{ pretix_virtualenv_dir }} is present + ansible.builtin.pip: + name: "{{ pretix_virtualenv_packages }}" + state: "{{ pretix_virtualenv_state }}" + chdir: "{{ pretix_install_dir }}" + virtualenv: "{{ pretix_virtualenv_dir }}" + virtualenv_command: "{{ pretix_virtualenv_command | default(omit, true) }}" + virtualenv_site_packages: "{{ pretix_virtualenv_site_packages }}" + become: true + become_user: "{{ pretix_user }}" + +# TODO: determine to only do this on a) upgrades or b) initial deployis +- name: Ensure pretix static assets are built + ansible.builtin.command: + cmd: "{{ pretix_virtualenv_dir }}/bin/python -m pretix rebuild" + chdir: "{{ pretix_install_dir }}" + environment: + VIRTUAL_ENV: "{{ pretix_virtualenv_dir }}" + become: true + become_user: "{{ pretix_user }}" + +- name: Ensure pretix systemd service is enabled + ansible.builtin.systemd_service: + name: "{{ pretix_systemd_service_name }}" + enabled: true + when: pretix_state == 'present' + +- name: Ensure pretix systemd service is {{ pretix_state }} + ansible.builtin.systemd_service: + name: "{{ pretix_systemd_service_name }}" + state: "{{ (pretix_state == 'present') | ternary('started', 'stopped') }}" diff --git a/roles/pretix/tasks/deploy.yml b/roles/pretix/tasks/deploy.yml new file mode 100644 index 0000000..e4b340c --- /dev/null +++ b/roles/pretix/tasks/deploy.yml @@ -0,0 +1,5 @@ +--- +- name: Ensure pretix is deployed using {{ pretix_deployment_method }} + ansible.builtin.include_tasks: + file: "deploy-{{ pretix_deployment_method }}.yml" + when: pretix_state == 'present' diff --git a/roles/pretix/tasks/main.yml b/roles/pretix/tasks/main.yml new file mode 100644 index 0000000..1cb4ea5 --- /dev/null +++ b/roles/pretix/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: Ensure preconditions are met + ansible.builtin.include_tasks: + file: "check.yml" + +- name: Ensure deployment preparations are done + ansible.builtin.include_tasks: + file: "prepare.yml" + +- name: Ensure pretix is configured + ansible.builtin.include_tasks: + file: "configure.yml" + +- name: Ensure pretix is deployed + ansible.builtin.include_tasks: + file: "deploy.yml" diff --git a/roles/pretix/tasks/prepare-systemd.yml b/roles/pretix/tasks/prepare-systemd.yml new file mode 100644 index 0000000..61e7bba --- /dev/null +++ b/roles/pretix/tasks/prepare-systemd.yml @@ -0,0 +1,30 @@ +--- +- name: Ensure ansible facts are collected + ansible.builtin.setup: + gather_subset: + - "!all" + - "pkg_mgr" + - "distribution" + - "distribution_release" + - "distribution_version" + - "distribution_major_version" + +- name: Ensure system packages are present (apt) + ansible.builtin.apt: + name: "{{ package }}" + state: "{{ pretix_state }}" + loop: "{{ pretix_packages[ansible_distribution | lower][ansible_distribution_major_version] }}" + loop_control: + loop_var: "package" + when: ansible_facts['pkg_mgr'] == 'apt' + +- name: Ensure systemd unit is present + ansible.builtin.template: + src: "pretix.service.j2" + dest: "{{ pretix_systemd_unit_file_path }}" + register: pretix_systemd_unit_info + +- name: Ensure systemd is reloaded + ansible.builtin.systemd_service: + daemon_reload: true + when: pretix_systemd_unit_info.changed diff --git a/roles/pretix/tasks/prepare.yml b/roles/pretix/tasks/prepare.yml new file mode 100644 index 0000000..3bea8cc --- /dev/null +++ b/roles/pretix/tasks/prepare.yml @@ -0,0 +1,29 @@ +--- +- name: Ensure pretix user '{{ pretix_user }}' is {{ pretix_state }} + ansible.builtin.user: + name: "{{ pretix_user }}" + state: "{{ pretix_state }}" + system: "{{ pretix_user_system }}" + create_home: "{{ pretix_user_create_home }}" + register: pretix_user_info + +- name: Ensure host directories are {{ pretix_state }} + ansible.builtin.file: + path: "{{ item.path }}" + owner: "{{ item.owner | default(pretix_user_id) }}" + group: "{{ item.group | default(pretix_group_id) }}" + mode: "{{ item.mode | default('0750') }}" + state: "directory" + loop: + - path: "{{ pretix_config_dir }}" + - path: "{{ pretix_virtualenv_dir }}" + - path: "{{ pretix_data_dir }}" + - path: "{{ pretix_media_dir }}" + when: pretix_state == 'present' + +- name: Ensure deployment-type specific preparations for '{{ pretix_deployment_method }}' are run + ansible.builtin.include_tasks: + file: "prepare-{{ pretix_deployment_method }}.yml" + when: + - pretix_state == 'present' + - pretix_deployment_method in ['systemd'] diff --git a/roles/pretix/templates/pretix.service.j2 b/roles/pretix/templates/pretix.service.j2 new file mode 100644 index 0000000..c4cfe08 --- /dev/null +++ b/roles/pretix/templates/pretix.service.j2 @@ -0,0 +1,16 @@ +[Unit] +Description={{ pretix_systemd_unit_description }} +After={{ pretix_systemd_unit_after }} + +[Service] +User={{ pretix_systemd_service_user }} +Group={{ pretix_systemd_service_group }} +{% for kv in pretix_systemd_service_environment | dict2items %} +Environment="{{ kv.key }}={{ kv.value }}" +{% endfor %} +WorkingDirectory={{ pretix_systemd_service_working_directory }} +ExecStart={{ pretix_systemd_service_exec_start }} +Restart={{ pretix_systemd_service_restart }} + +[Install] +WantedBy={{ pretix_systemd_install_wanted_by }} diff --git a/roles/pretix/vars/main.yml b/roles/pretix/vars/main.yml new file mode 100644 index 0000000..464f8aa --- /dev/null +++ b/roles/pretix/vars/main.yml @@ -0,0 +1,7 @@ +--- +pretix_states: + - "present" + - "absent" + +pretix_deployment_methods: + - "systemd"