Role for Python applications/services.
In order to be compatible with this role, a project must follow these rules:
- It must be available as a Git repository that is visible from the host.
- The repository directory must be pip-installable using
pip -e REPO_PATH. - At the top level, there must be a file named
pinned-requirements.txtinstallable withpip -rwhich contains versions for all dependencies.- You may generate this file from a
pyproject.toml,setup.pyorrequirements.txtfile with thepip-compileutility in thepip-toolspackage. Do not forget to commit it and update it whenever there are changes.
- You may generate this file from a
By default, the pyservice role will create a user, and the following hierarchy:
# Root for all pyservice-installed applications, owner is root
/applications # Variable: {{ pyservice_global_root }}
# uv binary is installed here
/uv # Variable: {{ pyservice_uv_location }}
# App directory; owner is the the app user
/APP_NAME # Variable: {{ pyservice_root }}
# Application code and configuration
/app # Variable: {{ pyservice_dir }}
# The repository is cloned here
/code # Variable: {{ pyservice_code_dir }}
# Configuration files, certificates, keys, etc.
/config # Variable: {{ pyservice_config_dir }}
# Default configuration file, provided for convenience but not mandatory
config.yaml # Variable: {{ pyservice_config }}
# EnvironmentFile used by the systemd services
env # Variable: {{ pyservice_env }}
# Application data (anything that must be backed up)
/data # Variable: {{ pyservice_code_dir }}
# Virtual environment
/uv
/venv
/etc
/systemd
/system
# Service files defined in {{ pyservice_services }}
APP_NAME-xyz.service
...
# Timer files for recurrent work, defined in {{ pyservice_timers }}
APP_NAME-uvw.service
APP_NAME-uvw.timer
...Installation proceeds as follows:
setuptasks- Deactivate all services related to the app and delete their files
- Clone the repository
- Install uv
- Optionally write files as defined in
pyservice_files - (Do custom setup here)
activatetasks- Write the service files and activate them
The application's work must be defined in two lists:
pyservice_servicesfor services that must be up at all times, such as a web server or an APIpyservice_timersfor recurrent work, such as scraping information or generating a report every day or week
The following variables can/must be set in the inventory:
pyservice_name: The name of the application.pyservice_module: The name of the Python module defined by the application. Defaults to{{ pyservice_name }}.pyservice_user: The user under which the app should be run and which owns the configuration and data.- Set
pyservice_ensure_usertofalsein order not to do this, if you want to control user creation.
- Set
pyservice_group: The group to create the various directories under. Defaults to{{ ansible_common_remote_group | default(pyservice_user) }}.pyservice_repo: Path to the application repository.- If the repo is private, you should set
pyservice_ssh_keyto a private key that is authorized to read the repo (use the SSH URL for the repo), or setpyservice_ssh_key_pathto the path to the proper file on the host.
- If the repo is private, you should set
pyservice_tag: Tag or branch of thepyservice_repoto checkout.pyservice_uv_version: UV version to use. See the list here. Minimum allowed version should be 0.4.9 (that's the version in whichuv self update {{ pyservice_uv_version }}was added).
The pyservice_services variable should contain a list of services. Each service has a name, description and associated command:
pyservice_services:
- name: web
description: "Run the web interface for {{ pyservice_name }}"
command: python -m {{ pyservice_module }} --config {{ pyservice_config }} webThe command is run in the environment created by the role, and it can be anything you want.
The pyservice_timers variable should contain a list of timers. Each timer has a name, description, schedule and associated command:
pyservice_timers:
- name: something
description: "Do something every Monday at 2 AM"
schedule: "Mon, 02:00"
command: python -m {{ pyservice_module }} --config {{ pyservice_config }} do_somethingThe syntax for the schedule is described here in section 4.2. It ends up in an OnCalendar declaration, so you can look at the examples for that.
The command is run in the environment created by the role, and it can be anything you want.
You can easily write files based on content that is in various variables:
pyservice_files:
- dest: "{{ pyservice_config_dir }}/certificate"
content: "{{ certificate_contents }}"
mode: "0600" # default mode is also 0600pyservice_global_root: The path where all applications will be installed into subdirectories (defaults to/applications)pyservice_root: The path on the target system in which to put all the code, configuration and data. Defaults to{{ pyservice_global_root }}/{{ pyservice_name }}pyservice_code_dir: Path to the cloned repository.pyservice_data_dir: The path on the target system in which to put application data. Defaults to{{ pyservice_root }}/datapyservice_dir: The path on the target system in which to put all the code and configuration. Defaults to{{ pyservice_root }}/apppyservice_config_dir: The path on the target system in which to put configuration. Defaults to{{ pyservice_dir }}/config.pyservice_config: Path to the configuration file (defaults to{{ pyservice_config_dir }}/config.yaml)pyservice_uv_location: The path where to put the uv binary (defaults to{{ pyservice_global_root }}/bin)pyservice_uv_isolate: If true, uv will be put underpyservice_dir(defaults to false). It's pointless to change this if you changepyservice_uv_location.pyservice_run: Put that in front of a shell command to run it in the app's environment.
The typical playbook should run the setup tasks, then some custom operations, typically templating a configuration file, then run the activate tasks. The pyservice_services, pyservice_timers and pyservice_files variables can be defined in the playbook since it makes little sense to change them in the inventory, except possibly for the timer schedules.
---
- hosts: all
vars:
pyservice_services:
- name: web
description: "Run the web interface for {{ pyservice_name }}"
command: python -m {{ pyservice_module }} --config {{ pyservice_config }} web
pyservice_timers:
- name: something
description: "Do something every Monday at 2 AM"
schedule: "Mon, 02:00"
command: python -m {{ pyservice_module }} --config {{ pyservice_config }} do_something
pyservice_files:
- dest: "{{ pyservice_config_dir }}/certificate"
content: "{{ certificate_contents }}"
mode: "0600"
tasks:
- name: Install
import_role:
name: pyservice
tasks_from: setup
- name: Template configuration
ansible.builtin.template:
src: "config.yaml"
dest: "{{ pyservice_config }}"
owner: "{{ pyservice_user }}"
mode: "0600"
- name: Activate
import_role:
name: pyservice
tasks_from: activate