You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

243 lines
7.4 KiB

from flask import Flask, jsonify, request, redirect, url_for, render_template, session
from flask_cors import CORS
import docker
import os
import json
from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash
import psutil
import logging
# Helper: load password hash from auth file or env
PROJECT_ROOT = os.path.dirname(__file__)
AUTH_FILE = os.path.join(PROJECT_ROOT, 'auth.json')
SECRET_FILE = os.path.join(PROJECT_ROOT, '.secret_key')
# Setup logging (console + file) — placed after PROJECT_ROOT is known
LOGFILE = os.path.join(PROJECT_ROOT, 'webui.log')
logger = logging.getLogger('docker-web-ui')
if not logger.handlers:
logger.setLevel(logging.INFO)
fh = logging.FileHandler(LOGFILE)
fh.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
fh.setFormatter(fmt)
ch.setFormatter(fmt)
logger.addHandler(fh)
logger.addHandler(ch)
def load_password_hash():
# priority: auth.json file, then env ADMIN_PASSWORD_HASH, then env ADMIN_PASSWORD
if os.path.exists(AUTH_FILE):
try:
with open(AUTH_FILE, 'r') as f:
data = json.load(f)
return data.get('password_hash')
except Exception:
return None
ph = os.environ.get('ADMIN_PASSWORD_HASH')
if ph:
return ph
pwd = os.environ.get('ADMIN_PASSWORD')
if pwd:
return generate_password_hash(pwd)
return None
def get_secret_key():
# persistent secret key for sessions
if os.path.exists(SECRET_FILE):
with open(SECRET_FILE, 'rb') as f:
return f.read()
# generate and persist
v = os.urandom(24)
try:
with open(SECRET_FILE, 'wb') as f:
f.write(v)
except Exception:
pass
return v
app = Flask(__name__, static_folder='static', static_url_path='/')
CORS(app)
# set secret key
app.secret_key = os.environ.get('FLASK_SECRET_KEY') or get_secret_key()
# load password hash
PASSWORD_HASH = load_password_hash()
# NOTE: dynamic logo lookup and external API usage removed.
# The frontend will use local static files in `static/game_logos/<slug>.png` and
# fall back to `/default_game.svg` when a local image is not present.
def login_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not session.get('logged_in'):
return redirect(url_for('login', next=request.path))
return f(*args, **kwargs)
return wrapper
try:
client = docker.from_env()
except Exception:
client = None
@app.route('/')
@login_required
def index():
return app.send_static_file('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
global PASSWORD_HASH
if request.method == 'POST':
pwd = request.form.get('password', '')
if PASSWORD_HASH and check_password_hash(PASSWORD_HASH, pwd):
session['logged_in'] = True
nxt = request.args.get('next') or url_for('index')
return redirect(nxt)
else:
return render_template('login.html', error='Mot de passe invalide')
return render_template('login.html', error=None)
@app.route('/logout')
def logout():
session.pop('logged_in', None)
return redirect(url_for('login'))
@app.route('/api/containers', methods=['GET'])
@login_required
def list_containers():
if client is None:
return jsonify({'error': 'Docker client not available'}), 500
containers = client.containers.list(all=True)
res = []
for c in containers:
tag = None
try:
tag = c.image.tags[0] if c.image.tags else None
except Exception:
tag = None
res.append({
'id': c.id,
'short_id': c.id[:12],
'name': c.name,
'status': c.status,
'image': tag or str(c.image),
})
return jsonify(res)
@app.route('/api/containers/<id_or_name>/<action>', methods=['POST'])
@login_required
def container_action(id_or_name, action):
if client is None:
return jsonify({'error': 'Docker client not available'}), 500
try:
container = client.containers.get(id_or_name)
except Exception as e:
return jsonify({'error': f'Container not found: {e}'}), 404
try:
if action == 'start':
container.start()
elif action == 'stop':
container.stop()
elif action == 'restart':
container.restart()
else:
return jsonify({'error': 'Invalid action'}), 400
except Exception as e:
return jsonify({'error': str(e)}), 500
return jsonify({'result': 'ok', 'id': container.id, 'status': container.status})
@app.route('/api/containers/<id_or_name>/logs', methods=['GET'])
@login_required
def container_logs(id_or_name):
if client is None:
return jsonify({'error': 'Docker client not available'}), 500
tail = request.args.get('tail') or request.args.get('lines') or '200'
try:
c = client.containers.get(id_or_name)
# get last `tail` lines (docker SDK accepts tail as int)
logs = c.logs(tail=int(tail), stdout=True, stderr=True)
if isinstance(logs, bytes):
logs = logs.decode('utf-8', errors='replace')
return jsonify({'logs': logs})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<id_or_name>/exec', methods=['POST'])
@login_required
def container_exec(id_or_name):
if client is None:
return jsonify({'error': 'Docker client not available'}), 500
data = request.get_json() or {}
cmd = data.get('cmd') or data.get('command')
if not cmd:
return jsonify({'error': 'No command provided'}), 400
try:
c = client.containers.get(id_or_name)
# exec_run may return an ExecResult or (exit_code, output)
res = c.exec_run(cmd, stdout=True, stderr=True)
exit_code = None
output = ''
try:
exit_code = getattr(res, 'exit_code', None)
output = getattr(res, 'output', None)
except Exception:
pass
if exit_code is None:
# try tuple form
try:
exit_code, output = res
except Exception:
exit_code = -1
if isinstance(output, bytes):
output = output.decode('utf-8', errors='replace')
return jsonify({'exit_code': exit_code, 'output': output})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/stats', methods=['GET'])
@login_required
def system_stats():
# return CPU%, RAM%, and temperatures (if available)
try:
cpu = psutil.cpu_percent(interval=0.1)
vm = psutil.virtual_memory()
mem = vm.percent
temps = {}
try:
t = psutil.sensors_temperatures()
for k, v in (t or {}).items():
# take first readable entry
if v:
temps[k] = []
for si in v:
temps[k].append({'label': si.label or k, 'current': getattr(si, 'current', None)})
except Exception:
temps = {}
return jsonify({'cpu_percent': cpu, 'mem_percent': mem, 'temperatures': temps})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)