--- - name: Install Gyro runtime packages ansible.builtin.apt: name: - ca-certificates - git - openssh-client - python3 - python3-packaging - python3-venv - sudo - ufw state: present update_cache: true - name: Configure Gyro timezone community.general.timezone: name: "{{ gyro_timezone }}" - name: Create Gyro service group ansible.builtin.group: name: "{{ gyro_group }}" system: true - name: Create Gyro service user ansible.builtin.user: name: "{{ gyro_user }}" group: "{{ gyro_group }}" home: "{{ gyro_home }}" shell: /bin/bash system: true create_home: true - name: Create Gyro directories ansible.builtin.file: path: "{{ item.path }}" state: directory owner: "{{ item.owner }}" group: "{{ item.group }}" mode: "{{ item.mode }}" loop: - { path: "{{ gyro_app_dir }}", owner: "{{ gyro_user }}", group: "{{ gyro_group }}", mode: "0750" } - { path: "{{ gyro_config_dir }}", owner: root, group: root, mode: "0700" } - { path: "{{ gyro_cache_dir }}", owner: "{{ gyro_user }}", group: "{{ gyro_group }}", mode: "0700" } - { path: "{{ gyro_home }}/.ssh", owner: "{{ gyro_user }}", group: "{{ gyro_group }}", mode: "0700" } - name: Read system Python version ansible.builtin.command: python3 -c "import platform; print(platform.python_version())" register: gyro_python_version changed_when: false - name: Require Python 3.13 or newer ansible.builtin.assert: that: - gyro_python_version.stdout is version(gyro_python_min_version, '>=') fail_msg: "Gyro requires Python {{ gyro_python_min_version }} or newer; found {{ gyro_python_version.stdout }}." - name: Create isolated uv installation environment ansible.builtin.command: argv: - python3 - -m - venv - "{{ gyro_uv_venv }}" creates: "{{ gyro_uv_venv }}/bin/pip" - name: Install pinned uv version ansible.builtin.pip: name: "uv=={{ gyro_uv_version }}" executable: "{{ gyro_uv_venv }}/bin/pip" - name: Link uv into the system path ansible.builtin.file: src: "{{ gyro_uv_venv }}/bin/uv" dest: /usr/local/bin/uv state: link - name: Generate Git deploy key on the container ansible.builtin.command: argv: - ssh-keygen - -q - -t - ed25519 - -N - "" - -C - gyro@homelab - -f - "{{ gyro_home }}/.ssh/id_ed25519_gitea" creates: "{{ gyro_home }}/.ssh/id_ed25519_gitea" become: true become_user: "{{ gyro_user }}" vars: ansible_become: true - name: Secure Git deploy key ownership ansible.builtin.file: path: "{{ item.path }}" owner: "{{ gyro_user }}" group: "{{ gyro_group }}" mode: "{{ item.mode }}" loop: - { path: "{{ gyro_home }}/.ssh/id_ed25519_gitea", mode: "0600" } - { path: "{{ gyro_home }}/.ssh/id_ed25519_gitea.pub", mode: "0644" } - name: Read Git deploy public key ansible.builtin.slurp: src: "{{ gyro_home }}/.ssh/id_ed25519_gitea.pub" register: gyro_deploy_public_key - name: Show Git deploy public key ansible.builtin.debug: msg: "{{ gyro_deploy_public_key.content | b64decode | trim }}" - name: Allow SSH from the HomeLab LAN community.general.ufw: rule: allow port: "22" proto: tcp src: "{{ homelab_lan_cidr }}" - name: Allow SSH from the OpenVPN network community.general.ufw: rule: allow port: "22" proto: tcp src: "{{ openvpn_network_cidr }}" - name: Remove obsolete outbound Gitea SSH allowance community.general.ufw: rule: allow delete: true direction: out dest: 192.168.1.25 port: "2222" proto: tcp - name: Allow outbound Telegram proxy access community.general.ufw: rule: allow direction: out dest: 192.168.1.27 port: "7890" proto: tcp - name: Deny other outbound HomeLab LAN access community.general.ufw: rule: deny direction: out dest: "{{ homelab_lan_cidr }}" - name: Enable restrictive Gyro firewall community.general.ufw: state: enabled policy: deny direction: incoming - name: Mask mount units already provided by unprivileged LXC ansible.builtin.systemd: name: "{{ item }}" enabled: false masked: true state: stopped loop: - dev-mqueue.mount - run-lock.mount - tmp.mount - name: Clear stale failures from masked LXC mount units ansible.builtin.command: >- systemctl reset-failed dev-mqueue.mount run-lock.mount tmp.mount changed_when: false - name: Create locked placeholder environment file ansible.builtin.copy: dest: "{{ gyro_config_dir }}/gyro.env" owner: root group: root mode: "0600" force: false content: | # Managed by Ansible after gyro_secrets_configured is enabled. DRY_RUN_OVERRIDE=true - name: Validate deployment inputs ansible.builtin.assert: that: - gyro_repo_url | length > 0 - gyro_git_known_hosts_name | length > 0 - gyro_git_host_key | length > 0 fail_msg: Set the repository URL and verified SSH host key before enabling deployment. when: gyro_deploy_enabled | bool - name: Remove obsolete Git SSH alias ansible.builtin.file: path: "{{ gyro_home }}/.ssh/config" state: absent when: gyro_deploy_enabled | bool - name: Remove obsolete Gitea known host ansible.builtin.known_hosts: path: "{{ gyro_home }}/.ssh/known_hosts" name: "[192.168.1.25]:2222" state: absent when: gyro_deploy_enabled | bool - name: Pin verified Git host key ansible.builtin.known_hosts: path: "{{ gyro_home }}/.ssh/known_hosts" name: "{{ gyro_git_known_hosts_name }}" key: "{{ gyro_git_host_key }}" when: gyro_deploy_enabled | bool - name: Secure Git known hosts file ansible.builtin.file: path: "{{ gyro_home }}/.ssh/known_hosts" owner: "{{ gyro_user }}" group: "{{ gyro_group }}" mode: "0600" when: gyro_deploy_enabled | bool - name: Ensure Gyro checkout belongs to the service user ansible.builtin.file: path: "{{ gyro_app_dir }}" owner: "{{ gyro_user }}" group: "{{ gyro_group }}" recurse: true when: gyro_deploy_enabled | bool - name: Clone Gyro from Git remote ansible.builtin.git: repo: "{{ gyro_repo_url }}" dest: "{{ gyro_app_dir }}" version: "{{ gyro_repo_version }}" key_file: "{{ gyro_home }}/.ssh/id_ed25519_gitea" accept_hostkey: false ssh_opts: >- -o UserKnownHostsFile={{ gyro_home }}/.ssh/known_hosts -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes update: true become: true become_user: "{{ gyro_user }}" vars: ansible_become: true when: gyro_deploy_enabled | bool - name: Synchronize locked Gyro dependencies ansible.builtin.command: argv: - /usr/local/bin/uv - sync - --frozen args: chdir: "{{ gyro_app_dir }}" environment: UV_CACHE_DIR: "{{ gyro_cache_dir }}/uv" become: true become_user: "{{ gyro_user }}" vars: ansible_become: true register: gyro_uv_sync changed_when: "'Installed' in gyro_uv_sync.stderr or 'Uninstalled' in gyro_uv_sync.stderr" when: gyro_deploy_enabled | bool - name: Verify bundled T-Invest CA file ansible.builtin.stat: path: "{{ gyro_app_dir }}/config/certs/russian_ca.pem" register: gyro_ca_bundle when: gyro_deploy_enabled | bool - name: Require complete Gyro checkout ansible.builtin.assert: that: - gyro_ca_bundle.stat.exists - gyro_ca_bundle.stat.isreg | default(false) fail_msg: The Git checkout does not contain config/certs/russian_ca.pem. when: gyro_deploy_enabled | bool - name: Run Gyro unit tests ansible.builtin.command: argv: - /usr/local/bin/uv - run - --frozen - --no-sync - python - -m - unittest - discover - -s - tests args: chdir: "{{ gyro_app_dir }}" environment: UV_CACHE_DIR: "{{ gyro_cache_dir }}/uv" become: true become_user: "{{ gyro_user }}" vars: ansible_become: true changed_when: false when: gyro_deploy_enabled | bool - name: Validate Gyro Vault secrets ansible.builtin.assert: that: - gyro_tinvest_token | length > 0 - gyro_tinvest_account_id | length > 0 - gyro_telegram_bot_token | length > 0 - (gyro_telegram_user_id | string | length) > 0 - gyro_dry_run_override in ['true', 'false'] fail_msg: Populate and encrypt inventory/host_vars/gyro/vault.yml before enabling secrets. no_log: true when: gyro_secrets_configured | bool - name: Install Gyro environment file from Vault ansible.builtin.template: src: gyro.env.j2 dest: "{{ gyro_config_dir }}/gyro.env" owner: root group: root mode: "0600" no_log: true when: gyro_secrets_configured | bool - name: Install Gyro failure notifier ansible.builtin.copy: src: gyro-failure-notify.py dest: /usr/local/libexec/gyro-failure-notify owner: root group: root mode: "0755" - name: Install Gyro systemd units ansible.builtin.template: src: "{{ item.src }}" dest: "/etc/systemd/system/{{ item.dest }}" owner: root group: root mode: "0644" loop: - { src: gyro.service.j2, dest: gyro.service } - { src: gyro.timer.j2, dest: gyro.timer } - { src: gyro-failure@.service.j2, dest: "gyro-failure@.service" } notify: Reload systemd - name: Apply systemd unit changes ansible.builtin.meta: flush_handlers - name: Enable Gyro timer only after deployment and secret setup ansible.builtin.systemd: name: gyro.timer enabled: "{{ gyro_timer_ready }}" state: "{{ 'started' if gyro_timer_ready else 'stopped' }}" vars: gyro_timer_ready: "{{ gyro_timer_enabled | bool and gyro_deploy_enabled | bool and gyro_secrets_configured | bool }}" - name: Verify Gyro unit definitions ansible.builtin.command: >- systemd-analyze verify /etc/systemd/system/gyro.service /etc/systemd/system/gyro.timer /etc/systemd/system/gyro-failure@.service changed_when: false