read log files from end, added user management
This commit is contained in:
@@ -12,9 +12,11 @@ from flask_cors import CORS
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from db_py import (
|
||||
count_admin_users,
|
||||
count_tests,
|
||||
count_users,
|
||||
create_user,
|
||||
delete_user,
|
||||
del_config,
|
||||
get_all_tests,
|
||||
get_all_users,
|
||||
@@ -23,6 +25,7 @@ from db_py import (
|
||||
get_user_by_username,
|
||||
set_config,
|
||||
clear_users,
|
||||
update_user_password,
|
||||
)
|
||||
from scanner import full_scan, is_scan_in_progress, resolve_runtime_path, scan_results_only
|
||||
from watcher import start_results_watchers, stop_results_watchers
|
||||
@@ -404,6 +407,51 @@ def create_user_route():
|
||||
return jsonify({"id": user_id, "username": username, "role": role, "is_active": 1}), 201
|
||||
|
||||
|
||||
@app.post("/api/users/<int:user_id>/password")
|
||||
@require_auth
|
||||
@require_role("admin")
|
||||
def set_user_password_route(user_id):
|
||||
body = request.get_json(silent=True)
|
||||
if not isinstance(body, dict):
|
||||
return jsonify({"error": "Request body must be a JSON object"}), 400
|
||||
|
||||
password = body.get("password") or ""
|
||||
if not password:
|
||||
return jsonify({"error": "Password is required"}), 400
|
||||
|
||||
user = get_user_by_id(user_id)
|
||||
if not user:
|
||||
return jsonify({"error": "User not found"}), 404
|
||||
|
||||
updated = update_user_password(user_id, generate_password_hash(password))
|
||||
if updated == 0:
|
||||
return jsonify({"error": "User not found"}), 404
|
||||
|
||||
return jsonify({"ok": True, "id": user_id})
|
||||
|
||||
|
||||
@app.delete("/api/users/<int:user_id>")
|
||||
@require_auth
|
||||
@require_role("admin")
|
||||
def delete_user_route(user_id):
|
||||
user = get_user_by_id(user_id)
|
||||
if not user:
|
||||
return jsonify({"error": "User not found"}), 404
|
||||
|
||||
current_user = getattr(g, "current_user", None) or {}
|
||||
if current_user.get("id") == user_id:
|
||||
return jsonify({"error": "You cannot delete your own account"}), 400
|
||||
|
||||
if user.get("role") == "admin" and count_admin_users() <= 1:
|
||||
return jsonify({"error": "Cannot delete the last active admin"}), 400
|
||||
|
||||
deleted = delete_user(user_id)
|
||||
if deleted == 0:
|
||||
return jsonify({"error": "User not found"}), 404
|
||||
|
||||
return jsonify({"ok": True, "id": user_id})
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
@require_auth
|
||||
@require_role("admin")
|
||||
|
||||
@@ -452,6 +452,43 @@ def create_user(username, password_hash, role="viewer", is_active=1):
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
def update_user_password(user_id, password_hash):
|
||||
with _tx():
|
||||
cursor = _conn.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET password_hash = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(password_hash, user_id),
|
||||
)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
def delete_user(user_id):
|
||||
with _tx():
|
||||
cursor = _conn.execute(
|
||||
"""
|
||||
DELETE FROM users
|
||||
WHERE id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
def count_admin_users():
|
||||
with _lock:
|
||||
row = _conn.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS n
|
||||
FROM users
|
||||
WHERE role = 'admin' AND is_active = 1
|
||||
"""
|
||||
).fetchone()
|
||||
return row["n"]
|
||||
|
||||
|
||||
def count_users():
|
||||
with _lock:
|
||||
row = _conn.execute("SELECT COUNT(*) AS n FROM users").fetchone()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
|
||||
_DEFAULT_SCAN_EXCLUSIONS = ""
|
||||
INTERFERENCE_TYPES = {"COE", "P2P", "P3P"}
|
||||
DIRECTION_TYPES = {"UL", "DL"}
|
||||
|
||||
@@ -67,4 +68,17 @@ def parse_timestamp(filename):
|
||||
second = parts[5] if len(parts) > 5 else "00"
|
||||
return f"{year}-{month}-{day}T{hour}:{minute}:{second}"
|
||||
|
||||
def parse_deleted_result_dir_name(dir_name):
|
||||
segments = re.split(r"[_\-]", dir_name)
|
||||
test_id = next((s for s in segments if re.match(r"^R\d+[A-Z0-9]+$", s, re.IGNORECASE)), None)
|
||||
device = next((s for s in segments if re.match(r"^CGW\d+$", s, re.IGNORECASE)), None)
|
||||
return test_id, device
|
||||
|
||||
def parse_scan_exclusions(value):
|
||||
raw = value if value not in (None, "") else _DEFAULT_SCAN_EXCLUSIONS
|
||||
if raw == "" or raw is None:
|
||||
return None
|
||||
parts = re.split(r"[,\n;]", str(raw))
|
||||
return {token.strip().upper() for token in parts if token.strip()}
|
||||
|
||||
|
||||
|
||||
+41
-25
@@ -18,22 +18,16 @@ from parser import (
|
||||
parse_result_filename,
|
||||
parse_target_filename,
|
||||
parse_timestamp,
|
||||
parse_scan_exclusions,
|
||||
)
|
||||
|
||||
|
||||
_SMB_SESSIONS = set()
|
||||
_WIN_DRIVE_PATH_RE = re.compile(r"^[A-Za-z]:[\\/]")
|
||||
_SCAN_STATE_LOCK = threading.Lock()
|
||||
_ACTIVE_SCAN_COUNT = 0
|
||||
_DEFAULT_SCAN_EXCLUSIONS = ""
|
||||
_ELAPSED_TIME_LINE_RE = re.compile(r"Elapsed\s+time\s*:\s*(\d+):(\d{1,2}):(\d{1,2}(?:\.\d+)?)", re.IGNORECASE)
|
||||
|
||||
def _parse_scan_exclusions(value):
|
||||
raw = value if value not in (None, "") else _DEFAULT_SCAN_EXCLUSIONS
|
||||
if raw == "" or raw is None:
|
||||
return None
|
||||
parts = re.split(r"[,\n;]", str(raw))
|
||||
return {token.strip().upper() for token in parts if token.strip()}
|
||||
|
||||
|
||||
|
||||
def _build_match_tokens(parsed):
|
||||
@@ -299,22 +293,51 @@ def _parse_elapsed_time_to_seconds(line):
|
||||
seconds = float(match.group(3))
|
||||
return int(hours * 3600 + minutes * 60 + seconds)
|
||||
|
||||
|
||||
def extract_elapsed_seconds_from_logs(result_dir_path, files):
|
||||
elapsed_seconds = None
|
||||
def extract_elapsed_seconds(result_dir_path, files):
|
||||
log_files = [name for name in files if _is_candidate_log_file(name)]
|
||||
|
||||
for log_name in log_files:
|
||||
log_path = _join_path(result_dir_path, log_name)
|
||||
try:
|
||||
for line in _read_text_lines(log_path):
|
||||
parsed = _parse_elapsed_time_to_seconds(line)
|
||||
if parsed is not None:
|
||||
elapsed_seconds = parsed
|
||||
# Read the log file in reverse to find the most recent "Elapsed time" line
|
||||
path = _normalize_input_path(log_path)
|
||||
chunk_size = 8192
|
||||
|
||||
if _is_unc_path(path):
|
||||
_register_smb_session_if_needed(path)
|
||||
file_obj = smbclient.open_file(path, mode="rb")
|
||||
else:
|
||||
file_obj = open(path, "rb")
|
||||
|
||||
with file_obj as fh:
|
||||
fh.seek(0, os.SEEK_END)
|
||||
file_size = fh.tell()
|
||||
position = file_size
|
||||
carry = b""
|
||||
|
||||
while position > 0:
|
||||
read_size = min(chunk_size, position)
|
||||
position -= read_size
|
||||
fh.seek(position)
|
||||
chunk = fh.read(read_size)
|
||||
|
||||
data = chunk + carry
|
||||
lines = data.split(b"\n")
|
||||
carry = lines[0]
|
||||
|
||||
for raw_line in reversed(lines[1:]):
|
||||
parsed = _parse_elapsed_time_to_seconds(raw_line.decode("utf-8", errors="ignore"))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
if carry:
|
||||
parsed = _parse_elapsed_time_to_seconds(carry.decode("utf-8", errors="ignore"))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read log file {log_path}: {exc}")
|
||||
|
||||
return elapsed_seconds
|
||||
return None
|
||||
|
||||
|
||||
def scan_results_only(results_dir, results_dir_ref):
|
||||
@@ -374,7 +397,7 @@ def full_scan(target_dir, results_dir, results_dir_ref):
|
||||
|
||||
def scan_targets(target_dir):
|
||||
target_dir = _normalize_input_path(target_dir)
|
||||
exclusions = _parse_scan_exclusions(get_config("scan_exclusions"))
|
||||
exclusions = parse_scan_exclusions(get_config("scan_exclusions"))
|
||||
batch_tests = []
|
||||
|
||||
try:
|
||||
@@ -484,7 +507,7 @@ def process_result_dir(results_dir, result_dir_name):
|
||||
print(f"[scanner] Cannot read result dir {result_dir_name}: {exc}")
|
||||
return None
|
||||
|
||||
elapsed_seconds = extract_elapsed_seconds_from_logs(result_dir_path, files)
|
||||
elapsed_seconds = extract_elapsed_seconds(result_dir_path, files)
|
||||
|
||||
measurement_db = next((name for name in files if name.lower() == "measurement.db"), None)
|
||||
if not measurement_db:
|
||||
@@ -514,13 +537,6 @@ def process_result_dir(results_dir, result_dir_name):
|
||||
}
|
||||
|
||||
|
||||
def parse_deleted_result_dir_name(dir_name):
|
||||
segments = re.split(r"[_\-]", dir_name)
|
||||
test_id = next((s for s in segments if re.match(r"^R\d+[A-Z0-9]+$", s, re.IGNORECASE)), None)
|
||||
device = next((s for s in segments if re.match(r"^CGW\d+$", s, re.IGNORECASE)), None)
|
||||
return test_id, device
|
||||
|
||||
|
||||
def extract_measurement_data(db_file_path):
|
||||
result = {"duration_seconds": None, "tputResults": []}
|
||||
local_db_path = None
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@ from db_py import (
|
||||
update_all_p2p_coe_pairs_sql,
|
||||
update_all_p3p_pairs_sql,
|
||||
)
|
||||
from scanner import parse_deleted_result_dir_name, process_result_dir, resolve_runtime_path
|
||||
from scanner import process_result_dir, resolve_runtime_path
|
||||
|
||||
from parser import parse_deleted_result_dir_name
|
||||
|
||||
|
||||
_OBSERVER = None
|
||||
|
||||
Reference in New Issue
Block a user