diff --git a/AGENTS.md b/AGENTS.md index b618161..a5f0900 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,8 @@ break-glass восстановления и затем должны быть о - `playbooks/ru-vps-base.yml` — стек Caddy и его закреплённый образ; - `playbooks/pve-backup-jobs.yml` — списки VMID заданий PBS (поле `backup.job`); - `roles/backup_audit` — VMID для аудита (флаг `monitoring.backup_audit_vmid`); -- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox. +- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox; +- `playbooks/dashboard.yml` — `services.yaml` для Homepage-обзора инфраструктуры. Остальное (`pve-*.yml`, `status.yml`, monitoring, SSH config) по-прежнему дублирует значения и должно меняться согласованно. diff --git a/ansible/Makefile b/ansible/Makefile index 90adb69..b5ab643 100644 --- a/ansible/Makefile +++ b/ansible/Makefile @@ -191,6 +191,10 @@ bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с bootstrap-monitoring-token: ## Выпустить read-only PVE-токен для мониторинга $(ANSIBLE_PLAYBOOK) playbooks/bootstrap-monitoring-pve-token.yml $(EXTRA) +.PHONY: bootstrap-dashboard-token +bootstrap-dashboard-token: ## Выпустить read-only PVE-токен homepage@pve!dashboard для виджета Proxmox + $(ANSIBLE_PLAYBOOK) playbooks/bootstrap-dashboard-pve-token.yml $(EXTRA) + .PHONY: bootstrap-ansible-user bootstrap-ansible-user: ## Завести сервисный аккаунт ansible на shell-хостах (нужен sudo) $(ANSIBLE_PLAYBOOK) playbooks/bootstrap-ansible-user.yml $(ASK_BECOME) $(EXTRA) @@ -237,6 +241,14 @@ gyro: ## Настроить gyro-аллокатор в production CT 156 (Tofu-m uptime-kuma: ## Развернуть/обновить Uptime Kuma на monitoring LXC $(ANSIBLE_PLAYBOOK) playbooks/uptime-kuma.yml $(EXTRA) +.PHONY: dashboard +dashboard: ## Дашборд-обзор инфраструктуры (Homepage) на monitoring LXC, конфиг из реестра + @$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/dashboard.yml $(EXTRA) + +.PHONY: dry-dashboard +dry-dashboard: ## Предпросмотр dashboard.yml (--check --diff) + @$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/dashboard.yml --check --diff $(EXTRA) + .PHONY: offsite-restic offsite-restic: ## Настроить offsite restic-бэкапы на Yandex.Disk @$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/offsite-restic-yadisk.yml $(EXTRA) diff --git a/ansible/inventory/group_vars/all/services.yml b/ansible/inventory/group_vars/all/services.yml index 7b8d11d..4090901 100644 --- a/ansible/inventory/group_vars/all/services.yml +++ b/ansible/inventory/group_vars/all/services.yml @@ -19,6 +19,7 @@ # * playbooks/pve-backup-jobs.yml — списки VMID заданий по полю `backup.job`. # * roles/backup_audit — VMID по флагу `monitoring.backup_audit_vmid`. # * playbooks/validate.yml — сверка с фактическим состоянием Proxmox. +# * playbooks/dashboard.yml — services.yaml для Homepage-обзора инфры. # * Человек — как справочник вместо чтения семи playbook'ов по 200-400 строк. # # Существующие pve-*.yml пока НЕ читают этот реестр: их перевод на @@ -76,6 +77,35 @@ homelab_reverse_proxy_unit: homelab-caddy # Адрес центрального хоста мониторинга (источник scrape для node-exporter). homelab_monitoring_host_ip: 192.168.1.30 +# --- Дашборд-обзор инфраструктуры (Homepage, gethomepage.dev) ---------------- +# Потребитель реестра: playbooks/dashboard.yml рендерит services.yaml из +# homelab_services (плитка на сервис, ссылка на его UI, группировка по узлу). +# Живёт compose-стеком на LXC monitoring рядом с Uptime Kuma; наружу НЕ +# публикуется — доступ только из LAN и по OpenVPN. +homelab_dashboard_host: monitoring +homelab_dashboard_dir: /opt/homepage +homelab_dashboard_bind_ip: 192.168.1.30 +# Порт на хосте: 3000 в модели реестра занят замороженной Grafana, 3001 — +# Uptime Kuma, поэтому Homepage слушает 8082 -> контейнерный 3000. +homelab_dashboard_port: 8082 +# Образ закреплён по digest (закреплён 2026-09-03 после первого pull на CT 155). +# Обновление: docker pull <новый тег> на monitoring, взять digest +# docker inspect --format '{{ index .RepoDigests 0 }}' +# и вписать сюда. playbooks/dashboard.yml падает на assert'е без @sha256 +# (разовый обход — `-e dashboard_allow_floating_tag=true`). +homelab_dashboard_image: >- + ghcr.io/gethomepage/homepage:v2.2.0@sha256:753eeb0cc22ab7baad39ed47cbd1aae14e193dd1b264e965f193a9ea1d1e1bdd +# HOMEPAGE_ALLOWED_HOSTS: Homepage >=1.0 отклоняет запросы с прочих Host-ов. +homelab_dashboard_allowed_hosts: "192.168.1.30:8082" +# Виджет proxmox: URL любого узла кластера (без node: -> среднее по кластеру). +# Секрет — read-only токен homepage@pve!dashboard (роль PVEAuditor), выпускается +# playbooks/bootstrap-dashboard-pve-token.yml в корневой .env как DASHBOARD_PVE_*. +# Пустой URL -> плитка Proxmox не рендерится. +homelab_dashboard_proxmox_url: "https://192.168.1.10:8006" +# slug опубликованной Status Page в Uptime Kuma для виджета сводки up/down +# (мониторы и статус-страницы Kuma живут только в её UI — их тут не завести). +homelab_dashboard_kuma_slug: homelab + homelab_services: # -------------------------------------------------------------------------- diff --git a/ansible/playbooks/bootstrap-dashboard-pve-token.yml b/ansible/playbooks/bootstrap-dashboard-pve-token.yml new file mode 100644 index 0000000..115ced0 --- /dev/null +++ b/ansible/playbooks/bootstrap-dashboard-pve-token.yml @@ -0,0 +1,93 @@ +--- +# ============================================================================ +# Выпуск read-only Proxmox-токена для виджета Proxmox в Homepage-дашборде. +# +# Создаёт пользователя homepage@pve, роль PVEAuditor на / и privsep-токен +# homepage@pve!dashboard. Секрет пишется в КОРНЕВОЙ .env как +# DASHBOARD_PVE_API_USER / DASHBOARD_PVE_API_TOKEN_ID / +# DASHBOARD_PVE_API_TOKEN_SECRET — оттуда его читает playbooks/dashboard.yml +# через lookup('env', ...). +# +# Парный к playbooks/bootstrap-monitoring-pve-token.yml. Отдельный принципал, +# чтобы дашборд не зависел от кредов замороженного стека Prometheus. +# +# Запуск: make bootstrap-dashboard-token +# ============================================================================ +- name: Create read-only Proxmox token for the Homepage dashboard + hosts: mini-pc + gather_facts: false + vars: + dashboard_pve_user: homepage@pve + dashboard_pve_token_id: dashboard + # .env лежит в корне репозитория, playbook_dir — это ansible/playbooks. + dashboard_pve_env_file: "{{ playbook_dir }}/../../.env" + dashboard_pve_rotate_existing_token: false + tasks: + - name: Read existing Proxmox users + ansible.builtin.command: pveum user list --output-format json + register: dashboard_pve_users_raw + changed_when: false + + - name: Create the dashboard Proxmox user + ansible.builtin.command: >- + pveum user add {{ dashboard_pve_user }} + --comment 'Read-only Homepage dashboard user' + when: dashboard_pve_user not in (dashboard_pve_users_raw.stdout | from_json | map(attribute='userid') | list) + + - name: Grant PVEAuditor role to the dashboard user + ansible.builtin.command: >- + pveum acl modify / -user {{ dashboard_pve_user }} -role PVEAuditor + changed_when: false + + - name: Read the dashboard user tokens + ansible.builtin.command: >- + pveum user token list {{ dashboard_pve_user }} --output-format json + register: dashboard_pve_tokens_raw + changed_when: false + + - name: Refuse to overwrite an existing dashboard token + ansible.builtin.assert: + that: + - dashboard_pve_token_id not in (dashboard_pve_tokens_raw.stdout | from_json | map(attribute='tokenid') | list) + fail_msg: >- + Existing dashboard token secret cannot be recovered safely. Rotate it + explicitly (-e dashboard_pve_rotate_existing_token=true) before rerunning. + when: not dashboard_pve_rotate_existing_token | bool + + - name: Rotate the existing dashboard token explicitly + ansible.builtin.command: >- + pveum user token remove {{ dashboard_pve_user }} {{ dashboard_pve_token_id }} + when: + - dashboard_pve_rotate_existing_token | bool + - dashboard_pve_token_id in (dashboard_pve_tokens_raw.stdout | from_json | map(attribute='tokenid') | list) + + - name: Create the separated dashboard token + ansible.builtin.command: >- + pveum user token add {{ dashboard_pve_user }} {{ dashboard_pve_token_id }} + --privsep 1 --comment 'Homepage Proxmox widget' --output-format json + register: dashboard_pve_token_created + no_log: true + + - name: Grant PVEAuditor role to the separated dashboard token + ansible.builtin.command: >- + pveum acl modify / -token {{ dashboard_pve_user }}!{{ dashboard_pve_token_id }} -role PVEAuditor + changed_when: false + + - name: Store the dashboard token variables in the root .env + ansible.builtin.lineinfile: + path: "{{ dashboard_pve_env_file }}" + regexp: "^export {{ item.name }}=" + line: "export {{ item.name }}='{{ item.value }}'" + create: false + loop: + - name: DASHBOARD_PVE_API_USER + value: "{{ dashboard_pve_user }}" + - name: DASHBOARD_PVE_API_TOKEN_ID + value: "{{ dashboard_pve_token_id }}" + - name: DASHBOARD_PVE_API_TOKEN_SECRET + value: "{{ (dashboard_pve_token_created.stdout | from_json).value }}" + delegate_to: localhost + vars: + ansible_connection: local + ansible_become: false + no_log: true diff --git a/ansible/playbooks/dashboard.yml b/ansible/playbooks/dashboard.yml new file mode 100644 index 0000000..f2097b0 --- /dev/null +++ b/ansible/playbooks/dashboard.yml @@ -0,0 +1,131 @@ +--- +# ============================================================================ +# Дашборд-обзор всей инфраструктуры HomeLab (Homepage, gethomepage.dev). +# +# ОБЛАСТЬ ОТВЕТСТВЕННОСТИ +# Разворачивает compose-стек Homepage на LXC monitoring (рядом с Uptime +# Kuma) и ГЕНЕРИРУЕТ его конфиги из реестра homelab_services: одна плитка +# на сервис, ссылка на его UI, группировка по узлу Proxmox. Добавили +# сервис в реестр -> плитка появилась сама, второй список вести не нужно. +# +# Наружу дашборд НЕ публикуется: контейнер слушает только LAN-адрес CT 155, +# доступ из локальной сети или по OpenVPN. Внутренняя топология (IP, VMID, +# раскладка по нодам) на публичный периметр не выносится. +# +# ГРАНИЦА С reverse-proxy.yml +# Никакой общей области. Дашборд — отдельный стек на другом хосте, Caddyfile +# он не трогает. +# +# ЗАПУСК +# make dashboard (ansible-playbook playbooks/dashboard.yml) +# make dry-dashboard (--check --diff) +# Первый прогон, пока образ не закреплён по digest: +# ansible-playbook playbooks/dashboard.yml -e dashboard_allow_floating_tag=true +# +# СЕКРЕТЫ +# Виджет proxmox требует read-only токен homepage@pve!dashboard (роль +# PVEAuditor). Выпускается playbooks/bootstrap-dashboard-pve-token.yml, +# секрет кладётся в корневой .env как DASHBOARD_PVE_API_USER / +# DASHBOARD_PVE_API_TOKEN_ID / DASHBOARD_PVE_API_TOKEN_SECRET и подхватывается +# отсюда через lookup('env', ...). Без него дашборд работает — просто плитка +# "Proxmox кластер" показывает ошибку виджета, это не блокер. +# +# ТАРГЕТ +# По умолчанию homelab_dashboard_host (monitoring). Переопределяется ради +# blue-green: `-e dashboard_config_target=monitoring-new --limit monitoring-new` +# (голый --limit play не перенацеливает, а обнуляет — см. uptime-kuma.yml). +# ============================================================================ +- name: Deploy the HomeLab infrastructure dashboard (Homepage) on the monitoring LXC + hosts: "{{ dashboard_config_target | default(homelab_dashboard_host | default('monitoring')) }}" + gather_facts: true + + vars: + dash_root: "{{ homelab_dashboard_dir | default('/opt/homepage') }}" + dash_config_dir: "{{ (homelab_dashboard_dir | default('/opt/homepage')) ~ '/config' }}" + dash_bind: "{{ homelab_dashboard_bind_ip | default(expected_lan_ip) }}" + dash_port: "{{ homelab_dashboard_port | default(8082) }}" + dash_image: "{{ homelab_dashboard_image | default('') }}" + dash_allowed_hosts: >- + {{ homelab_dashboard_allowed_hosts | default(dash_bind ~ ':' ~ dash_port) }} + # Список узлов Proxmox, встречающихся в реестре, — по нему строятся группы. + dashboard_nodes: >- + {{ homelab_services | dict2items | map(attribute='value.node') + | unique | sort | list }} + # Секрет Proxmox-виджета из окружения (make load .env). Пусто -> плитка + # рендерится без данных. no_log на задаче, которая это пишет. + dash_pve_user: "{{ lookup('ansible.builtin.env', 'DASHBOARD_PVE_API_USER') }}" + dash_pve_token_id: "{{ lookup('ansible.builtin.env', 'DASHBOARD_PVE_API_TOKEN_ID') }}" + dash_pve_token_secret: "{{ lookup('ansible.builtin.env', 'DASHBOARD_PVE_API_TOKEN_SECRET') }}" + + pre_tasks: + - name: Require the dashboard registry variables + ansible.builtin.assert: + that: + - dash_image | length > 0 + - dash_bind | length > 0 + fail_msg: >- + Не заданы homelab_dashboard_* в + inventory/group_vars/all/services.yml. + + - name: Require the Homepage image to be pinned by digest + ansible.builtin.assert: + that: + - "'@sha256:' in dash_image or dashboard_allow_floating_tag | default(false) | bool" + fail_msg: >- + {{ dash_image }} не закреплён по digest. Выполни на + {{ inventory_hostname }}: + docker pull {{ dash_image }} + docker inspect --format '{{ '{{' }} index .RepoDigests 0 {{ '}}' }}' {{ dash_image }} + и пропиши tag@sha256 в homelab_dashboard_image. Разовый обход для + первого прогона: -e dashboard_allow_floating_tag=true + + - name: Ensure the Homepage config directory exists + ansible.builtin.file: + path: "{{ dash_config_dir }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Render Homepage configuration files from the registry + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ dash_config_dir }}/{{ item.dest }}" + owner: root + group: root + mode: "0644" + loop: + - {src: homepage-settings.yaml.j2, dest: settings.yaml} + - {src: homepage-services.yaml.j2, dest: services.yaml} + - {src: homepage-widgets.yaml.j2, dest: widgets.yaml} + - {src: homepage-bookmarks.yaml.j2, dest: bookmarks.yaml} + loop_control: + label: "{{ item.dest }}" + register: dash_config_files + + - name: Render the Homepage widget secrets file + ansible.builtin.template: + src: homepage.env.j2 + dest: "{{ dash_root }}/homepage.env" + owner: root + group: root + mode: "0600" + register: dash_env_file + no_log: true + + roles: + - role: compose_service + compose_service_name: homelab-homepage + compose_service_description: HomeLab infrastructure dashboard (Homepage) + compose_service_root: "{{ dash_root }}" + compose_service_root_mode: "0755" + compose_service_compose_file: docker-compose.yml + compose_service_compose_template: homepage-compose.yml.j2 + # | bool обязателен: роль фильтрует триггеры по truthiness, а строка + # "False" из "{{ ... is changed }}" тоже истинна. + compose_service_restart_triggers: + - "{{ (dash_config_files is changed) | bool }}" + - "{{ (dash_env_file is changed) | bool }}" + compose_service_health_url: "http://{{ dash_bind }}:{{ dash_port }}/" + compose_service_health_status: [200] + compose_service_health_follow_redirects: none diff --git a/ansible/playbooks/templates/homepage-bookmarks.yaml.j2 b/ansible/playbooks/templates/homepage-bookmarks.yaml.j2 new file mode 100644 index 0000000..9394658 --- /dev/null +++ b/ansible/playbooks/templates/homepage-bookmarks.yaml.j2 @@ -0,0 +1,16 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +# {{ ansible_managed }} +# Статические админ-ссылки (не из реестра). Рендерится playbooks/dashboard.yml. +- Управление: + - Proxmox cloud-pc: + - abbr: PVE + href: https://192.168.1.5:8006 + - Proxmox mini-pc: + - abbr: PVE + href: https://192.168.1.10:8006 + - Proxmox Backup Server: + - abbr: PBS + href: https://192.168.1.20:8007 + - Uptime Kuma: + - abbr: UK + href: http://{{ dash_bind }}:3001 diff --git a/ansible/playbooks/templates/homepage-compose.yml.j2 b/ansible/playbooks/templates/homepage-compose.yml.j2 new file mode 100644 index 0000000..5c4c5b4 --- /dev/null +++ b/ansible/playbooks/templates/homepage-compose.yml.j2 @@ -0,0 +1,21 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +# {{ ansible_managed }} +# Рендерится playbooks/dashboard.yml (роль compose_service). Правь шаблон, +# а не файл на хосте: следующий прогон перезапишет. +services: + homepage: + image: {{ dash_image }} + container_name: homelab-homepage + restart: unless-stopped + env_file: + - ./homepage.env + environment: + HOMEPAGE_ALLOWED_HOSTS: "{{ dash_allowed_hosts | trim }}" + ports: + # Только LAN-адрес CT 155 -> контейнерный 3000. Наружу не публикуется. + - "{{ dash_bind }}:{{ dash_port }}:3000" + volumes: + - ./config:/app/config + # Docker-сокет намеренно НЕ монтируется: сервисы живут на других хостах, + # а лишний доступ к сокету на LAN-видимом контейнере не нужен. +networks: {} diff --git a/ansible/playbooks/templates/homepage-services.yaml.j2 b/ansible/playbooks/templates/homepage-services.yaml.j2 new file mode 100644 index 0000000..80d4003 --- /dev/null +++ b/ansible/playbooks/templates/homepage-services.yaml.j2 @@ -0,0 +1,70 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +# {{ ansible_managed }} +# Рендерится playbooks/dashboard.yml ИЗ реестра homelab_services. НЕ править +# на хосте: меняй сервисы в ansible/inventory/group_vars/all/services.yml, +# затем `make dashboard`. +# +# Правило ссылки на сервис: +# * есть proxy.domain -> https:// (+ siteMonitor) +# * иначе первый веб-порт -> http(s)://: (+ siteMonitor для http) +# приоритет имён портов: http > ui > setup > uptime-kuma > grafana > +# controller > pbs-api +# * ни того, ни другого -> группа Headless, только ICMP ping по ip +{% set port_priority = ['http', 'ui', 'setup', 'uptime-kuma', 'grafana', 'controller', 'pbs-api'] %} +{% if homelab_dashboard_proxmox_url | default('') | length > 0 %} +- Infrastructure: + - Proxmox кластер: + description: cloud-pc + mini-pc + widget: + type: proxmox + url: {{ homelab_dashboard_proxmox_url }} + username: "{{ '{{HOMEPAGE_VAR_PROXMOX_USER}}' }}" + password: "{{ '{{HOMEPAGE_VAR_PROXMOX_TOKEN}}' }}" +{% endif %} +{% for node in dashboard_nodes %} +- {{ node }}: + {% for name, svc in homelab_services | dictsort %} + {% if svc.node == node %} + {% set proxy = svc.proxy | default({}) %} + {% set ns = namespace(scheme='', port=0) %} + {% for pref in port_priority %} + {% for p in svc.ports | default([]) %} + {% if ns.port == 0 and p.name == pref %} + {% set ns.port = p.port %} + {% set ns.scheme = 'https' if pref == 'pbs-api' else 'http' %} + {% endif %} + {% endfor %} + {% endfor %} + {% if proxy.domain is defined %} + - {{ name }}: + href: https://{{ proxy.domain }} + description: {{ svc.role }} + siteMonitor: https://{{ proxy.domain }} + {% elif ns.port > 0 %} + - {{ name }}: + href: {{ ns.scheme }}://{{ svc.ip }}:{{ ns.port }} + description: {{ svc.role }} + {% if ns.scheme == 'http' %} + siteMonitor: http://{{ svc.ip }}:{{ ns.port }} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} +{% endfor %} +- Headless: + {% for name, svc in homelab_services | dictsort %} + {% set proxy = svc.proxy | default({}) %} + {% set ns = namespace(web=false) %} + {% for pref in port_priority %} + {% for p in svc.ports | default([]) %} + {% if p.name == pref %} + {% set ns.web = true %} + {% endif %} + {% endfor %} + {% endfor %} + {% if proxy.domain is not defined and not ns.web %} + - {{ name }}: + description: {{ svc.role }} + ping: {{ svc.ip }} + {% endif %} + {% endfor %} diff --git a/ansible/playbooks/templates/homepage-settings.yaml.j2 b/ansible/playbooks/templates/homepage-settings.yaml.j2 new file mode 100644 index 0000000..62735d4 --- /dev/null +++ b/ansible/playbooks/templates/homepage-settings.yaml.j2 @@ -0,0 +1,21 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +# {{ ansible_managed }} +# Рендерится playbooks/dashboard.yml. Порядок и имена групп ОБЯЗАНЫ совпадать +# с группами в services.yaml — иначе Homepage покажет их в алфавитном порядке. +title: HomeLab +headerStyle: boxed +color: slate +layout: + {% if homelab_dashboard_proxmox_url | default('') | length > 0 %} + Infrastructure: + style: row + columns: 3 + {% endif %} + {% for node in dashboard_nodes %} + {{ node }}: + style: row + columns: 4 + {% endfor %} + Headless: + style: row + columns: 4 diff --git a/ansible/playbooks/templates/homepage-widgets.yaml.j2 b/ansible/playbooks/templates/homepage-widgets.yaml.j2 new file mode 100644 index 0000000..b2d90a1 --- /dev/null +++ b/ansible/playbooks/templates/homepage-widgets.yaml.j2 @@ -0,0 +1,18 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +# {{ ansible_managed }} +# Верхняя панель Homepage. Рендерится playbooks/dashboard.yml. +- datetime: + text_size: xl + format: + timeStyle: short + dateStyle: short +- resources: + label: monitoring CT + cpu: true + memory: true + disk: / +- uptimekuma: + # Требует опубликованной Status Page в Uptime Kuma с этим slug + # (мониторы и статус-страницы Kuma живут только в её UI). + url: http://{{ dash_bind }}:3001 + slug: {{ homelab_dashboard_kuma_slug | default('homelab') }} diff --git a/ansible/playbooks/templates/homepage.env.j2 b/ansible/playbooks/templates/homepage.env.j2 new file mode 100644 index 0000000..8661946 --- /dev/null +++ b/ansible/playbooks/templates/homepage.env.j2 @@ -0,0 +1,12 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +# {{ ansible_managed }} +# Секреты виджетов Homepage. mode 0600, в Git не попадает. Значения берутся +# из корневого .env через lookup('env', ...) в playbooks/dashboard.yml. +# Homepage подставляет их в widgets/services по имени {{ '{{HOMEPAGE_VAR_*}}' }}. +HOMEPAGE_VAR_PROXMOX_URL={{ homelab_dashboard_proxmox_url | default('') }} +{% if dash_pve_user | length > 0 and dash_pve_token_id | length > 0 %} +HOMEPAGE_VAR_PROXMOX_USER={{ dash_pve_user }}!{{ dash_pve_token_id }} +{% else %} +HOMEPAGE_VAR_PROXMOX_USER= +{% endif %} +HOMEPAGE_VAR_PROXMOX_TOKEN={{ dash_pve_token_secret | default('') }} diff --git a/docs/ai/architecture.md b/docs/ai/architecture.md index 3b4a7b2..31723e3 100644 --- a/docs/ai/architecture.md +++ b/docs/ai/architecture.md @@ -21,7 +21,8 @@ gates и вызывает playbooks. `ansible.cfg` выбирает inventory, r - `playbooks/ru-vps-base.yml` — стек Caddy и его закреплённый образ; - `playbooks/pve-backup-jobs.yml` — списки VMID заданий PBS (поле `backup.job`); - `roles/backup_audit` — VMID для аудита (флаг `monitoring.backup_audit_vmid`); -- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox. +- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox; +- `playbooks/dashboard.yml` — `services.yaml` для Homepage-обзора инфраструктуры. Остальное (`pve-*.yml`, `status.yml`, monitoring, SSH config) по-прежнему дублирует значения и должно меняться согласованно. @@ -114,6 +115,19 @@ Uptime Kuma в CT 146 является активным monitoring UI. Его р Exporter roles, groups и backup metrics остаются в репозитории. Hardcoded Prometheus targets могут расходиться с inventory, пока stack заморожен. +`playbooks/dashboard.yml` (`make dashboard`) поднимает Homepage вторым +compose-стеком на том же LXC `monitoring`: обзорная стартовая страница со +списком всех сервисов, ссылками на их UI и виджетами Proxmox/Uptime Kuma. +`services.yaml`, `settings.yaml`, `widgets.yaml`, `bookmarks.yaml` рендерятся +из `homelab_services` шаблонами `playbooks/templates/homepage-*.j2` — отдельный +список сервисов не ведётся. Контейнер слушает только LAN-адрес CT 155 +(`homelab_dashboard_bind_ip`), наружу через Caddy не публикуется. Виджет +Proxmox использует read-only токен `homepage@pve!dashboard` (роль `PVEAuditor`, +`make bootstrap-dashboard-token`), секрет — в корневом `.env` как +`DASHBOARD_PVE_*`. Образ Homepage закрепляется по digest как остальные active +images; до первого закрепления плейбук падает на assert'е (обход — +`-e dashboard_allow_floating_tag=true`). + ## Grimmory MCP `tools/grimmory-mcp/src/server.js` запускает локальный stdio MCP. Grimmory API diff --git a/docs/ai/plan.md b/docs/ai/plan.md index 4ae269a..0fe5b5f 100644 --- a/docs/ai/plan.md +++ b/docs/ai/plan.md @@ -2,6 +2,49 @@ ## Активные задачи +### Дашборд-обзор инфраструктуры (Homepage) + +Решение от 2026-09-03: поднять стартовую страницу-обзор всей инфры (список +сервисов + переходы + виджеты Proxmox/Uptime Kuma). + +Выбор: **Homepage** (gethomepage.dev) вторым compose-стеком на LXC +`monitoring` (CT 155) рядом с Uptime Kuma; доступ только LAN/OpenVPN, наружу +не публикуется. Конфиг Homepage **генерируется из `homelab_services`** — +`playbooks/dashboard.yml` + шаблоны `playbooks/templates/homepage-*.j2`, +новый потребитель реестра. Плитка на сервис: `href` = `https://` +для публичных, иначе `http(s)://:<порт>`; беспортовые — в группу Headless +с ICMP ping. + +**Развёрнуто на CT 155 2026-09-03.** Контейнер `homelab-homepage` (`Up healthy`), +`homelab-homepage.service` enabled, `http://192.168.1.30:8082/` -> 200. Прогон +`make dashboard` идемпотентен (третий заход `changed=0`), `make validate` и +`make lint` зелёные. Образ закреплён по digest +`sha256:753eeb0c…1e1bdd`. Порт 8082 (3000 в модели реестра занят замороженной +Grafana, 3001 — Kuma) -> контейнерный 3000. + +Состав: + +- `playbooks/dashboard.yml`, шаблоны `homepage-{settings,services,widgets,bookmarks}.yaml.j2`, + `homepage-compose.yml.j2`, `homepage.env.j2`; +- `playbooks/bootstrap-dashboard-pve-token.yml` — read-only токен + `homepage@pve!dashboard` (роль `PVEAuditor`), пишет `DASHBOARD_PVE_*` в корневой `.env`; +- реестр: блок `homelab_dashboard_*` в `group_vars/all/services.yml`, запись в + списках потребителей (`services.yml`, `AGENTS.md`, `architecture.md`); +- `Makefile`: цели `dashboard`, `dry-dashboard`, `bootstrap-dashboard-token`. + +Оставшиеся шаги оператора: + +1. `make bootstrap-dashboard-token` — выпустить PVE-токен для виджета Proxmox. + (В сессии-агенте был заблокирован auto-mode-классификатором как создание + credential; репозиторная часть готова, плейбук идемпотентен и сам допишет + `DASHBOARD_PVE_*` в корневой `.env`.) После — `make dashboard` заново, чтобы + `homepage.env` подхватил секрет. До этого плитка «Proxmox кластер» показывает + ошибку авторизации — не блокер. +2. Добавить те же три ключа-заглушки в `.env.example` (в сессии путь `.env*` + заблокирован в permissions). +3. В UI Uptime Kuma опубликовать Status Page со slug `homelab` — иначе виджет + сводки up/down показывает ошибку. Не блокер. + ### Вывод memoir-bot (CT 142) Решение от 2026-09-02: сервис не используется и выводится из эксплуатации.