Add container/VM autodiscovery, optional username in config, and management scripts
This commit is contained in:
14
README.md
14
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
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 <exec_prefix>
|
||||
# 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]"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -234,6 +234,60 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Containers / VMs -->
|
||||
{% set containers = d.get('container', []) if d.get('container') else [] %}
|
||||
{% if containers %}
|
||||
<div class="detail-section wide">
|
||||
<h4>Containers & VMs</h4>
|
||||
<div class="container-grid">
|
||||
{% for ct in containers %}
|
||||
{% set ct_running = ct.get('status', '')|lower in ['running', 'started'] %}
|
||||
<div class="container-card {% if not ct_running %}offline{% endif %}">
|
||||
<div class="ct-header">
|
||||
<span class="status-dot {% if ct_running %}online{% else %}offline{% endif %}"></span>
|
||||
<span class="ct-name">{{ ct.get('name', ct.get('id', '?')) }}</span>
|
||||
<span class="ct-type">{{ ct.get('type', '')|upper }}</span>
|
||||
</div>
|
||||
{% if ct_running %}
|
||||
<div class="ct-details">
|
||||
{% if ct.get('ip') %}
|
||||
<span class="ct-ip">{{ ct.get('ip') }}</span>
|
||||
{% endif %}
|
||||
{% if ct.get('uptime_seconds') %}
|
||||
<span class="ct-uptime">{{ ct.get('uptime_seconds', '')|format_uptime }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if ct.get('mem_percent') %}
|
||||
<div class="usage-bars">
|
||||
<div class="usage-row">
|
||||
<span class="usage-label">RAM</span>
|
||||
<div class="usage-bar-bg">
|
||||
<div class="usage-bar-fill" style="width: {{ ct.get('mem_percent', 0) }}%; background: {{ ct.get('mem_percent', '0')|float|usage_color }};"></div>
|
||||
</div>
|
||||
<span class="usage-pct">{{ ct.get('mem_percent', '0') }}%</span>
|
||||
</div>
|
||||
{% if ct.get('disk_percent') %}
|
||||
<div class="usage-row">
|
||||
<span class="usage-label">DISK</span>
|
||||
<div class="usage-bar-bg">
|
||||
<div class="usage-bar-fill" style="width: {{ ct.get('disk_percent', 0) }}%; background: {{ ct.get('disk_percent', '0')|float|usage_color }};"></div>
|
||||
</div>
|
||||
<span class="usage-pct">{{ ct.get('disk_percent', '0') }}%</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="ct-details">
|
||||
<span class="ct-status-label">{{ ct.get('status', 'stopped') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if d.get('error') %}
|
||||
<div class="detail-section wide">
|
||||
<h4>Error</h4>
|
||||
|
||||
19
infmap/check-config.sh
Executable file
19
infmap/check-config.sh
Executable file
@@ -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}"
|
||||
11
infmap/reload-config.sh
Executable file
11
infmap/reload-config.sh
Executable file
@@ -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"
|
||||
Reference in New Issue
Block a user