Replace MySQL with SQLite, fix Jinja2 template syntax, and add README
This commit is contained in:
88
README.md
Normal file
88
README.md
Normal 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
|
||||||
23
app/app.py
23
app/app.py
@@ -7,8 +7,6 @@ from datetime import datetime, timezone
|
|||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
import pymysql
|
|
||||||
pymysql.install_as_MySQLdb()
|
|
||||||
|
|
||||||
from flask import Flask, render_template, jsonify
|
from flask import Flask, render_template, jsonify
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
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')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DB_PATH = '/app/data/infmap.db'
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = (
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}'
|
||||||
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}
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
COLLECTION_INTERVAL = int(os.environ.get('COLLECTION_INTERVAL', 300))
|
COLLECTION_INTERVAL = int(os.environ.get('COLLECTION_INTERVAL', 300))
|
||||||
@@ -349,19 +345,10 @@ def usage_color(percent):
|
|||||||
# --- Main ---
|
# --- Main ---
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Wait for database
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
for attempt in range(30):
|
|
||||||
try:
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
logger.info("Database ready")
|
logger.info("Database ready at %s", DB_PATH)
|
||||||
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)
|
|
||||||
|
|
||||||
# Start collector thread
|
# Start collector thread
|
||||||
collector_thread = threading.Thread(target=collector_loop, daemon=True)
|
collector_thread = threading.Thread(target=collector_loop, daemon=True)
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ done
|
|||||||
|
|
||||||
# GPUs
|
# GPUs
|
||||||
gpu_idx=0
|
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 "[gpu:$gpu_idx]"
|
||||||
echo "description=$line"
|
echo "description=$line"
|
||||||
gpu_idx=$((gpu_idx + 1))
|
gpu_idx=$((gpu_idx + 1))
|
||||||
done
|
done < <(lspci 2>/dev/null | grep -iE 'vga|3d|display')
|
||||||
|
|
||||||
# Network interfaces
|
# Network interfaces
|
||||||
for iface in $(ls /sys/class/net/ 2>/dev/null); do
|
for iface in $(ls /sys/class/net/ 2>/dev/null); do
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
flask==3.1.*
|
flask==3.1.*
|
||||||
flask-sqlalchemy==3.1.*
|
flask-sqlalchemy==3.1.*
|
||||||
pymysql==1.1.*
|
|
||||||
paramiko==3.5.*
|
paramiko==3.5.*
|
||||||
cryptography>=43.0
|
cryptography>=43.0
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
<td>{{ iface.get('ipv6', '-') or '-' }}</td>
|
<td>{{ iface.get('ipv6', '-') or '-' }}</td>
|
||||||
<td>{{ iface.get('mac', '-') or '-' }}</td>
|
<td>{{ iface.get('mac', '-') or '-' }}</td>
|
||||||
<td>{{ iface.get('state', '-') }}</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>
|
<td>{{ iface.get('driver', '-') or '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
{% if d.get('error') %}
|
{% if d.get('error') %}
|
||||||
<div class="detail-section wide">
|
<div class="detail-section wide">
|
||||||
<h4>Error</h4>
|
<h4>Error</h4>
|
||||||
<p class="error-text">{{ d.error }}</p>
|
<p class="error-text">{{ d.get('error') }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,9 +10,3 @@ SSH_KEY_PATH=/root/.ssh/id_ed25519
|
|||||||
# Collection settings
|
# Collection settings
|
||||||
COLLECTION_INTERVAL=300
|
COLLECTION_INTERVAL=300
|
||||||
MAX_CONCURRENT_SSH=5
|
MAX_CONCURRENT_SSH=5
|
||||||
|
|
||||||
# MySQL credentials
|
|
||||||
MYSQL_ROOT_PASSWORD=changeme_root
|
|
||||||
MYSQL_DATABASE=infmap
|
|
||||||
MYSQL_USER=infmap
|
|
||||||
MYSQL_PASSWORD=changeme
|
|
||||||
|
|||||||
@@ -1,40 +1,18 @@
|
|||||||
services:
|
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:
|
app:
|
||||||
build: ./app
|
build: ./app
|
||||||
ports:
|
ports:
|
||||||
- "${WEB_PORT}:5000"
|
- "${WEB_PORT}:5000"
|
||||||
environment:
|
environment:
|
||||||
MYSQL_HOST: db
|
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
|
||||||
MYSQL_USER: ${MYSQL_USER}
|
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
|
||||||
COLLECTION_INTERVAL: ${COLLECTION_INTERVAL}
|
COLLECTION_INTERVAL: ${COLLECTION_INTERVAL}
|
||||||
MAX_CONCURRENT_SSH: ${MAX_CONCURRENT_SSH}
|
MAX_CONCURRENT_SSH: ${MAX_CONCURRENT_SSH}
|
||||||
volumes:
|
volumes:
|
||||||
- ${SSH_KEY_PATH}:/app/ssh_key:ro
|
- ${SSH_KEY_PATH}:/app/ssh_key:ro
|
||||||
- ${CONFIG_PATH}/infrastructure.conf:/app/infrastructure.conf:ro
|
- ${CONFIG_PATH}/infrastructure.conf:/app/infrastructure.conf:ro
|
||||||
depends_on:
|
- app_data:/app/data
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
app_data:
|
||||||
external: true
|
external: true
|
||||||
name: ${CONTAINER_NAME}_db_data
|
name: ${CONTAINER_NAME}_data
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ source "${AGENT_PATH}/common.sh"
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
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_docker_installed || _die "Docker test failed"
|
||||||
|
|
||||||
# Check SSH key exists
|
# Check SSH key exists
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ REQUIRES_HOST_ROOT=false
|
|||||||
REQUIRES_DOCKER=true
|
REQUIRES_DOCKER=true
|
||||||
REQUIRES_DOCKER_ROOT=false
|
REQUIRES_DOCKER_ROOT=false
|
||||||
|
|
||||||
DATA_VOLUME="${CONTAINER_NAME}_db_data"
|
DATA_VOLUME="${CONTAINER_NAME}_data"
|
||||||
|
|||||||
Reference in New Issue
Block a user