--- - name: Развернуть nginx web gateway на Proxmox host hosts: proxmox become: true gather_facts: true vars: proxmox_web_gateway_public_hostname: "detmir.example.local" proxmox_web_gateway_root: "/var/www/proxmox-web-gateway" proxmox_web_gateway_site_name: "proxmox-web-gateway.conf" proxmox_web_gateway_site_path: "/etc/nginx/sites-available/{{ proxmox_web_gateway_site_name }}" proxmox_web_gateway_site_enabled_path: "/etc/nginx/sites-enabled/{{ proxmox_web_gateway_site_name }}" proxmox_web_gateway_tls_dir: "/etc/nginx/ssl/proxmox-web-gateway" proxmox_web_gateway_tls_cert_path: "{{ proxmox_web_gateway_tls_dir }}/fullchain.pem" proxmox_web_gateway_tls_key_path: "{{ proxmox_web_gateway_tls_dir }}/privkey.pem" proxmox_web_gateway_health_path: "/healthz" proxmox_web_gateway_auth_realm: "AWatch-rus portal 2026-06-06" proxmox_web_gateway_auth_user: "detmir" proxmox_web_gateway_auth_file: "/etc/nginx/proxmox-web-gateway.htpasswd" proxmox_web_gateway_credentials_file: "/etc/detmir/proxmox-web-gateway.credentials" proxmox_web_gateway_routes: - slug: "proxmox-gui" title: "Proxmox VE" category: "Host" description: "Основная панель управления Proxmox VE." target_url: "https://192.0.2.2:8006/" external_enabled: false - slug: "file1c-brief" title: "1C Executive Brief" category: "Management" description: "Сводка по предприятиям и рискам 1С." target_url: "http://192.0.2.2:8710/manager/brief" proxy_path: "/r/file1c/brief" proxy_target_url: "http://192.0.2.2:8710/manager/brief" external_enabled: true - slug: "file1c-actions" title: "1C Management Actions" category: "Management" description: "Очередь действий по предприятиям в 1С." target_url: "http://192.0.2.2:8710/manager/actions" proxy_path: "/r/file1c/actions" proxy_target_url: "http://192.0.2.2:8710/manager/actions" external_enabled: true - slug: "file1c-finance" title: "1C Financial Reporting" category: "Management" description: "Первый financial board по файловой 1С с разделением ledger/proxy." target_url: "http://192.0.2.11:3000/d/1c-file-finance/1c-file-financial-reporting?orgId=1" proxy_path: "/d/1c-file-finance/1c-file-financial-reporting?orgId=1" external_enabled: true - slug: "file1c-telemetry" title: "1C Telemetry Board" category: "Dashboards" description: "Read-only telemetry экран по состоянию файловых баз, reglog и host." target_url: "http://192.0.2.11:3000/d/1c-file-telemetry/1c-file-telemetry-board?orgId=1" proxy_path: "/d/1c-file-telemetry/1c-file-telemetry-board?orgId=1" external_enabled: true - slug: "grafana-1c" title: "Grafana 1C" category: "Dashboards" description: "Рабочий file-1c dashboard contour в внешней Grafana." target_url: "http://192.0.2.11:3000/d/1c-file-mgmt/1c-file-management-board?orgId=1" proxy_path: "/d/1c-file-mgmt/1c-file-management-board?orgId=1" external_enabled: true - slug: "clickhouse-http" title: "ClickHouse HTTP" category: "Data" description: "HTTP endpoint ClickHouse для file-1C analytics." target_url: "http://192.0.2.2:8123/" external_enabled: false - slug: "influxdb" title: "InfluxDB" category: "Data" description: "InfluxDB LXC на CT 200." target_url: "http://192.0.2.10:8086/" external_enabled: false - slug: "grafana-core" title: "Grafana Core" category: "Dashboards" description: "Отдельный Grafana CT 201." target_url: "http://192.0.2.11:3000/" proxy_path: "/dashboards" external_enabled: true - slug: "loki-alloy" title: "Grafana Alloy" category: "Logs" description: "Web UI Alloy на CT 202." target_url: "http://192.0.2.12:12345/" external_enabled: false - slug: "aw-ui" title: "AW-rus UI" category: "Operations" description: "Основной ActivityWatch-Russian Web UI на CT 203." target_url: "http://192.0.2.13:5600/" proxy_path: "/r/aw/" external_enabled: true - slug: "aw-worktime" title: "AW-rus Management Report" category: "Operations" description: "Управленческий worktime/report API на CT 203." target_url: "http://192.0.2.13:5610/reports/worktime/management?format=html&host=HOST-EXAMPLE" proxy_path: "/r/aw-worktime" proxy_target_url: "http://192.0.2.13:5610/reports/worktime/management?format=html&host=HOST-EXAMPLE" external_enabled: true - slug: "detmir-portal" title: "DetMir Portal" category: "Operations" description: "Единый read-only портал для оператора, руководителя и владельца." target_url: "http://127.0.0.1:8720/" proxy_path: "/portal/" external_enabled: true tasks: - name: Установить nginx ansible.builtin.package: name: - nginx - openssl state: present - name: Создать web root gateway ansible.builtin.file: path: "{{ proxmox_web_gateway_root }}" state: directory owner: root group: root mode: "0755" - name: Создать каталог TLS gateway ansible.builtin.file: path: "{{ proxmox_web_gateway_tls_dir }}" state: directory owner: root group: root mode: "0700" - name: Сгенерировать self-signed TLS сертификат gateway ansible.builtin.command: argv: - openssl - req - -x509 - -nodes - -newkey - rsa:2048 - -sha256 - -days - "3650" - -keyout - "{{ proxmox_web_gateway_tls_key_path }}" - -out - "{{ proxmox_web_gateway_tls_cert_path }}" - -subj - "/CN={{ proxmox_web_gateway_public_hostname }}" - -addext - "subjectAltName=DNS:{{ proxmox_web_gateway_public_hostname }}" args: creates: "{{ proxmox_web_gateway_tls_cert_path }}" - name: Проверить наличие gateway Basic Auth файла ansible.builtin.stat: path: "{{ proxmox_web_gateway_auth_file }}" register: proxmox_web_gateway_auth_stat - name: Проверить наличие gateway credential файла ansible.builtin.stat: path: "{{ proxmox_web_gateway_credentials_file }}" register: proxmox_web_gateway_credentials_stat - name: Создать согласованные gateway Basic Auth credential ansible.builtin.shell: | set -eu umask 077 password="$(openssl rand -base64 24 | tr -d '\n')" hash="$(openssl passwd -apr1 "$password")" printf '%s:%s\n' '{{ proxmox_web_gateway_auth_user }}' "$hash" > '{{ proxmox_web_gateway_auth_file }}' printf 'url=https://{{ proxmox_web_gateway_public_hostname }}/\nuser={{ proxmox_web_gateway_auth_user }}\npassword=%s\n' "$password" > '{{ proxmox_web_gateway_credentials_file }}' args: executable: /bin/sh no_log: true when: > not (proxmox_web_gateway_auth_stat.stat.exists | default(false)) or not (proxmox_web_gateway_credentials_stat.stat.exists | default(false)) - name: Зафиксировать права gateway Basic Auth файла ansible.builtin.file: path: "{{ proxmox_web_gateway_auth_file }}" owner: root group: www-data mode: "0640" - name: Зафиксировать права файла с gateway credential ansible.builtin.file: path: "{{ proxmox_web_gateway_credentials_file }}" owner: root group: root mode: "0600" - name: Прочитать gateway credential для локальной проверки ansible.builtin.slurp: src: "{{ proxmox_web_gateway_credentials_file }}" register: proxmox_web_gateway_credentials_slurp no_log: true - name: Подготовить gateway auth password для проверок ansible.builtin.set_fact: proxmox_web_gateway_auth_password: "{{ (proxmox_web_gateway_credentials_slurp.content | b64decode).split('password=')[1].split('\n')[0] }}" no_log: true - name: Развернуть index.html gateway ansible.builtin.template: src: "templates/proxmox-web-gateway-index.html.j2" dest: "{{ proxmox_web_gateway_root }}/index.html" owner: root group: root mode: "0644" - name: Развернуть nginx site gateway ansible.builtin.template: src: "templates/proxmox-web-gateway.conf.j2" dest: "{{ proxmox_web_gateway_site_path }}" owner: root group: root mode: "0644" notify: Reload nginx - name: Отключить default nginx site ansible.builtin.file: path: "/etc/nginx/sites-enabled/default" state: absent notify: Reload nginx - name: Включить proxmox web gateway site ansible.builtin.file: src: "{{ proxmox_web_gateway_site_path }}" dest: "{{ proxmox_web_gateway_site_enabled_path }}" state: link notify: Reload nginx - name: Включить и запустить nginx ansible.builtin.systemd: name: nginx enabled: true state: started - name: Применить handlers до проверок ansible.builtin.meta: flush_handlers - name: Проверить синтаксис nginx после выкладки site ansible.builtin.command: "/usr/sbin/nginx -t" register: proxmox_web_gateway_nginx_test changed_when: false - name: Проверить локальный HTTP redirect на HTTPS ansible.builtin.uri: url: "http://127.0.0.1/" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" follow_redirects: none status_code: 301 register: proxmox_web_gateway_http_redirect failed_when: - proxmox_web_gateway_http_redirect.status != 301 - proxmox_web_gateway_http_redirect.location != "https://{{ proxmox_web_gateway_public_hostname }}/" changed_when: false - name: Проверить старый /go путь без auth закрыт ansible.builtin.uri: url: "https://127.0.0.1/go/proxmox-gui" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" validate_certs: false follow_redirects: none status_code: 401 register: proxmox_web_gateway_redirect failed_when: - proxmox_web_gateway_redirect.status != 401 changed_when: false - name: Проверить index без auth закрыт ansible.builtin.uri: url: "https://127.0.0.1/" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" validate_certs: false status_code: 401 changed_when: false - name: Проверить index по public hostname с auth ansible.builtin.uri: url: "https://127.0.0.1/" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" url_username: "{{ proxmox_web_gateway_auth_user }}" url_password: "{{ proxmox_web_gateway_auth_password }}" force_basic_auth: true return_content: true validate_certs: false register: proxmox_web_gateway_named_index failed_when: - proxmox_web_gateway_named_index.status != 200 - proxmox_web_gateway_public_hostname not in proxmox_web_gateway_named_index.content changed_when: false - name: Проверить reverse proxy к 1C brief с auth ansible.builtin.uri: url: "https://127.0.0.1/r/file1c/brief" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" url_username: "{{ proxmox_web_gateway_auth_user }}" url_password: "{{ proxmox_web_gateway_auth_password }}" force_basic_auth: true validate_certs: false status_code: 200 changed_when: false - name: Проверить reverse proxy к Grafana health с auth ansible.builtin.uri: url: "https://127.0.0.1/r/grafana/api/health" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" url_username: "{{ proxmox_web_gateway_auth_user }}" url_password: "{{ proxmox_web_gateway_auth_password }}" force_basic_auth: true validate_certs: false status_code: 200 changed_when: false - name: Проверить reverse proxy к AW info с auth ansible.builtin.uri: url: "https://127.0.0.1/r/aw/api/0/info" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" url_username: "{{ proxmox_web_gateway_auth_user }}" url_password: "{{ proxmox_web_gateway_auth_password }}" force_basic_auth: true validate_certs: false status_code: 200 changed_when: false - name: Проверить reverse proxy к DetMir Portal health с auth ansible.builtin.uri: url: "https://127.0.0.1/portal/api/health" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" url_username: "{{ proxmox_web_gateway_auth_user }}" url_password: "{{ proxmox_web_gateway_auth_password }}" force_basic_auth: true validate_certs: false status_code: 200 return_content: true register: proxmox_web_gateway_portal_health failed_when: - proxmox_web_gateway_portal_health.status != 200 - "'sources' not in proxmox_web_gateway_portal_health.content" changed_when: false - name: Проверить browser-origin AW API query без 403 ansible.builtin.uri: url: "https://127.0.0.1/api/0/query/" method: POST headers: Host: "{{ proxmox_web_gateway_public_hostname }}" Origin: "https://{{ proxmox_web_gateway_public_hostname }}" Referer: "https://{{ proxmox_web_gateway_public_hostname }}/r/aw/" Content-Type: "application/json" body_format: json body: timeperiods: - "1970-01-01T00:00:00+00:00/1970-01-01T00:01:00+00:00" query: - "RETURN = [];" url_username: "{{ proxmox_web_gateway_auth_user }}" url_password: "{{ proxmox_web_gateway_auth_password }}" force_basic_auth: true validate_certs: false status_code: 200 return_content: true register: proxmox_web_gateway_aw_query failed_when: - proxmox_web_gateway_aw_query.status != 200 - proxmox_web_gateway_aw_query.content != "[[]]" changed_when: false - name: Проверить локальный HTTPS health endpoint gateway ansible.builtin.uri: url: "https://127.0.0.1{{ proxmox_web_gateway_health_path }}" headers: Host: "{{ proxmox_web_gateway_public_hostname }}" return_content: true validate_certs: false register: proxmox_web_gateway_https_health failed_when: - proxmox_web_gateway_https_health.status != 200 - "'ok' not in proxmox_web_gateway_https_health.content" changed_when: false handlers: - name: Reload nginx ansible.builtin.systemd: name: nginx state: reloaded