Replace MySQL with SQLite, fix Jinja2 template syntax, and add README

This commit is contained in:
j
2026-03-07 19:50:14 +13:00
parent bd907c8d40
commit d8754a9d92
9 changed files with 104 additions and 58 deletions

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
# infmap - Infrastructure Map
A Dropshell template that provides a web dashboard showing the status of your servers. It SSHes into configured servers periodically to collect system information and displays it in an attractive dark-themed web UI.
## What It Collects
- **System**: hostname, OS, kernel, architecture, uptime
- **Hardware**: motherboard make/model/version, BIOS version/date
- **CPU**: model, cores, sockets, threads, live usage %
- **Memory**: total, used, available, live usage %
- **Storage**: physical disks, mounted filesystems with usage %
- **GPUs**: all detected graphics/3D/display adapters
- **Network**: all interfaces with IPv4/IPv6, MAC, state, speed, driver
- **Routing**: default gateway and interface
- **DNS**: configured nameservers
- **Tailscale**: IP and hostname (if installed)
All information is gathered without root access using `/sys/class/dmi/id/`, `lscpu`, `/proc/meminfo`, `lspci`, `ip addr`, etc.
## Architecture
Single Docker container running a Python Flask app:
- **Collector thread**: SSHes into servers on a schedule, runs a gather script, stores results in SQLite
- **Web server**: Serves the dashboard on a configurable HTTP port
Data is persisted in a Docker volume (`${CONTAINER_NAME}_data`).
## Setup
### 1. Create the service
```bash
dropshell create-service <server> infmap <service-name>
```
### 2. Configure
Edit `service.env`:
| Variable | Default | Description |
|---|---|---|
| `CONTAINER_NAME` | `infmap` | Docker container/project name |
| `SSH_USER` | `root` | Dropshell SSH user for this service |
| `WEB_PORT` | `8080` | HTTP port for the web dashboard |
| `SSH_KEY_PATH` | `/root/.ssh/id_ed25519` | Host path to SSH private key for connecting to monitored servers |
| `COLLECTION_INTERVAL` | `300` | Seconds between collection runs |
| `MAX_CONCURRENT_SSH` | `5` | Max simultaneous SSH connections |
Edit `infrastructure.conf` to define your servers:
```
Production
root@prod-web-01
root@prod-db-01
deploy@prod-app-01
Development
deploy@dev-01
deploy@dev-02
```
- Group names are freeform labels (no indentation)
- Servers are indented with `USERNAME@HOSTNAME`
- Lines starting with `#` are comments
### 3. Install
```bash
dropshell install <server> <service-name>
```
The dashboard will be available at `http://<server>:<WEB_PORT>`.
## Web Dashboard
- Servers displayed in cards grouped by group name, sorted by primary IP
- Each card shows hostname, IP, OS, and color-coded usage bars for CPU, RAM, and disk
- Green: < 60%
- Yellow: 60-75%
- Orange: 75-90%
- Red: > 90%
- Click a card to expand full hardware and network details
- Page auto-refreshes every 60 seconds
## API
- `GET /` - Web dashboard
- `GET /api/servers` - JSON array of all servers with full details

View File

@@ -7,8 +7,6 @@ from datetime import datetime, timezone
from concurrent.futures import ThreadPoolExecutor, as_completed
import paramiko
import pymysql
pymysql.install_as_MySQLdb()
from flask import Flask, render_template, jsonify
from flask_sqlalchemy import SQLAlchemy
@@ -16,12 +14,10 @@ from flask_sqlalchemy import SQLAlchemy
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
DB_PATH = '/app/data/infmap.db'
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = (
f"mysql+pymysql://{os.environ['MYSQL_USER']}:{os.environ['MYSQL_PASSWORD']}"
f"@{os.environ.get('MYSQL_HOST', 'db')}/{os.environ['MYSQL_DATABASE']}"
)
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'pool_recycle': 280, 'pool_pre_ping': True}
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}'
db = SQLAlchemy(app)
COLLECTION_INTERVAL = int(os.environ.get('COLLECTION_INTERVAL', 300))
@@ -349,19 +345,10 @@ def usage_color(percent):
# --- Main ---
if __name__ == '__main__':
# Wait for database
for attempt in range(30):
try:
with app.app_context():
db.create_all()
logger.info("Database ready")
break
except Exception as e:
logger.info("Waiting for database... (%s)", e)
time.sleep(2)
else:
logger.error("Could not connect to database after 30 attempts")
exit(1)
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
with app.app_context():
db.create_all()
logger.info("Database ready at %s", DB_PATH)
# Start collector thread
collector_thread = threading.Thread(target=collector_loop, daemon=True)

