From d8754a9d92909df19f372953fabbe94aaf52260f Mon Sep 17 00:00:00 2001 From: j Date: Sat, 7 Mar 2026 19:50:14 +1300 Subject: [PATCH] Replace MySQL with SQLite, fix Jinja2 template syntax, and add README --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++ app/app.py | 27 ++++-------- app/gather_info.sh | 4 +- app/requirements.txt | 1 - app/templates/index.html | 4 +- config/service.env | 6 --- docker-compose.yml | 28 ++----------- install.sh | 2 +- template_info.env | 2 +- 9 files changed, 104 insertions(+), 58 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ecbd08e --- /dev/null +++ b/README.md @@ -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 infmap +``` + +### 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 +``` + +The dashboard will be available at `http://:`. + +## 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 diff --git a/app/app.py b/app/app.py index a9dea15..1932505 100644 --- a/app/app.py +++ b/app/app.py @@ -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) diff --git a/app/gather_info.sh b/app/gather_info.sh index af6026a..5d847db 100755 --- a/app/gather_info.sh +++ b/app/gather_info.sh @@ -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 diff --git a/app/requirements.txt b/app/requirements.txt index 965d8c6..0a5fe1f 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,4 @@ flask==3.1.* flask-sqlalchemy==3.1.* -pymysql==1.1.* paramiko==3.5.* cryptography>=43.0 diff --git a/app/templates/index.html b/app/templates/index.html index 54a29f0..cb2fdcb 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -142,7 +142,7 @@ {{ iface.get('ipv6', '-') or '-' }} {{ iface.get('mac', '-') or '-' }} {{ iface.get('state', '-') }} - {% if iface.get('speed_mbps') %}{{ iface.speed_mbps }} Mbps{% else %}-{% endif %} + {% if iface.get('speed_mbps') %}{{ iface.get('speed_mbps') }} Mbps{% else %}-{% endif %} {{ iface.get('driver', '-') or '-' }} {% endfor %} @@ -203,7 +203,7 @@ {% if d.get('error') %}

Error

-

{{ d.error }}

+

{{ d.get('error') }}

{% endif %} diff --git a/config/service.env b/config/service.env index 444fe32..ee5506c 100644 --- a/config/service.env +++ b/config/service.env @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 1498cf4..a29de3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/install.sh b/install.sh index ab48f7c..7fc55a5 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/template_info.env b/template_info.env index 2e2ef0f..33988df 100644 --- a/template_info.env +++ b/template_info.env @@ -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"