Capture current Ansible control plane state
Commit the accumulated infrastructure work that was living only in the working tree: monitoring stack, emergency access/bot, gyro allocator, grimmory, adguard, backup audit and the OpenCode agent definitions. Also ignore Python bytecode, local archives and Nix/direnv artifacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
---
|
||||
- name: Create and verify current PBS audit before AdGuard update
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Verify AdGuard VMID ownership before backup
|
||||
ansible.builtin.command: pct config 144
|
||||
register: adguard_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to back up a foreign VMID 144
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- adguard_pct_config.rc == 0
|
||||
- adguard_existing_hostname == 'adguard'
|
||||
fail_msg: VMID 144 is not the AdGuard container.
|
||||
vars:
|
||||
adguard_existing_hostname: >-
|
||||
{{ adguard_pct_config.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Check for active Proxmox backup before AdGuard PBS backup
|
||||
ansible.builtin.command: pgrep -x vzdump
|
||||
register: adguard_vzdump_preflight
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Require no active Proxmox backup before AdGuard PBS backup
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- adguard_vzdump_preflight.rc != 0
|
||||
fail_msg: >-
|
||||
A Proxmox backup is already running on mini-pc.
|
||||
Retry after the existing backup completes.
|
||||
|
||||
- name: Create a fresh AdGuard PBS backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- vzdump
|
||||
- "144"
|
||||
- --storage
|
||||
- pbs
|
||||
- --mode
|
||||
- snapshot
|
||||
- --prune-backups
|
||||
- keep-all=1
|
||||
- --exclude-path
|
||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||
|
||||
- name: Run current PBS audit service
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-pbs.service
|
||||
changed_when: true
|
||||
|
||||
- import_playbook: pve-adguard.yml
|
||||
|
||||
- name: Verify AdGuard public services after update
|
||||
hosts: adguard
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Check AdGuard HTTP root
|
||||
ansible.builtin.uri:
|
||||
url: http://127.0.0.1/
|
||||
status_code: [200, 302]
|
||||
return_content: false
|
||||
register: adguard_http_root_check
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: adguard_http_root_check.status in [200, 302]
|
||||
|
||||
- name: Check AdGuard DNS over UDP
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- dig
|
||||
- "@127.0.0.1"
|
||||
- localhost
|
||||
- A
|
||||
- +time=2
|
||||
- +tries=1
|
||||
- +short
|
||||
register: adguard_dns_udp_check
|
||||
changed_when: false
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: adguard_dns_udp_check.rc == 0 and '127.0.0.1' in adguard_dns_udp_check.stdout
|
||||
|
||||
- name: Check AdGuard DNS over TCP
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- dig
|
||||
- "@127.0.0.1"
|
||||
- localhost
|
||||
- A
|
||||
- +tcp
|
||||
- +time=2
|
||||
- +tries=1
|
||||
- +short
|
||||
register: adguard_dns_tcp_check
|
||||
changed_when: false
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: adguard_dns_tcp_check.rc == 0 and '127.0.0.1' in adguard_dns_tcp_check.stdout
|
||||
@@ -0,0 +1,45 @@
|
||||
- name: Configure PBS backup audit on mini-pc
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: backup_audit
|
||||
vars:
|
||||
backup_audit_type: pbs
|
||||
|
||||
- name: Configure restic offsite audit on cloud-pc (Gitea)
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: backup_audit
|
||||
vars:
|
||||
backup_audit_type: restic
|
||||
backup_audit_restic_profiles:
|
||||
- name: gitea
|
||||
sqlite_name: gitea.db
|
||||
max_age_hours: 36
|
||||
|
||||
- name: Configure restic offsite audit on vaultwarden
|
||||
hosts: vaultwarden
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: backup_audit
|
||||
vars:
|
||||
backup_audit_type: restic
|
||||
backup_audit_restic_profiles:
|
||||
- name: vaultwarden
|
||||
sqlite_name: db.sqlite3
|
||||
max_age_hours: 36
|
||||
|
||||
- name: Configure restic offsite audit on Grimmory
|
||||
hosts: grimmory
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
roles:
|
||||
- role: backup_audit
|
||||
vars:
|
||||
backup_audit_type: restic
|
||||
backup_audit_restic_profiles:
|
||||
- name: grimmory
|
||||
expected_name: grimmory.sql
|
||||
max_age_hours: 36
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
- name: Create read-only Proxmox token for monitoring
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
monitoring_pve_user: monitoring@pve
|
||||
monitoring_pve_token_id: prometheus
|
||||
monitoring_pve_env_file: "{{ playbook_dir }}/../.env"
|
||||
monitoring_pve_rotate_existing_token: false
|
||||
tasks:
|
||||
- name: Read existing Proxmox users
|
||||
ansible.builtin.command: pveum user list --output-format json
|
||||
register: monitoring_pve_users_raw
|
||||
changed_when: false
|
||||
|
||||
- name: Create monitoring Proxmox user
|
||||
ansible.builtin.command: >-
|
||||
pveum user add {{ monitoring_pve_user }}
|
||||
--comment 'Read-only Prometheus monitoring user'
|
||||
when: monitoring_pve_user not in (monitoring_pve_users_raw.stdout | from_json | map(attribute='userid') | list)
|
||||
|
||||
- name: Grant PVEAuditor role to monitoring user
|
||||
ansible.builtin.command: >-
|
||||
pveum acl modify / -user {{ monitoring_pve_user }} -role PVEAuditor
|
||||
changed_when: false
|
||||
|
||||
- name: Read monitoring user tokens
|
||||
ansible.builtin.command: >-
|
||||
pveum user token list {{ monitoring_pve_user }} --output-format json
|
||||
register: monitoring_pve_tokens_raw
|
||||
changed_when: false
|
||||
|
||||
- name: Refuse to overwrite an existing monitoring token
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- monitoring_pve_token_id not in (monitoring_pve_tokens_raw.stdout | from_json | map(attribute='tokenid') | list)
|
||||
fail_msg: Existing monitoring token secret cannot be recovered safely. Rotate it explicitly before rerunning this playbook.
|
||||
when: not monitoring_pve_rotate_existing_token | bool
|
||||
|
||||
- name: Rotate existing monitoring token explicitly
|
||||
ansible.builtin.command: >-
|
||||
pveum user token remove {{ monitoring_pve_user }} {{ monitoring_pve_token_id }}
|
||||
when:
|
||||
- monitoring_pve_rotate_existing_token | bool
|
||||
- monitoring_pve_token_id in (monitoring_pve_tokens_raw.stdout | from_json | map(attribute='tokenid') | list)
|
||||
|
||||
- name: Create separated monitoring token
|
||||
ansible.builtin.command: >-
|
||||
pveum user token add {{ monitoring_pve_user }} {{ monitoring_pve_token_id }}
|
||||
--privsep 1 --comment 'Prometheus PVE exporter' --output-format json
|
||||
register: monitoring_pve_token_created
|
||||
no_log: true
|
||||
|
||||
- name: Grant PVEAuditor role to separated monitoring token
|
||||
ansible.builtin.command: >-
|
||||
pveum acl modify / -token {{ monitoring_pve_user }}!{{ monitoring_pve_token_id }} -role PVEAuditor
|
||||
changed_when: false
|
||||
|
||||
- name: Store monitoring token variables locally
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ monitoring_pve_env_file }}"
|
||||
regexp: "^export {{ item.name }}="
|
||||
line: "export {{ item.name }}='{{ item.value }}'"
|
||||
create: false
|
||||
loop:
|
||||
- name: MONITORING_PVE_API_USER
|
||||
value: "{{ monitoring_pve_user }}"
|
||||
- name: MONITORING_PVE_API_TOKEN_ID
|
||||
value: "{{ monitoring_pve_token_id }}"
|
||||
- name: MONITORING_PVE_API_TOKEN_SECRET
|
||||
value: "{{ (monitoring_pve_token_created.stdout | from_json).value }}"
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
no_log: true
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
- name: Bootstrap emergency-bot control identity
|
||||
hosts: emergency-bot
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Create emergency-bot control identity
|
||||
ansible.builtin.include_role:
|
||||
name: emergency_bot
|
||||
tasks_from: bootstrap
|
||||
|
||||
- name: Configure mini-pc reverse SSH client and restricted control path
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
emergency_vps_host_key: "{{ lookup('env', 'EMERGENCY_VPS_HOST_KEY') }}"
|
||||
emergency_bot_control_public_key: "{{ hostvars['emergency-bot'].emergency_bot_control_public_key }}"
|
||||
tasks:
|
||||
- name: Configure mini-pc emergency access client
|
||||
ansible.builtin.include_role:
|
||||
name: emergency_access
|
||||
tasks_from: client
|
||||
|
||||
- name: Configure ru-vps reverse SSH endpoint
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
vars:
|
||||
emergency_reverse_public_key: "{{ hostvars['mini-pc'].emergency_reverse_public_key }}"
|
||||
tasks:
|
||||
- name: Configure ru-vps emergency access endpoint
|
||||
ansible.builtin.include_role:
|
||||
name: emergency_access
|
||||
tasks_from: endpoint
|
||||
|
||||
- name: Configure and enable emergency Telegram bot
|
||||
hosts: emergency-bot
|
||||
gather_facts: false
|
||||
vars:
|
||||
emergency_bot_token: "{{ lookup('env', 'EMERGENCY_BOT_TOKEN') }}"
|
||||
emergency_bot_allowed_user_ids: "{{ lookup('env', 'EMERGENCY_ALLOWED_USER_IDS') }}"
|
||||
emergency_mini_pc_host_key: "{{ lookup('env', 'EMERGENCY_MINI_PC_HOST_KEY') }}"
|
||||
roles:
|
||||
- role: emergency_bot
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
- name: Create and verify Gitea backup before update
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Create a fresh Gitea offsite backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-restic-offsite-gitea.service
|
||||
changed_when: true
|
||||
|
||||
- name: Run Gitea offsite backup audit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-gitea.service
|
||||
changed_when: true
|
||||
|
||||
- import_playbook: pve-gitea.yml
|
||||
|
||||
- name: Verify Gitea public endpoints after update
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Check Gitea HTTPS endpoint
|
||||
ansible.builtin.uri:
|
||||
url: https://git.ada-dev.ru/
|
||||
status_code: 200
|
||||
return_content: false
|
||||
register: gitea_https_check
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: gitea_https_check.status == 200
|
||||
|
||||
- name: Check Gitea SSH port from ru-vps
|
||||
ansible.builtin.wait_for:
|
||||
host: 192.168.1.25
|
||||
port: 2222
|
||||
state: started
|
||||
timeout: 30
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
- name: Create and verify Grimmory backup before update
|
||||
hosts: grimmory
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Create a fresh Grimmory offsite backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-restic-offsite-grimmory.service
|
||||
changed_when: true
|
||||
|
||||
- name: Run Grimmory offsite backup audit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-grimmory.service
|
||||
changed_when: true
|
||||
|
||||
- name: Create a fresh Grimmory PBS backup
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read Grimmory LXC config
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- config
|
||||
- "149"
|
||||
register: grimmory_pct_config
|
||||
changed_when: false
|
||||
|
||||
- name: Assert VMID 149 belongs to Grimmory
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- grimmory_pct_hostname_line != ""
|
||||
- grimmory_pct_hostname == "grimmory"
|
||||
fail_msg: >-
|
||||
Refusing to run vzdump 149 because pct config hostname is not grimmory:
|
||||
{{ grimmory_pct_hostname_line | default('missing hostname line') }}
|
||||
vars:
|
||||
grimmory_pct_hostname_line: >-
|
||||
{{ (grimmory_pct_config.stdout_lines | select('match', '^hostname:\\s+') | list | first | default('')) }}
|
||||
grimmory_pct_hostname: >-
|
||||
{{ grimmory_pct_hostname_line | regex_replace('^hostname:\\s*', '') }}
|
||||
|
||||
- name: Check for active Proxmox backup before Grimmory PBS backup
|
||||
ansible.builtin.command: pgrep -x vzdump
|
||||
register: grimmory_vzdump_preflight
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Require no active Proxmox backup before Grimmory PBS backup
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- grimmory_vzdump_preflight.rc != 0
|
||||
fail_msg: >-
|
||||
A Proxmox backup is already running on cloud-pc.
|
||||
Retry after the existing backup completes.
|
||||
|
||||
- name: Create a fresh Grimmory PBS backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- vzdump
|
||||
- "149"
|
||||
- --storage
|
||||
- pbs
|
||||
- --mode
|
||||
- snapshot
|
||||
- --prune-backups
|
||||
- keep-all=1
|
||||
- --exclude-path
|
||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||
|
||||
- name: Run PBS backup audit on mini-pc
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Run current PBS backup audit on mini-pc
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-pbs.service
|
||||
changed_when: true
|
||||
|
||||
- import_playbook: pve-grimmory.yml
|
||||
|
||||
- name: Verify Grimmory public endpoint after update
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Check Grimmory public health endpoint
|
||||
ansible.builtin.uri:
|
||||
url: https://books.ada-dev.ru/api/v1/healthcheck
|
||||
status_code: 200
|
||||
return_content: false
|
||||
register: grimmory_public_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: grimmory_public_health.status == 200
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Configure Gyro investment allocator host
|
||||
hosts: gyro
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: gyro
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
- name: Verify Mihomo PBS audit before update
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read VMID 143 configuration
|
||||
ansible.builtin.command: "pct config 143"
|
||||
register: mihomo_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to run backup unless VMID 143 is Mihomo
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mihomo_pct_config.rc == 0
|
||||
- mihomo_update_hostname == 'mihomo'
|
||||
fail_msg: VMID 143 must be the Mihomo container before backup.
|
||||
vars:
|
||||
mihomo_update_hostname: >-
|
||||
{{ mihomo_pct_config.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Check for active Proxmox backup before Mihomo PBS backup
|
||||
ansible.builtin.command: pgrep -x vzdump
|
||||
register: mihomo_vzdump_preflight
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Require no active Proxmox backup before Mihomo PBS backup
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mihomo_vzdump_preflight.rc != 0
|
||||
fail_msg: >-
|
||||
A Proxmox backup is already running on mini-pc.
|
||||
Retry after the existing backup completes.
|
||||
|
||||
- name: Create a fresh Mihomo PBS backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- vzdump
|
||||
- "143"
|
||||
- --storage
|
||||
- pbs
|
||||
- --mode
|
||||
- snapshot
|
||||
- --prune-backups
|
||||
- keep-all=1
|
||||
- --exclude-path
|
||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||
|
||||
- name: Run current PBS backup audit on mini-pc
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-pbs.service
|
||||
changed_when: true
|
||||
|
||||
- import_playbook: pve-mihomo.yml
|
||||
|
||||
- name: Verify Mihomo after update
|
||||
hosts: mihomo
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Wait for Mihomo proxy TCP ports
|
||||
ansible.builtin.wait_for:
|
||||
host: 127.0.0.1
|
||||
port: "{{ item }}"
|
||||
state: started
|
||||
timeout: 120
|
||||
loop:
|
||||
- 7890
|
||||
- 7891
|
||||
- 9090
|
||||
|
||||
- name: Check Mihomo UI endpoint
|
||||
ansible.builtin.uri:
|
||||
url: http://127.0.0.1:8080/
|
||||
status_code: 200
|
||||
register: mihomo_ui_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: mihomo_ui_health.status == 200
|
||||
|
||||
- name: Check Mihomo controller /version endpoint
|
||||
ansible.builtin.uri:
|
||||
url: http://127.0.0.1:9090/version
|
||||
status_code: 200
|
||||
register: mihomo_controller_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: mihomo_controller_health.status == 200
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
- name: Configure Prometheus exporters
|
||||
hosts: monitoring_exporters
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: monitoring_exporter
|
||||
|
||||
- name: Configure SMART exporters on Proxmox nodes
|
||||
hosts: monitoring_smart_exporters
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: monitoring_exporter
|
||||
monitoring_exporter_install_node: false
|
||||
monitoring_exporter_install_smart: true
|
||||
|
||||
- name: Configure ru-vps external probes
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: monitoring_blackbox
|
||||
|
||||
- name: Configure central monitoring stack
|
||||
hosts: monitoring_server
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: monitoring_server
|
||||
@@ -0,0 +1,74 @@
|
||||
- name: Configure Gitea offsite backup to Yandex Disk
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
offsite_profile: gitea
|
||||
offsite_repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea
|
||||
offsite_source_path: /opt/data/gitea
|
||||
offsite_sqlite_db: /opt/data/gitea/gitea/gitea.db
|
||||
offsite_backup_tag: gitea,cloud-pc,yadisk
|
||||
offsite_timer_oncalendar: "*-*-* 04:15:00"
|
||||
offsite_rclone_config_local: ~/.config/rclone/rclone.conf
|
||||
offsite_restic_password_local: "{{ playbook_dir }}/../generated/restic-offsite-password"
|
||||
offsite_excludes:
|
||||
- /opt/data/gitea/gitea/gitea.db
|
||||
- /opt/data/gitea/gitea/gitea.db-shm
|
||||
- /opt/data/gitea/gitea/gitea.db-wal
|
||||
- /opt/data/gitea/gitea/log/**
|
||||
- /opt/data/gitea/gitea/sessions/**
|
||||
- /opt/data/gitea/gitea/queues/**
|
||||
- /opt/data/gitea/gitea/tmp/**
|
||||
tasks:
|
||||
- name: Configure restic offsite profile
|
||||
ansible.builtin.include_tasks: ../tasks/offsite-restic-profile.yml
|
||||
|
||||
- name: Configure Vaultwarden offsite backup to Yandex Disk
|
||||
hosts: vaultwarden
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
offsite_profile: vaultwarden
|
||||
offsite_repository: rclone:yadisk:System/Backups/HomeLab/restic/vaultwarden
|
||||
offsite_source_path: /opt/vaultwarden/data
|
||||
offsite_sqlite_db: /opt/vaultwarden/data/db.sqlite3
|
||||
offsite_backup_tag: vaultwarden,mini-pc,yadisk
|
||||
offsite_timer_oncalendar: "*-*-* 04:45:00"
|
||||
offsite_rclone_config_local: ~/.config/rclone/rclone.conf
|
||||
offsite_restic_password_local: "{{ playbook_dir }}/../generated/restic-offsite-password"
|
||||
offsite_excludes:
|
||||
- /opt/vaultwarden/data/db.sqlite3
|
||||
- /opt/vaultwarden/data/db.sqlite3-shm
|
||||
- /opt/vaultwarden/data/db.sqlite3-wal
|
||||
- /opt/vaultwarden/data/tmp/**
|
||||
tasks:
|
||||
- name: Configure restic offsite profile
|
||||
ansible.builtin.include_tasks: ../tasks/offsite-restic-profile.yml
|
||||
|
||||
- name: Configure Grimmory offsite backup to Yandex Disk
|
||||
hosts: grimmory
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
offsite_profile: grimmory
|
||||
offsite_repository: rclone:yadisk:System/Backups/HomeLab/restic/grimmory
|
||||
offsite_source_path: /opt/grimmory
|
||||
offsite_sqlite_db: ""
|
||||
offsite_mariadb_container: grimmory-mariadb
|
||||
offsite_mariadb_database: grimmory
|
||||
offsite_mariadb_user: grimmory
|
||||
offsite_mariadb_env: /opt/grimmory/.env
|
||||
offsite_mariadb_dump_name: grimmory.sql
|
||||
offsite_backup_tag: grimmory,cloud-pc,yadisk
|
||||
offsite_timer_oncalendar: "*-*-* 05:15:00"
|
||||
offsite_rclone_config_local: ~/.config/rclone/rclone.conf
|
||||
offsite_restic_password_local: "{{ playbook_dir }}/../generated/restic-offsite-password"
|
||||
offsite_excludes:
|
||||
- /opt/grimmory/books/**
|
||||
- /opt/grimmory/bookdrop/**
|
||||
- /opt/grimmory/mariadb/**
|
||||
- /opt/grimmory/backup-staging/**
|
||||
- /opt/grimmory/**/heapdump*.hprof
|
||||
- /opt/grimmory/**/cache/**
|
||||
tasks:
|
||||
- name: Configure restic offsite profile
|
||||
ansible.builtin.include_tasks: ../tasks/offsite-restic-profile.yml
|
||||
@@ -10,10 +10,22 @@
|
||||
ansible.builtin.command: nc -vz -w 5 192.168.1.10 8006
|
||||
changed_when: false
|
||||
|
||||
- name: Check mini-pc SSH through OpenVPN gateway
|
||||
ansible.builtin.command: nc -vz -w 5 192.168.1.10 22
|
||||
changed_when: false
|
||||
|
||||
- name: Check cloud-pc PVE port through OpenVPN gateway
|
||||
ansible.builtin.command: nc -vz -w 5 192.168.1.5 8006
|
||||
changed_when: false
|
||||
|
||||
- name: Check cloud-pc SSH through OpenVPN gateway
|
||||
ansible.builtin.command: nc -vz -w 5 192.168.1.5 22
|
||||
changed_when: false
|
||||
|
||||
- name: Check PBS port through OpenVPN gateway
|
||||
ansible.builtin.command: nc -vz -w 5 192.168.1.20 8007
|
||||
changed_when: false
|
||||
|
||||
- name: Check PBS SSH through OpenVPN gateway
|
||||
ansible.builtin.command: nc -vz -w 5 192.168.1.20 22
|
||||
changed_when: false
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
- name: Create AdGuard Home LXC on mini-pc
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
adguard_vmid: 144
|
||||
adguard_hostname: adguard
|
||||
adguard_ip: 192.168.1.28/24
|
||||
adguard_gateway: 192.168.1.1
|
||||
adguard_rootfs: local-lvm:8
|
||||
adguard_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
adguard_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
adguard_image: adguard/adguardhome:v0.107.78@sha256:2c127294fa5f96151d9d3a433fb9d66c17e4d18cf698c2b04372a80e26fdd26f
|
||||
handlers:
|
||||
- name: restart adguard lxc
|
||||
ansible.builtin.shell: "pct stop {{ adguard_vmid }} || true; pct start {{ adguard_vmid }}"
|
||||
changed_when: true
|
||||
tasks:
|
||||
- name: Check if AdGuard LXC exists
|
||||
ansible.builtin.command: "pct config {{ adguard_vmid }}"
|
||||
register: adguard_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign VMID {{ adguard_vmid }}
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- adguard_pct_config.rc != 0 or adguard_existing_hostname == adguard_hostname
|
||||
fail_msg: VMID {{ adguard_vmid }} already exists and is not the AdGuard container.
|
||||
vars:
|
||||
adguard_existing_hostname: >-
|
||||
{{ adguard_pct_config.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Install AdGuard LXC SSH public key on PVE host
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/adguard-lxc.pub
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
content: "{{ lookup('file', adguard_pubkey_file) }}\n"
|
||||
when: adguard_pct_config.rc != 0
|
||||
|
||||
- name: Create AdGuard LXC
|
||||
ansible.builtin.command: >-
|
||||
pct create {{ adguard_vmid }} {{ adguard_ostemplate }}
|
||||
--hostname {{ adguard_hostname }}
|
||||
--rootfs {{ adguard_rootfs }}
|
||||
--cores 1
|
||||
--memory 512
|
||||
--swap 512
|
||||
--net0 name=eth0,bridge=vmbr0,gw={{ adguard_gateway }},ip={{ adguard_ip }},firewall=1
|
||||
--nameserver 1.1.1.1
|
||||
--unprivileged 1
|
||||
--features nesting=1,keyctl=1
|
||||
--onboot 1
|
||||
--startup order=40
|
||||
--cmode shell
|
||||
--ssh-public-keys /tmp/adguard-lxc.pub
|
||||
when: adguard_pct_config.rc != 0
|
||||
|
||||
- name: Start AdGuard LXC
|
||||
ansible.builtin.command: "pct start {{ adguard_vmid }}"
|
||||
register: adguard_pct_start
|
||||
changed_when: adguard_pct_start.rc == 0
|
||||
failed_when: adguard_pct_start.rc not in [0, 255]
|
||||
|
||||
- name: Allow FUSE device in AdGuard LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ adguard_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
||||
state: present
|
||||
notify: restart adguard lxc
|
||||
|
||||
- name: Bind mount FUSE device in AdGuard LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ adguard_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
||||
state: present
|
||||
notify: restart adguard lxc
|
||||
|
||||
- name: Apply pending LXC config changes
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Wait for AdGuard SSH through ru-vps
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 120
|
||||
delegate_to: adguard
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
- name: Configure AdGuard Home inside LXC
|
||||
hosts: adguard
|
||||
gather_facts: true
|
||||
vars:
|
||||
ansible_become: false
|
||||
adguard_image: adguard/adguardhome:v0.107.78@sha256:2c127294fa5f96151d9d3a433fb9d66c17e4d18cf698c2b04372a80e26fdd26f
|
||||
tasks:
|
||||
- name: Install Docker packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker.io
|
||||
- fuse-overlayfs
|
||||
- ca-certificates
|
||||
- curl
|
||||
- dnsutils
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure Docker config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/docker
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure Docker storage driver for unprivileged LXC
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
{
|
||||
"storage-driver": "fuse-overlayfs"
|
||||
}
|
||||
register: adguard_docker_daemon_config
|
||||
|
||||
- name: Enable Docker service
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: "{{ 'restarted' if adguard_docker_daemon_config.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Ensure AdGuard data directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- /opt/adguard/work
|
||||
- /opt/adguard/conf
|
||||
|
||||
- name: Check if the configured AdGuard image is present
|
||||
ansible.builtin.command: "docker image inspect {{ adguard_image }}"
|
||||
register: adguard_image_inspect
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull the configured AdGuard image
|
||||
ansible.builtin.command: "docker pull {{ adguard_image }}"
|
||||
when: adguard_image_inspect.rc != 0
|
||||
register: adguard_image_pull
|
||||
changed_when: true
|
||||
|
||||
- name: Install AdGuard Home systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/adguard.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=AdGuard Home container
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
ExecStartPre=-/usr/bin/docker rm -f adguard
|
||||
ExecStart=/usr/bin/docker run --rm --name adguard --pull never \
|
||||
-p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 3000:3000/tcp \
|
||||
-v /opt/adguard/work:/opt/adguardhome/work \
|
||||
-v /opt/adguard/conf:/opt/adguardhome/conf \
|
||||
{{ adguard_image }}
|
||||
ExecStop=/usr/bin/docker stop adguard
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: adguard_unit
|
||||
|
||||
- name: Remove obsolete AdGuard post-start validation unit
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/adguard-post-start.service
|
||||
state: absent
|
||||
register: adguard_post_start_unit_removed
|
||||
|
||||
- name: Reload systemd when AdGuard units change
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: adguard_unit.changed or adguard_post_start_unit_removed.changed
|
||||
|
||||
- name: Enable and start AdGuard Home
|
||||
ansible.builtin.systemd:
|
||||
name: adguard
|
||||
state: "{{ 'restarted' if adguard_unit.changed or adguard_image_pull.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Wait for AdGuard HTTP root
|
||||
ansible.builtin.uri:
|
||||
url: http://127.0.0.1/
|
||||
status_code: [200, 302]
|
||||
return_content: false
|
||||
register: adguard_http_root
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: adguard_http_root.status in [200, 302]
|
||||
|
||||
- name: Verify AdGuard DNS over UDP
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- dig
|
||||
- "@127.0.0.1"
|
||||
- localhost
|
||||
- A
|
||||
- +time=2
|
||||
- +tries=1
|
||||
- +short
|
||||
register: adguard_dns_udp
|
||||
changed_when: false
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: adguard_dns_udp.rc == 0 and '127.0.0.1' in adguard_dns_udp.stdout
|
||||
|
||||
- name: Verify AdGuard DNS over TCP
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- dig
|
||||
- "@127.0.0.1"
|
||||
- localhost
|
||||
- A
|
||||
- +tcp
|
||||
- +time=2
|
||||
- +tries=1
|
||||
- +short
|
||||
register: adguard_dns_tcp
|
||||
changed_when: false
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: adguard_dns_tcp.rc == 0 and '127.0.0.1' in adguard_dns_tcp.stdout
|
||||
@@ -0,0 +1,114 @@
|
||||
- name: Configure Proxmox backup jobs
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
pve_backup_jobs:
|
||||
- id: homelab-pbs-daily-cloud
|
||||
comment: Daily PBS backup for cloud-pc service containers
|
||||
node: cloud-pc
|
||||
vmid: 141,145,146,147,149
|
||||
storage: pbs
|
||||
schedule: "02:10"
|
||||
mode: snapshot
|
||||
exclude_path: /var/lib/docker/fuse-overlayfs/*/merged
|
||||
- id: homelab-pbs-daily-mini
|
||||
comment: Daily PBS backup for mini-pc service containers
|
||||
node: mini-pc
|
||||
vmid: 132,140,142,143,144,150
|
||||
storage: pbs
|
||||
schedule: "02:40"
|
||||
mode: snapshot
|
||||
exclude_path: /var/lib/docker/fuse-overlayfs/*/merged
|
||||
- id: homelab-local-weekly-pbs
|
||||
comment: Weekly local backup for PBS container rootfs/config
|
||||
node: cloud-pc
|
||||
vmid: 120
|
||||
storage: backup
|
||||
schedule: "Sun 03:30"
|
||||
mode: snapshot
|
||||
prune_backups: keep-last=2
|
||||
|
||||
tasks:
|
||||
- name: Get existing Proxmox backup jobs
|
||||
ansible.builtin.command: pvesh get /cluster/backup --output-format json
|
||||
register: pve_backup_jobs_existing_raw
|
||||
changed_when: false
|
||||
|
||||
- name: Parse existing Proxmox backup jobs
|
||||
ansible.builtin.set_fact:
|
||||
pve_backup_jobs_existing: "{{ pve_backup_jobs_existing_raw.stdout | from_json }}"
|
||||
|
||||
- name: Create missing Proxmox backup jobs
|
||||
ansible.builtin.command: >-
|
||||
pvesh create /cluster/backup
|
||||
--id {{ item.id }}
|
||||
--enabled 1
|
||||
--node {{ item.node }}
|
||||
--vmid {{ item.vmid }}
|
||||
--storage {{ item.storage }}
|
||||
--schedule {{ item.schedule | quote }}
|
||||
--mode {{ item.mode }}
|
||||
{% if item.prune_backups is defined %}
|
||||
--prune-backups {{ item.prune_backups }} --remove 1
|
||||
{% else %}
|
||||
--remove 0
|
||||
{% endif %}
|
||||
{% if item.exclude_path is defined %}
|
||||
--exclude-path {{ item.exclude_path | quote }}
|
||||
{% endif %}
|
||||
--notes-template {{ '{{' }}guestname{{ '}}' }}
|
||||
--comment {{ item.comment | quote }}
|
||||
loop: "{{ pve_backup_jobs }}"
|
||||
when: item.id not in (pve_backup_jobs_existing | map(attribute='id') | list)
|
||||
|
||||
- name: Update existing Proxmox backup jobs
|
||||
ansible.builtin.command: >-
|
||||
pvesh set /cluster/backup/{{ item.id }}
|
||||
--enabled 1
|
||||
--node {{ item.node }}
|
||||
--vmid {{ item.vmid }}
|
||||
--storage {{ item.storage }}
|
||||
--schedule {{ item.schedule | quote }}
|
||||
--mode {{ item.mode }}
|
||||
{% if item.prune_backups is defined %}
|
||||
--prune-backups {{ item.prune_backups }} --remove 1
|
||||
{% else %}
|
||||
--delete prune-backups --remove 0
|
||||
{% endif %}
|
||||
{% if item.exclude_path is defined %}
|
||||
--exclude-path {{ item.exclude_path | quote }}
|
||||
{% else %}
|
||||
--delete exclude-path
|
||||
{% endif %}
|
||||
--notes-template {{ '{{' }}guestname{{ '}}' }}
|
||||
--comment {{ item.comment | quote }}
|
||||
loop: "{{ pve_backup_jobs }}"
|
||||
vars:
|
||||
current_job: >-
|
||||
{{ pve_backup_jobs_existing | selectattr('id', 'equalto', item.id) | first }}
|
||||
when:
|
||||
- item.id in (pve_backup_jobs_existing | map(attribute='id') | list)
|
||||
- >-
|
||||
current_job.enabled | int != 1 or
|
||||
current_job.node != item.node or
|
||||
current_job.vmid | string != item.vmid | string or
|
||||
current_job.storage != item.storage or
|
||||
current_job.schedule != item.schedule or
|
||||
current_job.mode != item.mode or
|
||||
current_job.comment != item.comment or
|
||||
current_job.remove | int != (1 if item.prune_backups is defined else 0) or
|
||||
current_job['exclude-path'] | default([]) != ([item.exclude_path] if item.exclude_path is defined else []) or
|
||||
(item.prune_backups is not defined and current_job['prune-backups'] is defined) or
|
||||
(item.prune_backups is defined and
|
||||
(current_job['prune-backups'] | default({})).get('keep-last', 0) | int !=
|
||||
item.prune_backups | regex_replace('^keep-last=', '') | int)
|
||||
changed_when: true
|
||||
|
||||
- name: Show configured Proxmox backup jobs
|
||||
ansible.builtin.command: pvesh get /cluster/backup --output-format yaml
|
||||
register: pve_backup_jobs_configured
|
||||
changed_when: false
|
||||
|
||||
- name: Print configured Proxmox backup jobs
|
||||
ansible.builtin.debug:
|
||||
var: pve_backup_jobs_configured.stdout_lines
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
- name: Create Docker test LXC on cloud-pc
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
docker_test_vmid: 145
|
||||
docker_test_hostname: docker-test
|
||||
docker_test_ip: 192.168.1.29/24
|
||||
docker_test_gateway: 192.168.1.1
|
||||
docker_test_rootfs: data:8
|
||||
docker_test_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
docker_test_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
handlers:
|
||||
- name: restart docker test lxc
|
||||
ansible.builtin.shell: "pct stop {{ docker_test_vmid }} || true; pct start {{ docker_test_vmid }}"
|
||||
changed_when: true
|
||||
tasks:
|
||||
- name: Check if Docker test LXC exists
|
||||
ansible.builtin.command: "pct config {{ docker_test_vmid }}"
|
||||
register: docker_test_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Install Docker test LXC SSH public key on PVE host
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/docker-test-lxc.pub
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
content: "{{ lookup('file', docker_test_pubkey_file) }}\n"
|
||||
when: docker_test_pct_config.rc != 0
|
||||
|
||||
- name: Create Docker test LXC
|
||||
ansible.builtin.command: >-
|
||||
pct create {{ docker_test_vmid }} {{ docker_test_ostemplate }}
|
||||
--hostname {{ docker_test_hostname }}
|
||||
--rootfs {{ docker_test_rootfs }}
|
||||
--cores 1
|
||||
--memory 512
|
||||
--swap 512
|
||||
--net0 name=eth0,bridge=vmbr0,gw={{ docker_test_gateway }},ip={{ docker_test_ip }},firewall=1
|
||||
--nameserver 1.1.1.1
|
||||
--unprivileged 1
|
||||
--features nesting=1,keyctl=1
|
||||
--onboot 1
|
||||
--startup order=50
|
||||
--cmode shell
|
||||
--ssh-public-keys /tmp/docker-test-lxc.pub
|
||||
when: docker_test_pct_config.rc != 0
|
||||
|
||||
- name: Start Docker test LXC
|
||||
ansible.builtin.command: "pct start {{ docker_test_vmid }}"
|
||||
register: docker_test_pct_start
|
||||
changed_when: docker_test_pct_start.rc == 0
|
||||
failed_when: docker_test_pct_start.rc not in [0, 255]
|
||||
|
||||
- name: Allow FUSE device in Docker test LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ docker_test_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
||||
state: present
|
||||
notify: restart docker test lxc
|
||||
|
||||
- name: Bind mount FUSE device in Docker test LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ docker_test_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
||||
state: present
|
||||
notify: restart docker test lxc
|
||||
|
||||
- name: Apply pending LXC config changes
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Wait for Docker test SSH through ru-vps
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 120
|
||||
delegate_to: docker-test
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
- name: Configure Docker test host
|
||||
hosts: docker-test
|
||||
gather_facts: true
|
||||
vars:
|
||||
ansible_become: false
|
||||
tasks:
|
||||
- name: Install Docker packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker.io
|
||||
- fuse-overlayfs
|
||||
- ca-certificates
|
||||
- curl
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure Docker config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/docker
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure Docker storage driver for unprivileged LXC
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
{
|
||||
"storage-driver": "fuse-overlayfs"
|
||||
}
|
||||
register: docker_test_daemon_config
|
||||
|
||||
- name: Enable Docker service
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: "{{ 'restarted' if docker_test_daemon_config.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Verify Docker can run a container
|
||||
ansible.builtin.command: docker run --rm hello-world
|
||||
changed_when: false
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
- name: Create emergency-bot LXC on mini-pc
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: false
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
pve_lxc_vmid: 148
|
||||
pve_lxc_node: mini-pc
|
||||
pve_lxc_hostname: emergency-bot
|
||||
pve_lxc_ip: 192.168.1.32/24
|
||||
pve_lxc_gateway: 192.168.1.1
|
||||
pve_lxc_disk: local-lvm:4
|
||||
pve_lxc_cores: 1
|
||||
pve_lxc_memory: 512
|
||||
pve_lxc_swap: 256
|
||||
pve_lxc_startup: order=70
|
||||
pve_lxc_ostemplate: "{{ lookup('env', 'PVE_LXC_OSTEMPLATE') | default('local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst', true) }}"
|
||||
roles:
|
||||
- role: pve_lxc
|
||||
|
||||
- name: Wait for emergency-bot SSH
|
||||
hosts: emergency-bot
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Wait for emergency-bot to accept SSH connections
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 120
|
||||
@@ -25,6 +25,19 @@
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign Gitea VMID
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- gitea_pct_config.rc != 0 or gitea_existing_hostname == 'gitea'
|
||||
fail_msg: VMID {{ gitea_vmid }} already exists and is not the Gitea container.
|
||||
vars:
|
||||
gitea_existing_hostname: >-
|
||||
{{ gitea_pct_config.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Install Gitea LXC SSH public key on PVE host
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/gitea-lxc.pub
|
||||
@@ -103,7 +116,7 @@
|
||||
vars:
|
||||
ansible_become: false
|
||||
gitea_data_dir: /opt/gitea/data
|
||||
gitea_image: gitea/gitea:latest
|
||||
gitea_image: gitea/gitea:1.27.1@sha256:b64126cf5c3f4e5f0f231b510bb13715f6cb8e508188b44de90bdb9a04f3055d
|
||||
gitea_container_name: gitea
|
||||
gitea_http_port: 3000
|
||||
gitea_ssh_port: 2222
|
||||
@@ -154,6 +167,18 @@
|
||||
group: "1000"
|
||||
mode: "0750"
|
||||
|
||||
- name: Check if the configured Gitea image is present
|
||||
ansible.builtin.command: "docker image inspect {{ gitea_image }}"
|
||||
register: gitea_image_inspect
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull the configured Gitea image
|
||||
ansible.builtin.command: "docker pull {{ gitea_image }}"
|
||||
when: gitea_image_inspect.rc != 0
|
||||
register: gitea_image_pull
|
||||
changed_when: true
|
||||
|
||||
- name: Install Gitea systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/gitea.service
|
||||
@@ -172,7 +197,7 @@
|
||||
ExecStartPre=-/usr/bin/docker rm -f {{ gitea_container_name }}
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name {{ gitea_container_name }} \
|
||||
--pull always \
|
||||
--pull never \
|
||||
-p {{ gitea_http_port }}:3000 \
|
||||
-p {{ gitea_ssh_port }}:22 \
|
||||
-v {{ gitea_data_dir }}:/data \
|
||||
@@ -193,5 +218,14 @@
|
||||
- name: Enable and start Gitea
|
||||
ansible.builtin.systemd:
|
||||
name: gitea
|
||||
state: started
|
||||
state: "{{ 'restarted' if gitea_unit.changed or gitea_image_pull.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Wait for Gitea HTTP health endpoint
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ gitea_http_port }}/api/healthz"
|
||||
status_code: 200
|
||||
register: gitea_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: gitea_health.status == 200
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
---
|
||||
- name: Guard Grimmory VMID before API updates
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read existing VMID 149 configuration
|
||||
ansible.builtin.command: pct config 149
|
||||
register: grimmory_existing_vmid
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign VMID 149
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- grimmory_existing_vmid.rc != 0 or grimmory_existing_hostname == 'grimmory'
|
||||
fail_msg: VMID 149 already exists and is not the Grimmory container.
|
||||
vars:
|
||||
grimmory_existing_hostname: >-
|
||||
{{ grimmory_existing_vmid.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Create Grimmory LXC on cloud-pc
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: false
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
pve_lxc_vmid: 149
|
||||
pve_lxc_node: cloud-pc
|
||||
pve_lxc_hostname: grimmory
|
||||
pve_lxc_ip: 192.168.1.34/24
|
||||
pve_lxc_gateway: 192.168.1.1
|
||||
pve_lxc_disk: data:64
|
||||
pve_lxc_cores: 2
|
||||
pve_lxc_memory: 4096
|
||||
pve_lxc_swap: 1024
|
||||
pve_lxc_startup: order=100
|
||||
pve_lxc_unprivileged: true
|
||||
pve_lxc_update: false
|
||||
pve_lxc_features:
|
||||
- nesting=1
|
||||
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
roles:
|
||||
- role: pve_lxc
|
||||
|
||||
- name: Configure Grimmory LXC devices
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
grimmory_vmid: 149
|
||||
handlers:
|
||||
- name: restart Grimmory LXC
|
||||
ansible.builtin.command: "pct reboot {{ grimmory_vmid }}"
|
||||
changed_when: true
|
||||
tasks:
|
||||
- name: Read Grimmory LXC configuration
|
||||
ansible.builtin.command: "pct config {{ grimmory_vmid }}"
|
||||
register: grimmory_lxc_config
|
||||
changed_when: false
|
||||
|
||||
- name: Require expected Grimmory LXC properties
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'unprivileged: 1' in grimmory_lxc_config.stdout"
|
||||
- "'rootfs: data:' in grimmory_lxc_config.stdout"
|
||||
- "'onboot: 1' in grimmory_lxc_config.stdout"
|
||||
|
||||
- name: Configure Grimmory LXC network
|
||||
ansible.builtin.command: >-
|
||||
pct set {{ grimmory_vmid }}
|
||||
--net0 name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.34/24
|
||||
vars:
|
||||
grimmory_net0: >-
|
||||
{{ grimmory_lxc_config.stdout_lines
|
||||
| select('match', '^net0: ')
|
||||
| first
|
||||
| default('') }}
|
||||
when: >-
|
||||
'name=eth0' not in grimmory_net0 or
|
||||
'bridge=vmbr0' not in grimmory_net0 or
|
||||
'firewall=1' not in grimmory_net0 or
|
||||
'gw=192.168.1.1' not in grimmory_net0 or
|
||||
'ip=192.168.1.34/24' not in grimmory_net0
|
||||
notify: restart Grimmory LXC
|
||||
|
||||
- name: Enable keyctl for Docker in Grimmory LXC
|
||||
ansible.builtin.command: "pct set {{ grimmory_vmid }} --features nesting=1,keyctl=1"
|
||||
when: "'nesting=1' not in grimmory_lxc_config.stdout or 'keyctl=1' not in grimmory_lxc_config.stdout"
|
||||
notify: restart Grimmory LXC
|
||||
|
||||
- name: Allow FUSE device in Grimmory LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ grimmory_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
||||
state: present
|
||||
notify: restart Grimmory LXC
|
||||
|
||||
- name: Bind mount FUSE device in Grimmory LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ grimmory_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
||||
state: present
|
||||
notify: restart Grimmory LXC
|
||||
|
||||
- name: Apply pending Grimmory LXC configuration
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Wait for Grimmory SSH through ru-vps
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 180
|
||||
delegate_to: grimmory
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
- name: Configure Grimmory runtime
|
||||
hosts: grimmory
|
||||
gather_facts: true
|
||||
vars:
|
||||
ansible_become: false
|
||||
grimmory_root: /opt/grimmory
|
||||
grimmory_image: grimmory/grimmory:v3.2.4@sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb
|
||||
grimmory_mariadb_image: lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f701bc7fc3a424b81beafca7a7c6c4c5b7c8be6afd2ae148698695c0b0c
|
||||
tasks:
|
||||
- name: Install Grimmory runtime packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- docker-compose
|
||||
- docker.io
|
||||
- fuse-overlayfs
|
||||
- mariadb-client
|
||||
- openssl
|
||||
- prometheus-node-exporter
|
||||
- ufw
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Verify FUSE device exists
|
||||
ansible.builtin.stat:
|
||||
path: /dev/fuse
|
||||
register: grimmory_fuse
|
||||
|
||||
- name: Require FUSE device
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- grimmory_fuse.stat.exists
|
||||
- grimmory_fuse.stat.ischr
|
||||
|
||||
- name: Configure Docker storage driver for unprivileged LXC
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
{
|
||||
"storage-driver": "fuse-overlayfs"
|
||||
}
|
||||
register: grimmory_docker_config
|
||||
|
||||
- name: Enable Docker service
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if grimmory_docker_config.changed else 'started' }}"
|
||||
|
||||
- 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: Allow Grimmory from the HomeLab LAN
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "6060"
|
||||
proto: tcp
|
||||
src: "{{ homelab_lan_cidr }}"
|
||||
|
||||
- name: Allow Grimmory from the OpenVPN network
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "6060"
|
||||
proto: tcp
|
||||
src: "{{ openvpn_network_cidr }}"
|
||||
|
||||
- name: Allow Node Exporter from the monitoring LXC
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "9100"
|
||||
proto: tcp
|
||||
src: 192.168.1.30
|
||||
|
||||
- name: Enable restrictive Grimmory firewall
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
policy: deny
|
||||
direction: incoming
|
||||
|
||||
- name: Create Grimmory directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: directory
|
||||
owner: "{{ item.owner }}"
|
||||
group: "{{ item.group }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { path: "{{ grimmory_root }}", owner: root, group: root, mode: "0750" }
|
||||
- { path: "{{ grimmory_root }}/data", owner: "1000", group: "1000", mode: "0750" }
|
||||
- { path: "{{ grimmory_root }}/books", owner: "1000", group: "1000", mode: "0750" }
|
||||
- { path: "{{ grimmory_root }}/bookdrop", owner: "1000", group: "1000", mode: "0750" }
|
||||
- { path: "{{ grimmory_root }}/mariadb", owner: "1000", group: "1000", mode: "0750" }
|
||||
- { path: "{{ grimmory_root }}/backup-staging", owner: root, group: root, mode: "0700" }
|
||||
|
||||
- name: Generate Grimmory secrets once
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
umask 077
|
||||
if [ -e {{ grimmory_root }}/.env ]; then
|
||||
exit 0
|
||||
fi
|
||||
db_password=$(openssl rand -hex 32)
|
||||
root_password=$(openssl rand -hex 32)
|
||||
cat > {{ grimmory_root }}/.env <<EOF
|
||||
TZ=Europe/Moscow
|
||||
APP_USER_ID=1000
|
||||
APP_GROUP_ID=1000
|
||||
DB_USER_ID=1000
|
||||
DB_GROUP_ID=1000
|
||||
DB_PASSWORD=$db_password
|
||||
MYSQL_ROOT_PASSWORD=$root_password
|
||||
EOF
|
||||
printf created
|
||||
args:
|
||||
executable: /bin/sh
|
||||
register: grimmory_secrets
|
||||
changed_when: grimmory_secrets.stdout == 'created'
|
||||
no_log: true
|
||||
|
||||
- name: Enforce Grimmory secret file permissions
|
||||
ansible.builtin.file:
|
||||
path: "{{ grimmory_root }}/.env"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Install Grimmory environment example
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ grimmory_root }}/.env.example"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
TZ=Europe/Moscow
|
||||
APP_USER_ID=1000
|
||||
APP_GROUP_ID=1000
|
||||
DB_USER_ID=1000
|
||||
DB_GROUP_ID=1000
|
||||
DB_PASSWORD=replace-with-random-password
|
||||
MYSQL_ROOT_PASSWORD=replace-with-separate-random-password
|
||||
|
||||
- name: Install Grimmory Compose configuration
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ grimmory_root }}/compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
services:
|
||||
grimmory:
|
||||
image: {{ grimmory_image }}
|
||||
container_name: grimmory
|
||||
environment:
|
||||
USER_ID: "${APP_USER_ID}"
|
||||
GROUP_ID: "${APP_GROUP_ID}"
|
||||
TZ: "${TZ}"
|
||||
DATABASE_URL: jdbc:mariadb://mariadb:3306/grimmory
|
||||
DATABASE_USERNAME: grimmory
|
||||
DATABASE_PASSWORD: "${DB_PASSWORD}"
|
||||
DISK_TYPE: LOCAL
|
||||
ALLOWED_ORIGINS: https://books.ada-dev.ru
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "192.168.1.34:6060:6060"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./books:/books
|
||||
- ./bookdrop:/bookdrop
|
||||
restart: unless-stopped
|
||||
|
||||
mariadb:
|
||||
image: {{ grimmory_mariadb_image }}
|
||||
container_name: grimmory-mariadb
|
||||
environment:
|
||||
PUID: "${DB_USER_ID}"
|
||||
PGID: "${DB_GROUP_ID}"
|
||||
TZ: "${TZ}"
|
||||
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
|
||||
MYSQL_DATABASE: grimmory
|
||||
MYSQL_USER: grimmory
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
volumes:
|
||||
- ./mariadb:/config
|
||||
healthcheck:
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
register: grimmory_compose
|
||||
|
||||
- name: Install Docker firewall script
|
||||
ansible.builtin.copy:
|
||||
dest: /usr/local/sbin/grimmory-docker-firewall
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
content: |
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
iptables -N GRIMMORY-FILTER 2>/dev/null || true
|
||||
iptables -F GRIMMORY-FILTER
|
||||
iptables -A GRIMMORY-FILTER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A GRIMMORY-FILTER -s {{ homelab_lan_cidr }} -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j ACCEPT
|
||||
iptables -A GRIMMORY-FILTER -s {{ openvpn_network_cidr }} -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j ACCEPT
|
||||
iptables -A GRIMMORY-FILTER -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j DROP
|
||||
iptables -A GRIMMORY-FILTER -j RETURN
|
||||
iptables -C DOCKER-USER -j GRIMMORY-FILTER 2>/dev/null || iptables -I DOCKER-USER 1 -j GRIMMORY-FILTER
|
||||
register: grimmory_firewall_script
|
||||
|
||||
- name: Install Docker firewall service
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/grimmory-docker-firewall.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Restrict Grimmory Docker published port
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
Before=grimmory.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/grimmory-docker-firewall
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: grimmory_firewall_unit
|
||||
|
||||
- name: Install Grimmory systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/grimmory.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Grimmory Compose stack
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.service grimmory-docker-firewall.service
|
||||
Requires=docker.service grimmory-docker-firewall.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory={{ grimmory_root }}
|
||||
ExecStart=/usr/bin/docker compose -f {{ grimmory_root }}/compose.yml up -d --remove-orphans
|
||||
ExecStop=/usr/bin/docker compose -f {{ grimmory_root }}/compose.yml down
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: grimmory_unit
|
||||
|
||||
- name: Reload systemd for Grimmory units
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: grimmory_firewall_unit.changed or grimmory_unit.changed
|
||||
|
||||
- name: Enable and apply Docker firewall
|
||||
ansible.builtin.systemd:
|
||||
name: grimmory-docker-firewall
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if grimmory_firewall_script.changed or grimmory_firewall_unit.changed else 'started' }}"
|
||||
|
||||
- name: Validate Grimmory Compose configuration
|
||||
ansible.builtin.command: docker compose -f {{ grimmory_root }}/compose.yml config --quiet
|
||||
args:
|
||||
chdir: "{{ grimmory_root }}"
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Enable and start Grimmory
|
||||
ansible.builtin.systemd:
|
||||
name: grimmory
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if grimmory_compose.changed or grimmory_unit.changed else 'started' }}"
|
||||
|
||||
- name: Wait for Grimmory health endpoint
|
||||
ansible.builtin.uri:
|
||||
url: http://192.168.1.34:6060/api/v1/healthcheck
|
||||
status_code: 200
|
||||
register: grimmory_health
|
||||
retries: 120
|
||||
delay: 5
|
||||
until: grimmory_health.status == 200
|
||||
|
||||
- name: Verify Docker storage driver
|
||||
ansible.builtin.command: docker info --format '{{ "{{" }}.Driver{{ "}}" }}'
|
||||
register: grimmory_docker_driver
|
||||
changed_when: false
|
||||
failed_when: grimmory_docker_driver.stdout != 'fuse-overlayfs'
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
- name: Guard Gyro VMID before API updates
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read existing VMID 150 configuration
|
||||
ansible.builtin.command: pct config 150
|
||||
register: gyro_existing_vmid
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign VMID 150
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- gyro_existing_vmid.rc != 0 or gyro_existing_hostname == 'gyro'
|
||||
fail_msg: VMID 150 already exists and is not the Gyro container.
|
||||
vars:
|
||||
gyro_existing_hostname: >-
|
||||
{{ gyro_existing_vmid.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Create Gyro LXC on mini-pc
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: false
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
pve_lxc_vmid: 150
|
||||
pve_lxc_node: mini-pc
|
||||
pve_lxc_hostname: gyro
|
||||
pve_lxc_ip: 192.168.1.35/24
|
||||
pve_lxc_gateway: 192.168.1.1
|
||||
pve_lxc_disk: local-lvm:2
|
||||
pve_lxc_cores: 1
|
||||
pve_lxc_memory: 512
|
||||
pve_lxc_swap: 256
|
||||
pve_lxc_startup: order=80
|
||||
pve_lxc_unprivileged: true
|
||||
pve_lxc_update: false
|
||||
pve_lxc_features: []
|
||||
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
roles:
|
||||
- role: pve_lxc
|
||||
|
||||
- name: Configure Gyro LXC isolation
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
gyro_vmid: 150
|
||||
tasks:
|
||||
- name: Read Gyro LXC configuration
|
||||
ansible.builtin.command: "pct config {{ gyro_vmid }}"
|
||||
register: gyro_lxc_config
|
||||
changed_when: false
|
||||
|
||||
- name: Require expected Gyro LXC properties
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'hostname: gyro' in gyro_lxc_config.stdout"
|
||||
- "'unprivileged: 1' in gyro_lxc_config.stdout"
|
||||
- "'rootfs: local-lvm:' in gyro_lxc_config.stdout"
|
||||
- "'onboot: 1' in gyro_lxc_config.stdout"
|
||||
|
||||
- name: Check for an existing Gyro firewall file
|
||||
ansible.builtin.stat:
|
||||
path: "/etc/pve/firewall/{{ gyro_vmid }}.fw"
|
||||
register: gyro_firewall_file
|
||||
|
||||
- name: Preserve the existing Gyro firewall file
|
||||
ansible.builtin.slurp:
|
||||
src: "/etc/pve/firewall/{{ gyro_vmid }}.fw"
|
||||
register: gyro_previous_firewall
|
||||
when: gyro_firewall_file.stat.exists
|
||||
|
||||
- name: Apply and verify Gyro firewall
|
||||
block:
|
||||
- name: Render Proxmox firewall for Gyro
|
||||
ansible.builtin.copy:
|
||||
dest: "/tmp/gyro-{{ gyro_vmid }}.fw"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
content: |
|
||||
[OPTIONS]
|
||||
enable: 1
|
||||
policy_in: DROP
|
||||
policy_out: ACCEPT
|
||||
|
||||
[RULES]
|
||||
IN ACCEPT -source {{ homelab_lan_cidr }} -p tcp -dport 22 -log nolog
|
||||
IN ACCEPT -source {{ openvpn_network_cidr }} -p tcp -dport 22 -log nolog
|
||||
OUT ACCEPT -dest 192.168.1.27 -p tcp -dport 7890 -log nolog
|
||||
OUT DROP -dest {{ homelab_lan_cidr }} -log nolog
|
||||
register: gyro_rendered_firewall
|
||||
changed_when: false
|
||||
|
||||
- name: Compare rendered and active Gyro firewall
|
||||
ansible.builtin.command: >-
|
||||
cmp -s /tmp/gyro-{{ gyro_vmid }}.fw /etc/pve/firewall/{{ gyro_vmid }}.fw
|
||||
register: gyro_firewall_comparison
|
||||
changed_when: false
|
||||
failed_when: gyro_firewall_comparison.rc not in [0, 1]
|
||||
when: gyro_firewall_file.stat.exists
|
||||
|
||||
- name: Install Proxmox firewall for Gyro
|
||||
ansible.builtin.command: >-
|
||||
cp /tmp/gyro-{{ gyro_vmid }}.fw /etc/pve/firewall/{{ gyro_vmid }}.fw
|
||||
when: not gyro_firewall_file.stat.exists or gyro_firewall_comparison.rc != 0
|
||||
changed_when: true
|
||||
|
||||
- name: Validate Proxmox firewall configuration
|
||||
ansible.builtin.command: pve-firewall compile
|
||||
changed_when: false
|
||||
|
||||
- name: Read cluster firewall options
|
||||
ansible.builtin.command: pvesh get /cluster/firewall/options --output-format json
|
||||
register: gyro_cluster_firewall_options
|
||||
changed_when: false
|
||||
|
||||
- name: Report staged Proxmox firewall state
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ 'Gyro Proxmox firewall is active.'
|
||||
if (gyro_cluster_firewall_options.stdout | from_json).enable | default(0) | int == 1
|
||||
else 'Gyro Proxmox firewall is staged but inactive because the cluster firewall is disabled; UFW remains the enforced isolation layer.' }}
|
||||
|
||||
- name: Verify SSH remains reachable through the firewall
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 180
|
||||
delegate_to: gyro
|
||||
vars:
|
||||
ansible_become: false
|
||||
rescue:
|
||||
- name: Restore the previous Gyro firewall file
|
||||
ansible.builtin.copy:
|
||||
dest: "/tmp/gyro-{{ gyro_vmid }}-previous.fw"
|
||||
content: "{{ gyro_previous_firewall.content | b64decode }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
when: gyro_firewall_file.stat.exists
|
||||
|
||||
- name: Reinstall the previous Gyro firewall file
|
||||
ansible.builtin.command: >-
|
||||
cp /tmp/gyro-{{ gyro_vmid }}-previous.fw /etc/pve/firewall/{{ gyro_vmid }}.fw
|
||||
when: gyro_firewall_file.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: Remove the failed new Gyro firewall file
|
||||
ansible.builtin.file:
|
||||
path: "/etc/pve/firewall/{{ gyro_vmid }}.fw"
|
||||
state: absent
|
||||
when: not gyro_firewall_file.stat.exists
|
||||
|
||||
- name: Recompile restored Proxmox firewall configuration
|
||||
ansible.builtin.command: pve-firewall compile
|
||||
changed_when: false
|
||||
|
||||
- name: Remove temporary Gyro firewall files after rollback
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "/tmp/gyro-{{ gyro_vmid }}.fw"
|
||||
- "/tmp/gyro-{{ gyro_vmid }}-previous.fw"
|
||||
|
||||
- name: Stop after rolling back the Gyro firewall
|
||||
ansible.builtin.fail:
|
||||
msg: Gyro firewall validation or SSH reachability failed; the previous firewall state was restored.
|
||||
|
||||
- name: Remove temporary Gyro firewall file
|
||||
ansible.builtin.file:
|
||||
path: "/tmp/gyro-{{ gyro_vmid }}.fw"
|
||||
state: absent
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Gyro SSH
|
||||
hosts: gyro
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Wait for Gyro to accept SSH connections
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 180
|
||||
@@ -0,0 +1,334 @@
|
||||
---
|
||||
- name: Create Hermes AI LXC on cloud-pc
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
hermes_ai_vmid: 147
|
||||
hermes_ai_hostname: hermes-ai
|
||||
hermes_ai_ip: 192.168.1.31/24
|
||||
hermes_ai_gateway: 192.168.1.1
|
||||
hermes_ai_rootfs: data:24
|
||||
hermes_ai_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
hermes_ai_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
tasks:
|
||||
- name: Check if Hermes AI LXC exists
|
||||
ansible.builtin.command: "pct config {{ hermes_ai_vmid }}"
|
||||
register: hermes_ai_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Install Hermes AI LXC SSH public key on PVE host
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/hermes-ai-lxc.pub
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
content: "{{ lookup('file', hermes_ai_pubkey_file) }}\n"
|
||||
when: hermes_ai_pct_config.rc != 0
|
||||
|
||||
- name: Create Hermes AI LXC
|
||||
ansible.builtin.command: >-
|
||||
pct create {{ hermes_ai_vmid }} {{ hermes_ai_ostemplate }}
|
||||
--hostname {{ hermes_ai_hostname }}
|
||||
--rootfs {{ hermes_ai_rootfs }}
|
||||
--cores 2
|
||||
--memory 4096
|
||||
--swap 512
|
||||
--net0 name=eth0,bridge=vmbr0,gw={{ hermes_ai_gateway }},ip={{ hermes_ai_ip }},firewall=1
|
||||
--nameserver 1.1.1.1
|
||||
--unprivileged 1
|
||||
--features nesting=1,keyctl=1
|
||||
--onboot 1
|
||||
--startup order=90
|
||||
--cmode shell
|
||||
--ssh-public-keys /tmp/hermes-ai-lxc.pub
|
||||
when: hermes_ai_pct_config.rc != 0
|
||||
|
||||
- name: Start Hermes AI LXC
|
||||
ansible.builtin.command: "pct start {{ hermes_ai_vmid }}"
|
||||
register: hermes_ai_pct_start
|
||||
changed_when: hermes_ai_pct_start.rc == 0
|
||||
failed_when: hermes_ai_pct_start.rc not in [0, 255]
|
||||
|
||||
- name: Configure Hermes AI runtime
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
hermes_ai_vmid: 147
|
||||
handlers:
|
||||
- name: restart Hermes AI LXC
|
||||
ansible.builtin.command: "pct reboot {{ hermes_ai_vmid }}"
|
||||
changed_when: true
|
||||
tasks:
|
||||
- name: Read Hermes AI LXC configuration
|
||||
ansible.builtin.command: "pct config {{ hermes_ai_vmid }}"
|
||||
register: hermes_ai_lxc_config
|
||||
changed_when: false
|
||||
|
||||
- name: Enable keyctl for Docker in Hermes AI LXC
|
||||
ansible.builtin.command: "pct set {{ hermes_ai_vmid }} --features nesting=1,keyctl=1"
|
||||
when: "'keyctl=1' not in hermes_ai_lxc_config.stdout"
|
||||
notify: restart Hermes AI LXC
|
||||
|
||||
- name: Allow FUSE device in Hermes AI LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
||||
state: present
|
||||
notify: restart Hermes AI LXC
|
||||
|
||||
- name: Bind mount FUSE device in Hermes AI LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
||||
state: present
|
||||
notify: restart Hermes AI LXC
|
||||
|
||||
- name: Check TUN device on PVE host
|
||||
ansible.builtin.stat:
|
||||
path: /dev/net/tun
|
||||
register: hermes_ai_tun_device
|
||||
|
||||
- name: Require TUN device on PVE host
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hermes_ai_tun_device.stat.exists
|
||||
- hermes_ai_tun_device.stat.ischr
|
||||
fail_msg: /dev/net/tun must exist on cloud-pc before enabling the Hermes AI transparent proxy.
|
||||
|
||||
- name: Allow TUN device in Hermes AI LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:200 rwm"
|
||||
state: present
|
||||
notify: restart Hermes AI LXC
|
||||
|
||||
- name: Bind mount TUN device in Hermes AI LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file"
|
||||
state: present
|
||||
notify: restart Hermes AI LXC
|
||||
|
||||
- name: Apply pending Hermes AI LXC configuration
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Wait for Hermes AI SSH through ru-vps
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 120
|
||||
delegate_to: hermes-ai
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
- name: Install Hermes AI base runtime
|
||||
hosts: hermes-ai
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
hermes_ai_root: /opt/hermes-ai
|
||||
hermes_ai_state: /srv/hermes-ai
|
||||
hermes_ai_proxy_dir: /opt/hermes-ai/tun-proxy
|
||||
hermes_ai_proxy_image: metacubex/mihomo@sha256:e6acd921addecfd59a8e2d38203f88356d635b54de6c0673db0e015139989312
|
||||
hermes_ai_proxy_name: hermes-ai-tun-proxy
|
||||
tasks:
|
||||
- name: Install Hermes AI runtime packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- docker-compose
|
||||
- docker.io
|
||||
- fuse-overlayfs
|
||||
- git
|
||||
- ufw
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Configure Docker storage driver for unprivileged LXC
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
{
|
||||
"storage-driver": "fuse-overlayfs"
|
||||
}
|
||||
register: hermes_ai_docker_config
|
||||
|
||||
- name: Enable Docker service
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if hermes_ai_docker_config.changed else 'started' }}"
|
||||
|
||||
- name: Allow SSH only 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: Enable restrictive Hermes AI firewall
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
policy: deny
|
||||
direction: incoming
|
||||
|
||||
- name: Create Hermes AI application directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ hermes_ai_root }}"
|
||||
- "{{ hermes_ai_state }}"
|
||||
- "{{ hermes_ai_proxy_dir }}"
|
||||
|
||||
- name: Configure Hermes AI transparent proxy
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ hermes_ai_proxy_dir }}/config.yaml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
content: |
|
||||
ipv6: false
|
||||
|
||||
tun:
|
||||
enable: true
|
||||
stack: system
|
||||
device: hermes-tun
|
||||
auto-route: true
|
||||
auto-redirect: true
|
||||
auto-detect-interface: true
|
||||
route-exclude-address:
|
||||
- 192.168.1.27/32
|
||||
dns-hijack:
|
||||
- any:53
|
||||
|
||||
dns:
|
||||
enable: true
|
||||
enhanced-mode: redir-host
|
||||
nameserver:
|
||||
- https://cloudflare-dns.com/dns-query
|
||||
|
||||
sniffer:
|
||||
enable: true
|
||||
force-dns-mapping: true
|
||||
parse-pure-ip: true
|
||||
|
||||
proxies:
|
||||
- name: mihomo-lan
|
||||
type: socks5
|
||||
server: 192.168.1.27
|
||||
port: 7891
|
||||
|
||||
proxy-groups:
|
||||
- name: PROXY
|
||||
type: select
|
||||
proxies:
|
||||
- mihomo-lan
|
||||
|
||||
rules:
|
||||
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
||||
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
|
||||
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
|
||||
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
||||
- IP-CIDR,169.254.0.0/16,DIRECT,no-resolve
|
||||
- MATCH,PROXY
|
||||
register: hermes_ai_proxy_config
|
||||
|
||||
- name: Wait for Mihomo SOCKS5 upstream
|
||||
ansible.builtin.wait_for:
|
||||
host: 192.168.1.27
|
||||
port: 7891
|
||||
timeout: 15
|
||||
|
||||
- name: Validate Hermes AI transparent proxy config
|
||||
ansible.builtin.command: >-
|
||||
docker run --rm --network none
|
||||
-v {{ hermes_ai_proxy_dir }}:/root/.config/mihomo:ro
|
||||
{{ hermes_ai_proxy_image }} -t -d /root/.config/mihomo
|
||||
changed_when: false
|
||||
|
||||
- name: Install Hermes AI transparent proxy service
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/hermes-ai-tun-proxy.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Hermes AI transparent Mihomo proxy
|
||||
After=docker.service network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
ExecStartPre=-/usr/bin/docker rm -f {{ hermes_ai_proxy_name }}
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name {{ hermes_ai_proxy_name }} \
|
||||
--network host \
|
||||
--cap-drop ALL \
|
||||
--cap-add NET_ADMIN \
|
||||
--security-opt no-new-privileges \
|
||||
--device /dev/net/tun \
|
||||
-v {{ hermes_ai_proxy_dir }}:/root/.config/mihomo:ro \
|
||||
{{ hermes_ai_proxy_image }}
|
||||
ExecStop=/usr/bin/docker stop {{ hermes_ai_proxy_name }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: hermes_ai_proxy_unit
|
||||
|
||||
- name: Reload systemd when transparent proxy service changes
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: hermes_ai_proxy_unit.changed
|
||||
|
||||
- name: Enable Hermes AI transparent proxy
|
||||
ansible.builtin.systemd:
|
||||
name: hermes-ai-tun-proxy
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if hermes_ai_proxy_config.changed or hermes_ai_proxy_unit.changed else 'started' }}"
|
||||
|
||||
- name: Check Hermes AI transparent proxy service
|
||||
ansible.builtin.command: systemctl is-active hermes-ai-tun-proxy
|
||||
register: hermes_ai_proxy_status
|
||||
changed_when: false
|
||||
failed_when: hermes_ai_proxy_status.stdout != 'active'
|
||||
|
||||
- name: Check Hermes AI external HTTPS access
|
||||
ansible.builtin.command: curl --fail --silent --show-error --max-time 20 https://api.ipify.org
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Document Hermes AI runtime layout
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ hermes_ai_root }}/README.md"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
# Hermes AI runtime
|
||||
|
||||
Deploy the Hermes application here. Keep runtime state and the real
|
||||
`.env` file in /srv/hermes-ai. Do not commit Telegram or LLM tokens.
|
||||
|
||||
Docker is installed with fuse-overlayfs for this unprivileged LXC.
|
||||
Hermes is a Telegram bot and needs no published Docker ports. Do not
|
||||
use `ports:` or `-p` without adding an explicit firewall policy:
|
||||
Docker port publishing can bypass UFW.
|
||||
|
||||
The hermes-ai-tun-proxy service routes external traffic through
|
||||
Mihomo at 192.168.1.27:7890. LAN traffic remains direct.
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
- name: Create memoir-bot LXC on mini-pc
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
memoir_bot_vmid: 142
|
||||
memoir_bot_hostname: memoir-bot
|
||||
memoir_bot_ip: 192.168.1.26/24
|
||||
memoir_bot_gateway: 192.168.1.1
|
||||
memoir_bot_bridge: vmbr0
|
||||
memoir_bot_rootfs: local-lvm:16
|
||||
memoir_bot_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
memoir_bot_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
handlers:
|
||||
- name: restart memoir-bot lxc
|
||||
ansible.builtin.shell: "pct stop {{ memoir_bot_vmid }} || true; pct start {{ memoir_bot_vmid }}"
|
||||
changed_when: true
|
||||
tasks:
|
||||
- name: Check if memoir-bot LXC exists
|
||||
ansible.builtin.command: "pct config {{ memoir_bot_vmid }}"
|
||||
register: memoir_bot_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Install memoir-bot LXC SSH public key on PVE host
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/memoir-bot-lxc.pub
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
content: "{{ lookup('file', memoir_bot_pubkey_file) }}\n"
|
||||
when: memoir_bot_pct_config.rc != 0
|
||||
|
||||
- name: Create memoir-bot LXC
|
||||
ansible.builtin.command: >-
|
||||
pct create {{ memoir_bot_vmid }} {{ memoir_bot_ostemplate }}
|
||||
--hostname {{ memoir_bot_hostname }}
|
||||
--rootfs {{ memoir_bot_rootfs }}
|
||||
--cores 1
|
||||
--memory 512
|
||||
--swap 512
|
||||
--net0 name=eth0,bridge={{ memoir_bot_bridge }},gw={{ memoir_bot_gateway }},ip={{ memoir_bot_ip }},firewall=1
|
||||
--nameserver 1.1.1.1
|
||||
--unprivileged 1
|
||||
--features nesting=1,keyctl=1
|
||||
--onboot 1
|
||||
--startup order=60
|
||||
--cmode shell
|
||||
--ssh-public-keys /tmp/memoir-bot-lxc.pub
|
||||
when: memoir_bot_pct_config.rc != 0
|
||||
|
||||
- name: Start memoir-bot LXC
|
||||
ansible.builtin.command: "pct start {{ memoir_bot_vmid }}"
|
||||
register: memoir_bot_pct_start
|
||||
changed_when: memoir_bot_pct_start.rc == 0
|
||||
failed_when: memoir_bot_pct_start.rc not in [0, 255]
|
||||
|
||||
- name: Allow FUSE device in memoir-bot LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ memoir_bot_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
||||
state: present
|
||||
notify: restart memoir-bot lxc
|
||||
|
||||
- name: Bind mount FUSE device in memoir-bot LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ memoir_bot_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
||||
state: present
|
||||
notify: restart memoir-bot lxc
|
||||
|
||||
- name: Apply pending LXC config changes
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Wait for memoir-bot SSH through ru-vps
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 120
|
||||
delegate_to: memoir-bot
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
- name: Configure Docker and memoir-bot inside LXC
|
||||
hosts: memoir-bot
|
||||
gather_facts: true
|
||||
vars:
|
||||
ansible_become: false
|
||||
memoir_bot_source_dir: /home/ada/Documents/Projects/Other/memoir_bot/
|
||||
memoir_bot_app_dir: /opt/memoir-bot/app
|
||||
memoir_bot_state_dir: /srv/memoir-bot
|
||||
memoir_bot_env_file: /srv/memoir-bot/.env
|
||||
memoir_bot_env_source: /home/ada/Documents/Projects/Other/memoir_bot/.env
|
||||
memoir_bot_ssh_private_key_file: ~/.ssh/id_ed25519
|
||||
memoir_bot_ssh_public_key_file: ~/.ssh/id_ed25519.pub
|
||||
memoir_bot_vault_repo: git@github.com:ada-dmitry/SecondBrain.git
|
||||
memoir_bot_vault_dir: /srv/memoir-bot/vault
|
||||
memoir_bot_image: memoir-bot:local
|
||||
memoir_bot_container_name: memoir-bot
|
||||
tasks:
|
||||
- name: Install Docker and deploy dependencies
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker.io
|
||||
- fuse-overlayfs
|
||||
- git
|
||||
- openssh-client
|
||||
- rsync
|
||||
- ca-certificates
|
||||
- curl
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure Docker config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/docker
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure Docker storage driver for unprivileged LXC
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
{
|
||||
"storage-driver": "fuse-overlayfs"
|
||||
}
|
||||
register: docker_daemon_config
|
||||
|
||||
- name: Ensure Docker service is enabled and running
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: "{{ 'restarted' if docker_daemon_config.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Ensure memoir-bot directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ memoir_bot_app_dir }}"
|
||||
- "{{ memoir_bot_state_dir }}"
|
||||
- "{{ memoir_bot_state_dir }}/ssh"
|
||||
|
||||
- name: Copy memoir-bot env example
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ memoir_bot_state_dir }}/.env.example"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
content: |
|
||||
BOT_TOKEN=123456:telegram-token
|
||||
ALLOWED_USER_IDS=123456789
|
||||
OBSIDIAN_VAULT_PATH=/srv/obsidian/vault
|
||||
DAILY_NOTES_DIR=03 Journal
|
||||
NOTE_PATH_FORMAT=%Y/%m/%d.%m.%y.md
|
||||
MESSAGE_TIME_FORMAT=%H:%M
|
||||
TELEGRAM_PROXY=
|
||||
REMINDER_MIN_HOURS=2
|
||||
REMINDER_MAX_HOURS=3
|
||||
REMINDER_START_HOUR=7
|
||||
REMINDER_END_HOUR=23
|
||||
REMINDER_TIMEZONE=Europe/Moscow
|
||||
|
||||
- name: Copy memoir-bot env file when present locally
|
||||
ansible.builtin.copy:
|
||||
src: "{{ memoir_bot_env_source }}"
|
||||
dest: "{{ memoir_bot_env_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
when: lookup('ansible.builtin.fileglob', memoir_bot_env_source) | length > 0
|
||||
no_log: true
|
||||
|
||||
- name: Install GitHub SSH private key for memoir-bot
|
||||
ansible.builtin.copy:
|
||||
src: "{{ memoir_bot_ssh_private_key_file }}"
|
||||
dest: "{{ memoir_bot_state_dir }}/ssh/id_ed25519"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Install GitHub SSH public key for memoir-bot when present locally
|
||||
ansible.builtin.copy:
|
||||
src: "{{ memoir_bot_ssh_public_key_file }}"
|
||||
dest: "{{ memoir_bot_state_dir }}/ssh/id_ed25519.pub"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: lookup('ansible.builtin.fileglob', memoir_bot_ssh_public_key_file) | length > 0
|
||||
|
||||
- name: Scan GitHub SSH host key
|
||||
ansible.builtin.command: ssh-keyscan github.com
|
||||
register: memoir_bot_github_host_key
|
||||
changed_when: false
|
||||
|
||||
- name: Trust GitHub SSH host key for memoir-bot
|
||||
ansible.builtin.known_hosts:
|
||||
path: "{{ memoir_bot_state_dir }}/ssh/known_hosts"
|
||||
name: github.com
|
||||
key: "{{ memoir_bot_github_host_key.stdout }}"
|
||||
state: present
|
||||
|
||||
- name: Clone SecondBrain vault
|
||||
ansible.builtin.git:
|
||||
repo: "{{ memoir_bot_vault_repo }}"
|
||||
dest: "{{ memoir_bot_vault_dir }}"
|
||||
key_file: "{{ memoir_bot_state_dir }}/ssh/id_ed25519"
|
||||
accept_hostkey: true
|
||||
update: true
|
||||
version: main
|
||||
register: memoir_bot_vault_checkout
|
||||
|
||||
- name: Configure memoir-bot vault Git author name
|
||||
ansible.builtin.command: git config user.name memoir-bot
|
||||
args:
|
||||
chdir: "{{ memoir_bot_vault_dir }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Configure memoir-bot vault Git author email
|
||||
ansible.builtin.command: git config user.email memoir-bot@homelab.local
|
||||
args:
|
||||
chdir: "{{ memoir_bot_vault_dir }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Sync memoir-bot source code
|
||||
ansible.posix.synchronize:
|
||||
src: "{{ memoir_bot_source_dir }}"
|
||||
dest: "{{ memoir_bot_app_dir }}/"
|
||||
delete: true
|
||||
rsync_opts:
|
||||
- "--exclude=.env"
|
||||
- "--exclude=.git"
|
||||
- "--exclude=.venv"
|
||||
- "--exclude=__pycache__"
|
||||
- "--exclude=*.pyc"
|
||||
register: memoir_bot_source_sync
|
||||
|
||||
- name: Check if memoir-bot image exists
|
||||
ansible.builtin.command: "docker image inspect {{ memoir_bot_image }}"
|
||||
register: memoir_bot_image_inspect
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Build memoir-bot image
|
||||
ansible.builtin.command: "docker build -t {{ memoir_bot_image }} {{ memoir_bot_app_dir }}"
|
||||
when: memoir_bot_source_sync.changed or memoir_bot_image_inspect.rc != 0
|
||||
register: memoir_bot_image_build
|
||||
changed_when: memoir_bot_image_build.rc == 0
|
||||
|
||||
- name: Install memoir-bot systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/memoir-bot.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Memoir Telegram bot container
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
ConditionPathExists={{ memoir_bot_env_file }}
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
ExecStartPre=-/usr/bin/docker rm -f {{ memoir_bot_container_name }}
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name {{ memoir_bot_container_name }} \
|
||||
--env-file {{ memoir_bot_env_file }} \
|
||||
-v {{ memoir_bot_state_dir }}/vault:/srv/obsidian/vault \
|
||||
-v {{ memoir_bot_state_dir }}/ssh:/root/.ssh:ro \
|
||||
{{ memoir_bot_image }}
|
||||
ExecStop=/usr/bin/docker stop {{ memoir_bot_container_name }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: memoir_bot_unit
|
||||
|
||||
- name: Reload systemd when memoir-bot unit changes
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: memoir_bot_unit.changed
|
||||
|
||||
- name: Check memoir-bot env file
|
||||
ansible.builtin.stat:
|
||||
path: "{{ memoir_bot_env_file }}"
|
||||
register: memoir_bot_env
|
||||
|
||||
- name: Enable memoir-bot service
|
||||
ansible.builtin.systemd:
|
||||
name: memoir-bot
|
||||
enabled: true
|
||||
|
||||
- name: Start memoir-bot when env file exists
|
||||
ansible.builtin.systemd:
|
||||
name: memoir-bot
|
||||
state: "{{ 'restarted' if memoir_bot_source_sync.changed or memoir_bot_image_build.changed or memoir_bot_unit.changed or memoir_bot_vault_checkout.changed else 'started' }}"
|
||||
when: memoir_bot_env.stat.exists
|
||||
@@ -0,0 +1,332 @@
|
||||
---
|
||||
- name: Create mihomo LXC on mini-pc
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
vars:
|
||||
mihomo_vmid: 143
|
||||
mihomo_hostname: mihomo
|
||||
mihomo_ip: 192.168.1.27/24
|
||||
mihomo_gateway: 192.168.1.1
|
||||
mihomo_bridge: vmbr0
|
||||
mihomo_rootfs: local-lvm:8
|
||||
mihomo_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
mihomo_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
handlers:
|
||||
- name: restart mihomo lxc
|
||||
ansible.builtin.shell: "pct stop {{ mihomo_vmid }} || true; pct start {{ mihomo_vmid }}"
|
||||
changed_when: true
|
||||
tasks:
|
||||
- name: Check if mihomo LXC exists
|
||||
ansible.builtin.command: "pct config {{ mihomo_vmid }}"
|
||||
register: mihomo_pct_config
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign VMID 143
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mihomo_pct_config.rc != 0 or mihomo_existing_hostname == mihomo_hostname
|
||||
fail_msg: VMID 143 already exists and is not the Mihomo container.
|
||||
vars:
|
||||
mihomo_existing_hostname: >-
|
||||
{{ mihomo_pct_config.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Install mihomo LXC SSH public key on PVE host
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/mihomo-lxc.pub
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
content: "{{ lookup('file', mihomo_pubkey_file) }}\n"
|
||||
when: mihomo_pct_config.rc != 0
|
||||
|
||||
- name: Create mihomo LXC
|
||||
ansible.builtin.command: >-
|
||||
pct create {{ mihomo_vmid }} {{ mihomo_ostemplate }}
|
||||
--hostname {{ mihomo_hostname }}
|
||||
--rootfs {{ mihomo_rootfs }}
|
||||
--cores 1
|
||||
--memory 512
|
||||
--swap 512
|
||||
--net0 name=eth0,bridge={{ mihomo_bridge }},gw={{ mihomo_gateway }},ip={{ mihomo_ip }},firewall=1
|
||||
--nameserver 1.1.1.1
|
||||
--unprivileged 1
|
||||
--features nesting=1,keyctl=1
|
||||
--onboot 1
|
||||
--startup order=70
|
||||
--cmode shell
|
||||
--ssh-public-keys /tmp/mihomo-lxc.pub
|
||||
when: mihomo_pct_config.rc != 0
|
||||
|
||||
- name: Start mihomo LXC
|
||||
ansible.builtin.command: "pct start {{ mihomo_vmid }}"
|
||||
register: mihomo_pct_start
|
||||
changed_when: mihomo_pct_start.rc == 0
|
||||
failed_when: mihomo_pct_start.rc not in [0, 255]
|
||||
|
||||
- name: Allow FUSE device in mihomo LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ mihomo_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
||||
state: present
|
||||
notify: restart mihomo lxc
|
||||
|
||||
- name: Bind mount FUSE device in mihomo LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ mihomo_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
||||
state: present
|
||||
notify: restart mihomo lxc
|
||||
|
||||
- name: Allow TUN device in mihomo LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ mihomo_vmid }}.conf"
|
||||
line: "lxc.cgroup2.devices.allow: c 10:200 rwm"
|
||||
state: present
|
||||
notify: restart mihomo lxc
|
||||
|
||||
- name: Bind mount TUN device in mihomo LXC config
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/pve/lxc/{{ mihomo_vmid }}.conf"
|
||||
line: "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file"
|
||||
state: present
|
||||
notify: restart mihomo lxc
|
||||
|
||||
- name: Apply pending LXC config changes
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Wait for mihomo SSH through ru-vps
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 120
|
||||
delegate_to: mihomo
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
- name: Prepare mihomo runtime host
|
||||
hosts: mihomo
|
||||
gather_facts: true
|
||||
vars:
|
||||
ansible_become: false
|
||||
mihomo_config_dir: /opt/mihomo/config
|
||||
mihomo_ui_dir: /opt/mihomo/ui
|
||||
mihomo_image: metacubex/mihomo:v1.19.29@sha256:5e7bcc5e7a866afcc8b007ef827c9ba773f2f34b6d7311b6d39ed1751f37cfd5
|
||||
mihomo_ui_image: ghcr.io/metacubex/metacubexd:v1.270.6@sha256:156d55be885d4ba6254d840bd781b715c20c00afee6e6c24c76be4cfe5eb89d4
|
||||
mihomo_container_name: mihomo
|
||||
mihomo_ui_container_name: mihomo-ui
|
||||
tasks:
|
||||
- name: Install Docker and mihomo runtime packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker.io
|
||||
- fuse-overlayfs
|
||||
- ca-certificates
|
||||
- curl
|
||||
- git
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure Docker config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/docker
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure Docker storage driver for unprivileged LXC
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
{
|
||||
"storage-driver": "fuse-overlayfs"
|
||||
}
|
||||
register: docker_daemon_config
|
||||
|
||||
- name: Ensure Docker service is enabled and running
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: "{{ 'restarted' if docker_daemon_config.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Ensure mihomo directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
loop:
|
||||
- /opt/mihomo
|
||||
- "{{ mihomo_config_dir }}"
|
||||
- "{{ mihomo_ui_dir }}"
|
||||
|
||||
- name: Check if the configured Mihomo image is present
|
||||
ansible.builtin.command: "docker image inspect {{ mihomo_image }}"
|
||||
register: mihomo_image_inspect
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull the configured Mihomo image
|
||||
ansible.builtin.command: "docker pull {{ mihomo_image }}"
|
||||
when: mihomo_image_inspect.rc != 0
|
||||
register: mihomo_image_pull
|
||||
changed_when: true
|
||||
|
||||
- name: Check if the configured Mihomo UI image is present
|
||||
ansible.builtin.command: "docker image inspect {{ mihomo_ui_image }}"
|
||||
register: mihomo_ui_image_inspect
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull the configured Mihomo UI image
|
||||
ansible.builtin.command: "docker pull {{ mihomo_ui_image }}"
|
||||
when: mihomo_ui_image_inspect.rc != 0
|
||||
register: mihomo_ui_image_pull
|
||||
changed_when: true
|
||||
|
||||
- name: Install default mihomo config if missing
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ mihomo_config_dir }}/config.yaml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
force: false
|
||||
content: |
|
||||
mixed-port: 7890
|
||||
socks-port: 7891
|
||||
allow-lan: true
|
||||
bind-address: 0.0.0.0
|
||||
mode: rule
|
||||
log-level: info
|
||||
external-controller: 0.0.0.0:9090
|
||||
|
||||
dns:
|
||||
enable: true
|
||||
listen: 0.0.0.0:1053
|
||||
enhanced-mode: fake-ip
|
||||
nameserver:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
|
||||
proxies: []
|
||||
proxy-groups:
|
||||
- name: PROXY
|
||||
type: select
|
||||
proxies:
|
||||
- DIRECT
|
||||
|
||||
rules:
|
||||
- MATCH,DIRECT
|
||||
register: mihomo_config
|
||||
|
||||
- name: Install mihomo systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/mihomo.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Mihomo proxy container
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
ExecStartPre=-/usr/bin/docker rm -f {{ mihomo_container_name }}
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name {{ mihomo_container_name }} \
|
||||
--pull never \
|
||||
--cap-add NET_ADMIN \
|
||||
--device /dev/net/tun \
|
||||
-p 7890:7890 \
|
||||
-p 7891:7891 \
|
||||
-p 9090:9090 \
|
||||
-v {{ mihomo_config_dir }}:/root/.config/mihomo \
|
||||
{{ mihomo_image }}
|
||||
ExecStop=/usr/bin/docker stop {{ mihomo_container_name }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: mihomo_unit
|
||||
|
||||
- name: Install mihomo UI systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/mihomo-ui.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Mihomo MetaCubeXD web UI container
|
||||
After=docker.service mihomo.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
ExecStartPre=-/usr/bin/docker rm -f {{ mihomo_ui_container_name }}
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name {{ mihomo_ui_container_name }} \
|
||||
--pull never \
|
||||
-p 8080:80 \
|
||||
{{ mihomo_ui_image }}
|
||||
ExecStop=/usr/bin/docker stop {{ mihomo_ui_container_name }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
register: mihomo_ui_unit
|
||||
|
||||
- name: Reload systemd when mihomo units change
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: mihomo_unit.changed or mihomo_ui_unit.changed
|
||||
|
||||
- name: Enable and start mihomo
|
||||
ansible.builtin.systemd:
|
||||
name: mihomo
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if mihomo_config.changed or mihomo_unit.changed or mihomo_image_pull is changed else 'started' }}"
|
||||
|
||||
- name: Enable and start mihomo UI
|
||||
ansible.builtin.systemd:
|
||||
name: mihomo-ui
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if mihomo_ui_unit.changed or mihomo_ui_image_pull is changed else 'started' }}"
|
||||
|
||||
- name: Wait for Mihomo proxy TCP ports
|
||||
ansible.builtin.wait_for:
|
||||
host: 127.0.0.1
|
||||
port: "{{ item }}"
|
||||
state: started
|
||||
timeout: 120
|
||||
loop:
|
||||
- 7890
|
||||
- 7891
|
||||
- 9090
|
||||
|
||||
- name: Check Mihomo UI endpoint
|
||||
ansible.builtin.uri:
|
||||
url: http://127.0.0.1:8080/
|
||||
status_code: 200
|
||||
register: mihomo_ui_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: mihomo_ui_health.status == 200
|
||||
|
||||
- name: Check Mihomo controller /version endpoint
|
||||
ansible.builtin.uri:
|
||||
url: http://127.0.0.1:9090/version
|
||||
status_code: 200
|
||||
register: mihomo_controller_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: mihomo_controller_health.status == 200
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
- name: Create monitoring LXC on cloud-pc
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: false
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_become: false
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
pve_lxc_vmid: 146
|
||||
pve_lxc_node: cloud-pc
|
||||
pve_lxc_hostname: monitoring
|
||||
pve_lxc_ip: 192.168.1.30/24
|
||||
pve_lxc_gateway: 192.168.1.1
|
||||
pve_lxc_disk: data:24
|
||||
pve_lxc_cores: 2
|
||||
pve_lxc_memory: 4096
|
||||
pve_lxc_swap: 512
|
||||
pve_lxc_startup: order=80
|
||||
pve_lxc_features:
|
||||
- nesting=1
|
||||
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||
roles:
|
||||
- role: pve_lxc
|
||||
|
||||
- name: Enable Docker keyctl feature for monitoring LXC
|
||||
hosts: cloud-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read monitoring LXC configuration
|
||||
ansible.builtin.command: pct config 146
|
||||
register: monitoring_lxc_config
|
||||
changed_when: false
|
||||
|
||||
- name: Enable keyctl for Docker in monitoring LXC
|
||||
ansible.builtin.command: pct set 146 --features nesting=1,keyctl=1
|
||||
when: "'keyctl=1' not in monitoring_lxc_config.stdout"
|
||||
register: monitoring_lxc_keyctl
|
||||
|
||||
- name: Restart monitoring LXC after feature change
|
||||
ansible.builtin.command: pct reboot 146
|
||||
when: monitoring_lxc_keyctl.changed
|
||||
@@ -1,4 +1,4 @@
|
||||
- name: Create wg-mini LXC on mini-pc
|
||||
- name: Create ovpn-mini LXC on mini-pc
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
@@ -7,19 +7,19 @@
|
||||
vars:
|
||||
pve_lxc_vmid: 132
|
||||
pve_lxc_node: mini-pc
|
||||
pve_lxc_hostname: wg-mini
|
||||
pve_lxc_hostname: ovpn-mini
|
||||
pve_lxc_ip: 192.168.1.23/24
|
||||
pve_lxc_gateway: 192.168.1.1
|
||||
pve_lxc_storage: local-lvm
|
||||
pve_lxc_disk: local-lvm:8
|
||||
pve_lxc_ostemplate: "{{ lookup('env', 'PVE_LXC_OSTEMPLATE') | default('local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst', true) }}"
|
||||
|
||||
- name: Allow TUN device in wg-mini LXC config
|
||||
- name: Allow TUN device in ovpn-mini LXC config
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
become: true
|
||||
handlers:
|
||||
- name: restart wg-mini lxc
|
||||
- name: restart ovpn-mini lxc
|
||||
ansible.builtin.shell: pct stop 132 || true; pct start 132
|
||||
changed_when: true
|
||||
tasks:
|
||||
@@ -28,11 +28,11 @@
|
||||
path: /etc/pve/lxc/132.conf
|
||||
line: "lxc.cgroup2.devices.allow: c 10:200 rwm"
|
||||
state: present
|
||||
notify: restart wg-mini lxc
|
||||
notify: restart ovpn-mini lxc
|
||||
|
||||
- name: Bind mount /dev/net/tun
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pve/lxc/132.conf
|
||||
line: "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file"
|
||||
state: present
|
||||
notify: restart wg-mini lxc
|
||||
notify: restart ovpn-mini lxc
|
||||
@@ -1,3 +1,27 @@
|
||||
---
|
||||
- name: Guard Vaultwarden VMID before API updates
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read existing VMID 140 configuration
|
||||
ansible.builtin.command: pct config 140
|
||||
register: vaultwarden_existing_vmid
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign VMID 140
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vaultwarden_existing_vmid.rc != 0 or vaultwarden_existing_hostname == 'vaultwarden'
|
||||
fail_msg: VMID 140 already exists and is not the Vaultwarden container.
|
||||
vars:
|
||||
vaultwarden_existing_hostname: >-
|
||||
{{ vaultwarden_existing_vmid.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Create Vaultwarden LXC on mini-pc
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
@@ -84,7 +108,7 @@
|
||||
vars:
|
||||
ansible_become: false
|
||||
vaultwarden_data_dir: /opt/vaultwarden/data
|
||||
vaultwarden_image: vaultwarden/server:latest
|
||||
vaultwarden_image: vaultwarden/server:1.37.1@sha256:e9efdf001bf0d68c21f2cbfb8e1d9b5961a7ca9c85e0a7e58bf51a13b997d744
|
||||
vaultwarden_container_name: vaultwarden
|
||||
vaultwarden_http_port: 80
|
||||
tasks:
|
||||
@@ -133,6 +157,18 @@
|
||||
group: root
|
||||
mode: "0750"
|
||||
|
||||
- name: Check if the configured Vaultwarden image is present
|
||||
ansible.builtin.command: "docker image inspect {{ vaultwarden_image }}"
|
||||
register: vaultwarden_image_inspect
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Pull the configured Vaultwarden image
|
||||
ansible.builtin.command: "docker pull {{ vaultwarden_image }}"
|
||||
when: vaultwarden_image_inspect.rc != 0
|
||||
register: vaultwarden_image_pull
|
||||
changed_when: true
|
||||
|
||||
- name: Install Vaultwarden systemd unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/vaultwarden.service
|
||||
@@ -151,7 +187,7 @@
|
||||
ExecStartPre=-/usr/bin/docker rm -f {{ vaultwarden_container_name }}
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name {{ vaultwarden_container_name }} \
|
||||
--pull always \
|
||||
--pull never \
|
||||
-p {{ vaultwarden_http_port }}:80 \
|
||||
-v {{ vaultwarden_data_dir }}:/data \
|
||||
-e WEBSOCKET_ENABLED=true \
|
||||
@@ -170,5 +206,20 @@
|
||||
- name: Enable and start Vaultwarden
|
||||
ansible.builtin.systemd:
|
||||
name: vaultwarden
|
||||
state: started
|
||||
state: "{{ 'restarted' if vaultwarden_unit.changed or vaultwarden_image_pull.changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Wait for Vaultwarden HTTP endpoint
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ vaultwarden_http_port }}/"
|
||||
status_code: 200
|
||||
register: vaultwarden_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: vaultwarden_health.status == 200
|
||||
|
||||
- name: Verify Docker storage driver
|
||||
ansible.builtin.command: docker info --format '{{ "{{" }}.Driver{{ "}}" }}'
|
||||
register: vaultwarden_docker_driver
|
||||
changed_when: false
|
||||
failed_when: vaultwarden_docker_driver.stdout != 'fuse-overlayfs'
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
- name: Configure Grimmory reverse proxy on ru-vps
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
vars:
|
||||
caddy_dir: /opt/services/ru-vps/caddy
|
||||
caddyfile_path: /opt/services/ru-vps/caddy/Caddyfile
|
||||
grimmory_domain: books.ada-dev.ru
|
||||
grimmory_upstream: 192.168.1.34:6060
|
||||
tasks:
|
||||
- name: Ensure Caddy service directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ caddy_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure Grimmory Caddy site
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
create: true
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
marker: "# {mark} ANSIBLE MANAGED GRIMMORY SITE"
|
||||
block: |
|
||||
{{ grimmory_domain }} {
|
||||
@grimmory_opds_atom path /api/v1/opds /api/v1/opds/libraries /api/v1/opds/shelves /api/v1/opds/magic-shelves /api/v1/opds/authors /api/v1/opds/series /api/v1/opds/catalog /api/v1/opds/recent /api/v1/opds/surprise
|
||||
handle @grimmory_opds_atom {
|
||||
reverse_proxy {{ grimmory_upstream }} {
|
||||
header_up Accept "application/atom+xml"
|
||||
header_up Accept-Encoding identity
|
||||
transport http {
|
||||
compression off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@grimmory_opds_search path /api/v1/opds/search.opds
|
||||
handle @grimmory_opds_search {
|
||||
reverse_proxy {{ grimmory_upstream }} {
|
||||
header_up Accept "application/opensearchdescription+xml"
|
||||
header_up Accept-Encoding identity
|
||||
transport http {
|
||||
compression off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@grimmory_device_api path /api/koreader /api/koreader/* /api/v1/opds /api/v1/opds/*
|
||||
handle @grimmory_device_api {
|
||||
reverse_proxy {{ grimmory_upstream }} {
|
||||
header_up Accept-Encoding identity
|
||||
transport http {
|
||||
compression off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy {{ grimmory_upstream }}
|
||||
}
|
||||
}
|
||||
register: grimmory_caddy_site
|
||||
|
||||
- name: Validate host Caddy config
|
||||
ansible.builtin.command: caddy validate --config {{ caddyfile_path }}
|
||||
changed_when: false
|
||||
|
||||
- name: Check Caddy container mounted config
|
||||
ansible.builtin.command: docker exec caddy grep -F '@grimmory_device_api path /api/koreader' /etc/caddy/Caddyfile
|
||||
register: grimmory_container_caddyfile
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Restart Caddy when config changed or bind mount is stale
|
||||
ansible.builtin.command: docker restart caddy
|
||||
when: grimmory_caddy_site.changed or grimmory_container_caddyfile.rc != 0
|
||||
|
||||
- name: Validate Caddy container config after restart
|
||||
ansible.builtin.command: docker exec caddy caddy validate --config /etc/caddy/Caddyfile
|
||||
changed_when: false
|
||||
|
||||
- name: Check Grimmory upstream from ru-vps
|
||||
ansible.builtin.uri:
|
||||
url: http://{{ grimmory_upstream }}/api/v1/healthcheck
|
||||
status_code: 200
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
- name: Harden live Mihomo on ru-vps
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
vars:
|
||||
ru_vps_mihomo_harden_confirm: false
|
||||
mihomo_root: /opt/services/ru-vps/mihomo
|
||||
mihomo_config_path: "{{ mihomo_root }}/config/config.yaml"
|
||||
mihomo_compose_path: "{{ mihomo_root }}/docker-compose.yml"
|
||||
mihomo_backup_dir: /var/backups/ru-vps-mihomo
|
||||
mihomo_state_dir: /var/lib/ru-vps-mihomo
|
||||
mihomo_state_path: "{{ mihomo_state_dir }}/auth.json"
|
||||
mihomo_harden_script_path: /usr/local/sbin/ru-vps-mihomo-harden
|
||||
mihomo_harden_script_src: "{{ playbook_dir }}/../files/ru-vps-mihomo-harden.py"
|
||||
mihomo_backup_path: "{{ mihomo_backup_dir }}/config.yaml.{{ lookup('pipe', 'date -u +%Y%m%d%H%M%S') }}"
|
||||
pre_tasks:
|
||||
- name: Require explicit Mihomo hardening confirmation
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ru_vps_mihomo_harden_confirm | bool
|
||||
fail_msg: Run with -e ru_vps_mihomo_harden_confirm=true.
|
||||
|
||||
- name: Preflight Mihomo config file exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ mihomo_config_path }}"
|
||||
register: mihomo_config_stat
|
||||
|
||||
- name: Preflight Mihomo compose file exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ mihomo_compose_path }}"
|
||||
register: mihomo_compose_stat
|
||||
|
||||
- name: Preflight Mihomo state file exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ mihomo_state_path }}"
|
||||
register: mihomo_state_stat
|
||||
|
||||
- name: Refuse to run without live Mihomo config and compose files
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mihomo_config_stat.stat.exists
|
||||
- mihomo_compose_stat.stat.exists
|
||||
fail_msg: Live Mihomo config or compose file is missing.
|
||||
|
||||
- name: Preflight Mihomo container is running
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- inspect
|
||||
- -f
|
||||
- '{{ "{{" }}.State.Running{{ "}}" }}'
|
||||
- mihomo
|
||||
register: mihomo_container_state
|
||||
changed_when: false
|
||||
failed_when: mihomo_container_state.stdout.strip() != 'true'
|
||||
|
||||
- name: Preflight PyYAML is available on the target
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- -c
|
||||
- import yaml
|
||||
changed_when: false
|
||||
|
||||
- name: Preflight current Mihomo config validates
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- compose
|
||||
- -f
|
||||
- "{{ mihomo_compose_path }}"
|
||||
- exec
|
||||
- -T
|
||||
- mihomo
|
||||
- /mihomo
|
||||
- -t
|
||||
- -d
|
||||
- /root/.config/mihomo
|
||||
changed_when: false
|
||||
|
||||
- name: Ensure Mihomo backup directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mihomo_backup_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Install root-only Mihomo hardening helper
|
||||
ansible.builtin.copy:
|
||||
src: "{{ mihomo_harden_script_src }}"
|
||||
dest: "{{ mihomo_harden_script_path }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Create fresh backup of the live Mihomo config after validation
|
||||
ansible.builtin.copy:
|
||||
src: "{{ mihomo_config_path }}"
|
||||
dest: "{{ mihomo_backup_path }}"
|
||||
remote_src: true
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
tasks:
|
||||
- block:
|
||||
- name: Apply Mihomo hardening in place
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ mihomo_harden_script_path }}"
|
||||
- apply
|
||||
- --config
|
||||
- "{{ mihomo_config_path }}"
|
||||
- --state
|
||||
- "{{ mihomo_state_path }}"
|
||||
register: mihomo_harden_apply
|
||||
changed_when: (mihomo_harden_apply.stdout | from_json).changed
|
||||
no_log: true
|
||||
|
||||
- name: Validate hardened Mihomo config in the running container
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- compose
|
||||
- -f
|
||||
- "{{ mihomo_compose_path }}"
|
||||
- exec
|
||||
- -T
|
||||
- mihomo
|
||||
- /mihomo
|
||||
- -t
|
||||
- -d
|
||||
- /root/.config/mihomo
|
||||
changed_when: false
|
||||
|
||||
- name: Recreate Mihomo container after config hardening
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- compose
|
||||
- -f
|
||||
- "{{ mihomo_compose_path }}"
|
||||
- up
|
||||
- -d
|
||||
- --force-recreate
|
||||
- mihomo
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for hardened Mihomo listeners on loopback
|
||||
ansible.builtin.wait_for:
|
||||
host: 127.0.0.1
|
||||
port: "{{ item }}"
|
||||
state: started
|
||||
timeout: 30
|
||||
loop:
|
||||
- 7890
|
||||
- 7891
|
||||
|
||||
- name: Run authenticated Mihomo SOCKS probe to Telegram
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ mihomo_harden_script_path }}"
|
||||
- probe
|
||||
- --config
|
||||
- "{{ mihomo_config_path }}"
|
||||
- --state
|
||||
- "{{ mihomo_state_path }}"
|
||||
register: mihomo_harden_probe
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Check anonymous SOCKS access fails
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- curl
|
||||
- --silent
|
||||
- --show-error
|
||||
- --connect-timeout
|
||||
- "5"
|
||||
- --max-time
|
||||
- "15"
|
||||
- --proxy
|
||||
- socks5h://127.0.0.1:7891
|
||||
- https://api.telegram.org
|
||||
- --output
|
||||
- /dev/null
|
||||
register: mihomo_anon_probe
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Remove public Mihomo UFW rules after successful hardening
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ item.port }}"
|
||||
proto: "{{ item.proto }}"
|
||||
delete: true
|
||||
loop:
|
||||
- { port: 7890, proto: tcp }
|
||||
- { port: 7890, proto: udp }
|
||||
- { port: 7891, proto: tcp }
|
||||
- { port: 7891, proto: udp }
|
||||
|
||||
- name: Verify Mihomo listeners are loopback only
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ss
|
||||
- -H
|
||||
- -ltnp
|
||||
- '( sport = :7890 or sport = :7891 )'
|
||||
register: mihomo_ss
|
||||
changed_when: false
|
||||
|
||||
- name: Confirm hardened Mihomo is bound to loopback only and anonymous access fails
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mihomo_ss.stdout is search('127\\.0\\.0\\.1:7890')
|
||||
- mihomo_ss.stdout is search('127\\.0\\.0\\.1:7891')
|
||||
- mihomo_ss.stdout is not search('0\\.0\\.0\\.0:7890|:::7890|0\\.0\\.0\\.0:7891|:::7891')
|
||||
- mihomo_anon_probe.rc != 0
|
||||
fail_msg: Hardened Mihomo must listen on loopback only and reject anonymous SOCKS access.
|
||||
|
||||
rescue:
|
||||
- name: Restore the live Mihomo config from backup
|
||||
ansible.builtin.copy:
|
||||
src: "{{ mihomo_backup_path }}"
|
||||
dest: "{{ mihomo_config_path }}"
|
||||
remote_src: true
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
|
||||
- name: Recreate Mihomo container after rollback
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- compose
|
||||
- -f
|
||||
- "{{ mihomo_compose_path }}"
|
||||
- up
|
||||
- -d
|
||||
- --force-recreate
|
||||
- mihomo
|
||||
changed_when: true
|
||||
|
||||
- name: Remove newly created Mihomo credentials state after rollback
|
||||
ansible.builtin.file:
|
||||
path: "{{ mihomo_state_path }}"
|
||||
state: absent
|
||||
when: not mihomo_state_stat.stat.exists
|
||||
|
||||
- name: Wait for restored Mihomo listeners on loopback
|
||||
ansible.builtin.wait_for:
|
||||
host: 127.0.0.1
|
||||
port: "{{ item }}"
|
||||
state: started
|
||||
timeout: 30
|
||||
loop:
|
||||
- 7890
|
||||
- 7891
|
||||
|
||||
- name: Validate restored Mihomo config in the running container
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- compose
|
||||
- -f
|
||||
- "{{ mihomo_compose_path }}"
|
||||
- exec
|
||||
- -T
|
||||
- mihomo
|
||||
- /mihomo
|
||||
- -t
|
||||
- -d
|
||||
- /root/.config/mihomo
|
||||
changed_when: false
|
||||
|
||||
- name: Fail Mihomo hardening after restoring the backup
|
||||
ansible.builtin.fail:
|
||||
msg: Mihomo hardening failed and the live config was restored from backup.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Freeze Prometheus monitoring and configure Uptime Kuma
|
||||
hosts: monitoring_server
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: uptime_kuma
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
- name: Install user SSH public key on managed hosts
|
||||
hosts: servers
|
||||
gather_facts: false
|
||||
vars:
|
||||
homelab_user_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
homelab_user_pubkey: "{{ lookup('file', homelab_user_pubkey_file) }}"
|
||||
tasks:
|
||||
- name: Install user SSH key for connection user
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ ansible_user }}"
|
||||
key: "{{ homelab_user_pubkey }}"
|
||||
state: present
|
||||
|
||||
- name: Install user SSH key for root
|
||||
ansible.posix.authorized_key:
|
||||
user: root
|
||||
key: "{{ homelab_user_pubkey }}"
|
||||
state: present
|
||||
become: true
|
||||
when: ansible_user != 'root'
|
||||
|
||||
- name: Bootstrap user SSH public key into legacy LXC containers via Proxmox
|
||||
hosts: pve_nodes
|
||||
gather_facts: false
|
||||
vars:
|
||||
homelab_user_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
||||
homelab_user_pubkey: "{{ lookup('file', homelab_user_pubkey_file) }}"
|
||||
legacy_lxc_key_targets:
|
||||
- node: cloud-pc
|
||||
vmid: 120
|
||||
name: pbs
|
||||
tasks:
|
||||
- name: Install user SSH key for root inside legacy LXC
|
||||
ansible.builtin.shell: |
|
||||
pct exec {{ item.vmid }} -- sh -c '
|
||||
key=$(printf "%s" {{ homelab_user_pubkey | b64encode | quote }} | base64 -d)
|
||||
mkdir -p /root/.ssh
|
||||
chmod 700 /root/.ssh
|
||||
touch /root/.ssh/authorized_keys
|
||||
if grep -qxF "$key" /root/.ssh/authorized_keys; then
|
||||
echo present
|
||||
else
|
||||
printf "%s\n" "$key" >> /root/.ssh/authorized_keys
|
||||
echo added
|
||||
fi
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
'
|
||||
loop: "{{ legacy_lxc_key_targets }}"
|
||||
when: item.node == inventory_hostname
|
||||
register: legacy_lxc_key_install
|
||||
changed_when: "'added' in legacy_lxc_key_install.stdout"
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
- name: Create and verify Vaultwarden backup before update
|
||||
hosts: vaultwarden
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Create a fresh Vaultwarden offsite backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-restic-offsite-vaultwarden.service
|
||||
changed_when: true
|
||||
|
||||
- name: Run Vaultwarden offsite backup audit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-vaultwarden.service
|
||||
changed_when: true
|
||||
|
||||
- name: Create and verify Vaultwarden PBS backup before update
|
||||
hosts: mini-pc
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read existing VMID 140 configuration
|
||||
ansible.builtin.command: pct config 140
|
||||
register: vaultwarden_existing_vmid
|
||||
changed_when: false
|
||||
|
||||
- name: Refuse to modify a foreign VMID 140
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vaultwarden_existing_vmid.rc == 0
|
||||
- vaultwarden_existing_hostname == 'vaultwarden'
|
||||
fail_msg: VMID 140 already exists and is not the Vaultwarden container.
|
||||
vars:
|
||||
vaultwarden_existing_hostname: >-
|
||||
{{ vaultwarden_existing_vmid.stdout_lines
|
||||
| select('match', '^hostname: ')
|
||||
| map('regex_replace', '^hostname: ', '')
|
||||
| first
|
||||
| default('') }}
|
||||
|
||||
- name: Check for active Proxmox backup before Vaultwarden PBS backup
|
||||
ansible.builtin.command: pgrep -x vzdump
|
||||
register: vaultwarden_vzdump_preflight
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Require no active Proxmox backup before Vaultwarden PBS backup
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vaultwarden_vzdump_preflight.rc != 0
|
||||
fail_msg: >-
|
||||
A Proxmox backup is already running on mini-pc.
|
||||
Retry after the existing backup completes.
|
||||
|
||||
- name: Create a fresh Vaultwarden PBS backup
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- vzdump
|
||||
- "140"
|
||||
- --storage
|
||||
- pbs
|
||||
- --mode
|
||||
- snapshot
|
||||
- --prune-backups
|
||||
- keep-all=1
|
||||
- --exclude-path
|
||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||
|
||||
- name: Run current PBS backup audit on mini-pc
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- start
|
||||
- --wait
|
||||
- homelab-backup-audit-pbs.service
|
||||
changed_when: true
|
||||
|
||||
- import_playbook: pve-vaultwarden.yml
|
||||
|
||||
- name: Verify Vaultwarden public endpoint after update
|
||||
hosts: ru-vps
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Check Vaultwarden HTTPS endpoint
|
||||
ansible.builtin.uri:
|
||||
url: https://pass.ada-dev.ru/
|
||||
status_code: 200
|
||||
return_content: false
|
||||
register: vaultwarden_public_health
|
||||
retries: 24
|
||||
delay: 5
|
||||
until: vaultwarden_public_health.status == 200
|
||||
Reference in New Issue
Block a user