feat: infrastructure dashboard (Homepage) generated from the service registry
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s
playbooks/dashboard.yml deploys Homepage as a second compose stack on the monitoring LXC (CT 155) next to Uptime Kuma and renders its config from homelab_services: one tile per service, link to its UI, grouped by Proxmox node. Adding a service to the registry is enough — no second service list. - new registry consumer: playbooks/dashboard.yml + playbooks/templates/homepage-*.j2 - homelab_dashboard_* vars in group_vars/all/services.yml (top-level, like homelab_reverse_proxy_*); image pinned by digest, floating tag needs an explicit -e dashboard_allow_floating_tag=true - bootstrap-dashboard-pve-token.yml: read-only homepage@pve!dashboard token (PVEAuditor) for the Proxmox widget, secret in the root .env as DASHBOARD_PVE_* - Makefile: dashboard, dry-dashboard, bootstrap-dashboard-token - container binds the LAN address only (192.168.1.30:8082), not published via Caddy - docs: architecture.md Monitoring section, plan.md active task, consumer lists Deployed to CT 155 on 2026-09-03: container healthy, http://192.168.1.30:8082/ returns 200, `make dashboard` idempotent, `make validate` and `make lint` green. Pending operator steps: `make bootstrap-dashboard-token` (blocked in the agent session as credential creation) and an Uptime Kuma status page with slug homelab. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ca48ef2696
commit
05d8c748ab
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: {}
|
||||
@@ -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://<domain> (+ siteMonitor)
|
||||
# * иначе первый веб-порт -> http(s)://<ip>:<port> (+ 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 %}
|
||||
@@ -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
|
||||
@@ -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') }}
|
||||
@@ -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('') }}
|
||||
Reference in New Issue
Block a user