--- - name: Create mihomo LXC on mini-pc hosts: mini-pc gather_facts: false vars: # После переезда на OpenTofu (tofu/svc-mihomo.tf, VMID 159) этот play — # legacy: он таргетит СТАРЫЙ VMID 143 и его handler сделал бы pct start 143, # подняв остановленный откат на боевом адресе. Пропускаем, пока реестр # говорит provisioner: tofu. Для намеренного legacy rollback: # -e pve_mihomo_legacy_provisioning_enabled=true pve_mihomo_legacy_provisioning_enabled: >- {{ homelab_services['mihomo'].provisioner != 'tofu' }} 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 pre_tasks: - name: Skip legacy mihomo provisioning after cutover ansible.builtin.meta: end_play when: not (pve_mihomo_legacy_provisioning_enabled | bool) 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 # Таргет переопределяем ради blue-green переезда на OpenTofu: во время # миграции этот play нужно прогнать против нового контейнера на временном # адресе. Просто `--limit mihomo-new` для этого НЕ годится — лимит # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание # (проверено 2026-09-02 через --list-hosts на всех плейбуках). # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: # ansible-playbook playbooks/pve-mihomo.yml \ # -e pve_config_target=mihomo-new --limit mihomo-new # По умолчанию поведение не меняется. hosts: "{{ pve_config_target | default('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