diff --git a/README.md b/README.md index 17017a5..3d64c43 100644 --- a/README.md +++ b/README.md @@ -2,45 +2,89 @@ Lightweight web GUI to manage SMB shares, NFS exports, and quotas on a single home server (Proxmox LXC/VM). Runs directly on the host as a systemd service. -## Requirements +## Proxmox LXC setup -``` -apt install samba nfs-kernel-server quota python3 python3-venv +Use a **privileged** Debian/Ubuntu container — required for the NFS server kernel module and quotas. + +On the **Proxmox host**, load the NFS kernel module before starting the container: + +```bash +modprobe nfsd +echo nfsd >> /etc/modules ``` ## Install -```bash -# Copy files -cp -r . /opt/micronas +Run everything below **inside the LXC container** as root. -# Create virtualenv and install deps +### 1. System dependencies + +```bash +apt update +apt install -y git samba nfs-kernel-server quota python3 python3-venv +``` + +### 2. Clone the repo + +```bash +git clone https://github.com/yourusername/micronas.git /opt/micronas +# or if you're self-hosting: +# git clone git@yourserver:micronas.git /opt/micronas +``` + +### 3. Python environment + +```bash python3 -m venv /opt/micronas/venv /opt/micronas/venv/bin/pip install flask gunicorn +``` -# Create environment file with your credentials +### 4. Credentials + +```bash cat > /etc/micronas.env <:5000` in your browser. + +## Updates + +```bash +cd /opt/micronas +git pull +systemctl restart micronas +``` + +## Network + +The service binds to `127.0.0.1:5000` by default. To reach it directly from the LAN, change `-b 127.0.0.1:5000` to `-b 0.0.0.0:5000` in `micronas.service`, then run `systemctl daemon-reload && systemctl restart micronas`. + +For HTTPS, put nginx in front as a reverse proxy. + +LAN or Tailscale only — never expose to the internet. ## Dev / test ```bash -MICRONAS_USER=admin MICRONAS_PASS=secret flask --app app run --debug -# or with docker-compose for a sandbox: +# Quick local run (no gunicorn needed): +MICRONAS_USER=admin MICRONAS_PASS=secret flask --app app.py run --debug --host=0.0.0.0 + +# Or with docker-compose (UI only — systemctl/samba calls won't work): docker compose up ``` ## Auth -Set `MICRONAS_USER` and `MICRONAS_PASS` in the environment (or `EnvironmentFile`). All routes require HTTP Basic Auth. Never expose this to the internet — LAN or Tailscale only. +All routes require HTTP Basic Auth. Credentials come from `MICRONAS_USER` / `MICRONAS_PASS` environment variables (set via `/etc/micronas.env` in production). If `MICRONAS_PASS` is not set the app starts unprotected and logs a warning — don't do this in production. diff --git a/app.py b/app.py index 4e6f4bb..31a8be5 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ import os import re +import shutil import subprocess import functools from pathlib import Path @@ -219,5 +220,106 @@ def set_quota(): return jsonify({'status': 'ok'}) +# ===== DASHBOARD ===== + +def _daemon_status(): + def is_active(service): + r = subprocess.run(['systemctl', 'is-active', service], capture_output=True, text=True) + return r.stdout.strip() == 'active' + return {'smbd': is_active('smbd'), 'nfsd': is_active('nfs-server')} + + +def _disk_usage(): + try: + u = shutil.disk_usage('/srv') + return { + 'path': '/srv', + 'total_gb': round(u.total / 1024**3, 1), + 'used_gb': round(u.used / 1024**3, 1), + 'free_gb': round(u.free / 1024**3, 1), + 'percent': round(u.used / u.total * 100, 1) if u.total else 0, + } + except Exception as e: + return {'error': str(e)} + + +def _du_bytes(path): + try: + r = subprocess.run(['du', '-sb', path], capture_output=True, text=True, timeout=15) + if r.returncode == 0: + parts = r.stdout.split() + return int(parts[0]) if parts else 0 + except Exception: + pass + return 0 + + +def _shares_with_usage(): + result = [] + for s in parse_smb(): + result.append({ + 'type': 'smb', + 'name': s['name'], + 'path': s['path'], + 'access': s['valid_users'], + 'size_gb': round(_du_bytes(s['path']) / 1024**3, 2) if s['path'] else 0, + }) + for s in get_nfs_shares(): + result.append({ + 'type': 'nfs', + 'name': None, + 'path': s['path'], + 'access': s['clients'], + 'size_gb': round(_du_bytes(s['path']) / 1024**3, 2) if s['path'] else 0, + }) + return result + + +def _quota_usage(): + try: + r = subprocess.run(['repquota', '-u', '/srv'], capture_output=True, text=True, timeout=10) + except FileNotFoundError: + return [] + + quotas = [] + past_sep = False + for line in r.stdout.splitlines(): + if line.startswith('---'): + past_sep = True + continue + if not past_sep: + continue + parts = line.split() + if len(parts) < 5 or not re.match(r'^[+-]{2}$', parts[1]): + continue + try: + used_kb = int(parts[2]) + soft_kb = int(parts[3]) + hard_kb = int(parts[4]) + except (ValueError, IndexError): + continue + if soft_kb == 0 and hard_kb == 0 and used_kb == 0: + continue + quotas.append({ + 'user': parts[0], + 'used_gb': round(used_kb / 1024**2, 2), + 'soft_gb': round(soft_kb / 1024**2, 2), + 'hard_gb': round(hard_kb / 1024**2, 2), + 'percent': round(used_kb / soft_kb * 100, 1) if soft_kb else 0, + }) + return quotas + + +@app.route('/api/dashboard', methods=['GET']) +@require_auth +def get_dashboard(): + return jsonify({ + 'status': _daemon_status(), + 'disk': _disk_usage(), + 'shares': _shares_with_usage(), + 'quotas': _quota_usage(), + }) + + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/index.html b/index.html index b74974d..57721c5 100644 --- a/index.html +++ b/index.html @@ -1,223 +1,374 @@ - + - SMB + NFS Manager + + MicroNAS -
-

📁 Share Manager (SMB + NFS)

- - +
+
+

MicroNAS

+ +
+ - -
-

SMB Shares

- -
-
- -
-
- -
-
- -
-
- -
-
+
-
- - - - - - - - - - -
NamePathValid UsersActions
+ +
+
+

Dashboard

+ +
+ + +
+
+
+
+
smbd
+ … +
+
+
+
+
+
+
nfsd
+ … +
+
+
+
+
+
+
+ /srv + … +
+
+
0%
+
+
+
+
+
+ + +
Shares
+
+ + + + + + + + + + + + + +
TypeNamePathAccessSize on disk
Loading…
+
+ + +
Quota Usage
+
+ + + + + + + + + + + + +
UserUsedSoft limitUsage
Loading…
+
-
- -
-

NFS Exports

- -
-
- + +
+

SMB Shares

+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + + + + +
NamePathValid UsersActions
-
- -
-
- -
-
- -
- - -
- - - - - - - - - - -
PathClientsOptionsActions
-
- -
-

Set Quotas

- -
-
- + +
+

NFS Exports

+ +
+ +
+
+ +
+
+
+ + +
+
+
+ +
+ +
+ + + + + +
PathClientsActions
-
- -
-
- -
-
- -
-
- -
- -
+
+ +
+

Set Quota

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ - - \ No newline at end of file +