View File

@@ -92,11 +92,11 @@ done
# GPUs
gpu_idx=0
lspci 2>/dev/null | grep -iE 'vga|3d|display' | while read -r line; do
while read -r line; do
echo "[gpu:$gpu_idx]"
echo "description=$line"
gpu_idx=$((gpu_idx + 1))
done
done < <(lspci 2>/dev/null | grep -iE 'vga|3d|display')
# Network interfaces
for iface in $(ls /sys/class/net/ 2>/dev/null); do

View File

@@ -1,5 +1,4 @@
flask==3.1.*
flask-sqlalchemy==3.1.*
pymysql==1.1.*
paramiko==3.5.*
cryptography>=43.0

View File

@@ -142,7 +142,7 @@
<td>{{ iface.get('ipv6', '-') or '-' }}</td>
<td>{{ iface.get('mac', '-') or '-' }}</td>
<td>{{ iface.get('state', '-') }}</td>
<td>{% if iface.get('speed_mbps') %}{{ iface.speed_mbps }} Mbps{% else %}-{% endif %}</td>
<td>{% if iface.get('speed_mbps') %}{{ iface.get('speed_mbps') }} Mbps{% else %}-{% endif %}</td>
<td>{{ iface.get('driver', '-') or '-' }}</td>
</tr>
{% endfor %}
@@ -203,7 +203,7 @@
{% if d.get('error') %}
<div class="detail-section wide">
<h4>Error</h4>
<p class="error-text">{{ d.error }}</p>
<p class="error-text">{{ d.get('error') }}</p>
</div>
{% endif %}
</div>

View File

@@ -10,9 +10,3 @@ SSH_KEY_PATH=/root/.ssh/id_ed25519
# Collection settings
COLLECTION_INTERVAL=300
MAX_CONCURRENT_SSH=5
# MySQL credentials
MYSQL_ROOT_PASSWORD=changeme_root
MYSQL_DATABASE=infmap
MYSQL_USER=infmap
MYSQL_PASSWORD=changeme

View File

@@ -1,40 +1,18 @@
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 10
app:
build: ./app
ports:
- "${WEB_PORT}:5000"
environment:
MYSQL_HOST: db
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
COLLECTION_INTERVAL: ${COLLECTION_INTERVAL}
MAX_CONCURRENT_SSH: ${MAX_CONCURRENT_SSH}
volumes:
- ${SSH_KEY_PATH}:/app/ssh_key:ro
- ${CONFIG_PATH}/infrastructure.conf:/app/infrastructure.conf:ro
depends_on:
db:
condition: service_healthy
- app_data:/app/data
restart: unless-stopped
volumes:
db_data:
app_data:
external: true
name: ${CONTAINER_NAME}_db_data
name: ${CONTAINER_NAME}_data

View File

@@ -3,7 +3,7 @@ source "${AGENT_PATH}/common.sh"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
_check_required_env_vars "CONTAINER_NAME" "WEB_PORT" "SSH_KEY_PATH" "MYSQL_ROOT_PASSWORD" "MYSQL_DATABASE" "MYSQL_USER" "MYSQL_PASSWORD" "DATA_VOLUME"
_check_required_env_vars "CONTAINER_NAME" "WEB_PORT" "SSH_KEY_PATH" "DATA_VOLUME"
_check_docker_installed || _die "Docker test failed"
# Check SSH key exists

View File

@@ -2,4 +2,4 @@ REQUIRES_HOST_ROOT=false
REQUIRES_DOCKER=true
REQUIRES_DOCKER_ROOT=false
DATA_VOLUME="${CONTAINER_NAME}_db_data"
DATA_VOLUME="${CONTAINER_NAME}_data"