diff --git a/README.md b/README.md index f0c52c2..65a774c 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,19 @@ Edit `infrastructure.conf` to define your servers: ``` Production - infmap@prod-web-01 https://web01.example.com - infmap@prod-db-01 - infmap@prod-app-01 https://app01.example.com:8080 + prod-web-01 https://web01.example.com + prod-db-01 + prod-app-01 https://app01.example.com:8080 Development - infmap@dev-01 - infmap@dev-02 + dev-01 + dev-02 + admin@legacy-server ``` - Group names are freeform labels (no indentation) -- Servers are indented with `USERNAME@HOSTNAME [URL]` +- Servers are indented with `HOSTNAME`, `USERNAME@HOSTNAME`, or either followed by a URL +- Username defaults to `infmap` if not specified - An optional URL after the host adds a clickable link on the dashboard - Lines starting with `#` are comments diff --git a/app/app.py b/app/app.py index b344d60..f43f691 100644 --- a/app/app.py +++ b/app/app.py @@ -61,6 +61,9 @@ def parse_infrastructure_conf(): url = parts[1] if len(parts) > 1 else '' if '@' in entry: user, host = entry.split('@', 1) + else: + user, host = 'infmap', entry + if host: servers.append({ 'group': current_group or 'Default', 'username': user.strip(), diff --git a/app/gather_info.sh b/app/gather_info.sh index df8da0d..6cfc42d 100755 --- a/app/gather_info.sh +++ b/app/gather_info.sh @@ -156,4 +156,115 @@ else echo "installed=false" fi +# --- Container / VM autodiscovery --- + +# Helper: collect basic stats by execing into a container +# Usage: gather_container_stats +# e.g. gather_container_stats "pct exec 100 --" +gather_container_stats() { + local exec_cmd="$1" + + # Memory + local mem_total mem_avail mem_used mem_pct + mem_total=$($exec_cmd cat /proc/meminfo 2>/dev/null | awk '/MemTotal/{print int($2/1024)}') + mem_avail=$($exec_cmd cat /proc/meminfo 2>/dev/null | awk '/MemAvailable/{print int($2/1024)}') + if [ -n "$mem_total" ] && [ -n "$mem_avail" ] && [ "$mem_total" -gt 0 ] 2>/dev/null; then + mem_used=$((mem_total - mem_avail)) + mem_pct=$((mem_used * 100 / mem_total)) + echo "mem_total_mb=$mem_total" + echo "mem_used_mb=$mem_used" + echo "mem_percent=$mem_pct" + fi + + # Disk (rootfs) + local disk_info + disk_info=$($exec_cmd df -B1 / 2>/dev/null | tail -1) + if [ -n "$disk_info" ]; then + echo "disk_total=$(echo "$disk_info" | awk '{print $2}')" + echo "disk_used=$(echo "$disk_info" | awk '{print $3}')" + echo "disk_percent=$(echo "$disk_info" | awk '{gsub(/%/,""); print $5}')" + fi + + # IP + local ip + ip=$($exec_cmd hostname -I 2>/dev/null | awk '{print $1}') + [ -n "$ip" ] && echo "ip=$ip" + + # Uptime + local uptime_s + uptime_s=$($exec_cmd cut -d' ' -f1 /proc/uptime 2>/dev/null | cut -d. -f1) + [ -n "$uptime_s" ] && echo "uptime_seconds=$uptime_s" +} + +# Proxmox LXC (pct) +if command -v pct &>/dev/null; then + pct list 2>/dev/null | tail -n +2 | while read -r vmid status _ name _; do + [ -z "$vmid" ] && continue + echo "[container:pct-${vmid}]" + echo "type=lxc" + echo "platform=proxmox" + echo "id=$vmid" + echo "name=${name:-$vmid}" + echo "status=$status" + if [ "$status" = "running" ]; then + gather_container_stats "pct exec $vmid --" + fi + done +fi + +# Proxmox VMs (qm) +if command -v qm &>/dev/null; then + qm list 2>/dev/null | tail -n +2 | while read -r vmid name status _ mem _; do + [ -z "$vmid" ] && continue + echo "[container:qm-${vmid}]" + echo "type=vm" + echo "platform=proxmox" + echo "id=$vmid" + echo "name=${name:-$vmid}" + echo "status=$status" + [ -n "$mem" ] && echo "mem_allocated_mb=$mem" + # VM stats require guest agent - best effort + if [ "$status" = "running" ]; then + agent_test=$(qm guest exec "$vmid" -- cat /proc/meminfo 2>/dev/null) + if [ -n "$agent_test" ]; then + gather_container_stats "qm guest exec $vmid --" + fi + fi + done +fi + +# Plain LXC (lxc/lxd) +if command -v lxc &>/dev/null && ! command -v pct &>/dev/null; then + lxc list --format csv -c nsN 2>/dev/null | while IFS=',' read -r name status network; do + [ -z "$name" ] && continue + echo "[container:lxc-${name}]" + echo "type=lxc" + echo "platform=lxd" + echo "name=$name" + echo "status=$status" + if [ "$status" = "RUNNING" ]; then + gather_container_stats "lxc exec $name --" + lxd_ip=$(echo "$network" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1) + [ -n "$lxd_ip" ] && echo "ip=$lxd_ip" + fi + done +fi + +# libvirt VMs (virsh) +if command -v virsh &>/dev/null; then + virsh list --all --name 2>/dev/null | while read -r name; do + [ -z "$name" ] && continue + state=$(virsh domstate "$name" 2>/dev/null | head -1) + echo "[container:virsh-${name}]" + echo "type=vm" + echo "platform=libvirt" + echo "name=$name" + echo "status=$state" + if [ "$state" = "running" ]; then + virsh_ip=$(virsh domifaddr "$name" --source agent 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1) + [ -n "$virsh_ip" ] && echo "ip=$virsh_ip" + fi + done +fi + echo "[end]" diff --git a/app/static/style.css b/app/static/style.css index a2f72db..8c3dcb6 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -288,6 +288,70 @@ main { text-align: right; } +/* --- Container / VM Sub-cards --- */ + +.container-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; +} + +.container-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 8px; + padding: 10px; +} + +.container-card.offline { + opacity: 0.6; +} + +.ct-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.ct-name { + font-weight: 600; + font-size: 0.85rem; + color: #f1f5f9; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.ct-type { + font-size: 0.6rem; + font-weight: 600; + color: #64748b; + background: #334155; + padding: 1px 5px; + border-radius: 3px; + letter-spacing: 0.05em; + flex-shrink: 0; +} + +.ct-details { + display: flex; + gap: 10px; + font-size: 0.75rem; + color: #94a3b8; + margin-bottom: 6px; +} + +.ct-ip { + font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; +} + +.ct-status-label { + color: #64748b; + font-style: italic; +} + /* --- Empty State --- */ .empty-state { diff --git a/app/templates/index.html b/app/templates/index.html index f5c87db..1e2d17b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -234,6 +234,60 @@ {% endif %} + + {% set containers = d.get('container', []) if d.get('container') else [] %} + {% if containers %} +
+

Containers & VMs

+
+ {% for ct in containers %} + {% set ct_running = ct.get('status', '')|lower in ['running', 'started'] %} +
+
+ + {{ ct.get('name', ct.get('id', '?')) }} + {{ ct.get('type', '')|upper }} +
+ {% if ct_running %} +
+ {% if ct.get('ip') %} + {{ ct.get('ip') }} + {% endif %} + {% if ct.get('uptime_seconds') %} + {{ ct.get('uptime_seconds', '')|format_uptime }} + {% endif %} +
+ {% if ct.get('mem_percent') %} +
+
+ RAM +
+
+
+ {{ ct.get('mem_percent', '0') }}% +
+ {% if ct.get('disk_percent') %} +
+ DISK +
+
+
+ {{ ct.get('disk_percent', '0') }}% +
+ {% endif %} +
+ {% endif %} + {% else %} +
+ {{ ct.get('status', 'stopped') }} +
+ {% endif %} +
+ {% endfor %} +
+
+ {% endif %} + {% if d.get('error') %}

Error

diff --git a/infmap/check-config.sh b/infmap/check-config.sh new file mode 100755 index 0000000..dc48ebd --- /dev/null +++ b/infmap/check-config.sh @@ -0,0 +1,19 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" || exit 1 + +_check_required_env_vars "CONTAINER_NAME" "WEB_PORT" "SSH_KEY_PATH" + +# Check SSH key exists +[ -f "${SSH_KEY_PATH}" ] || _die "SSH key not found at ${SSH_KEY_PATH}" + +# Check infrastructure.conf exists and is non-empty +[ -f "${CONFIG_PATH}/infrastructure.conf" ] || _die "infrastructure.conf not found at ${CONFIG_PATH}/infrastructure.conf" +[ -s "${CONFIG_PATH}/infrastructure.conf" ] || _die "infrastructure.conf is empty" + +# Validate infrastructure.conf has at least one server entry +server_count=$(grep -cE '^\s+\S' "${CONFIG_PATH}/infrastructure.conf" 2>/dev/null || echo 0) +[ "$server_count" -gt 0 ] || _die "infrastructure.conf has no server entries" + +echo "Config OK: ${server_count} server(s) configured, SSH key present, port ${WEB_PORT}" diff --git a/infmap/reload-config.sh b/infmap/reload-config.sh new file mode 100755 index 0000000..e99fad2 --- /dev/null +++ b/infmap/reload-config.sh @@ -0,0 +1,11 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" || exit 1 + +_check_required_env_vars "CONTAINER_NAME" + +# Restart to pick up any config changes (infrastructure.conf, service.env) +docker compose -p "${CONTAINER_NAME}" restart || _die "Failed to restart ${CONTAINER_NAME}" + +echo "${CONTAINER_NAME} reloaded"