fix(install): 1Panel Redis 无账号与步骤 4 URL 自愈
确立 Redis 仅密码认证(:pass@/default),隐藏误导性用户名字段; connection-check 自动修正错误 NEXUS_REDIS_URL,并更新运维文档与验收脚本。
This commit is contained in:
+108
-6
@@ -12,7 +12,7 @@ import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote_plus, urlparse
|
||||
from urllib.parse import quote_plus, unquote_plus, urlparse
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -338,13 +338,28 @@ def _test_redis_url(redis_url: str) -> tuple[bool, str]:
|
||||
return False, f"✗ Redis 连接失败: {e}"
|
||||
|
||||
|
||||
def _is_docker_install_mode() -> bool:
|
||||
return os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1"
|
||||
|
||||
|
||||
def _sanitize_redis_request(req: InitDbRequest) -> InitDbRequest:
|
||||
"""1Panel/Docker: Redis has no account — ignore username (incl. mistaken root)."""
|
||||
user = (req.redis_user or "").strip()
|
||||
if _is_docker_install_mode():
|
||||
if user:
|
||||
return req.model_copy(update={"redis_user": ""})
|
||||
return req
|
||||
if user.lower() == "root":
|
||||
return req.model_copy(update={"redis_user": ""})
|
||||
return req
|
||||
|
||||
|
||||
def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
|
||||
"""Try order for 1Panel Redis (default user vs :pass@ vs explicit user)."""
|
||||
"""Auth variants: 1Panel tries :pass@ then default only (no root)."""
|
||||
password = (req.redis_pass or "").strip()
|
||||
if not password:
|
||||
return [("无密码", _build_redis_url(req))]
|
||||
|
||||
user = (req.redis_user or "").strip()
|
||||
seen: set[str] = set()
|
||||
out: list[tuple[str, str]] = []
|
||||
|
||||
@@ -355,7 +370,13 @@ def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
|
||||
seen.add(url)
|
||||
out.append((label, url))
|
||||
|
||||
if user:
|
||||
if _is_docker_install_mode():
|
||||
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
|
||||
add("用户 default", req.model_copy(update={"redis_user": "default"}))
|
||||
return out
|
||||
|
||||
user = (req.redis_user or "").strip()
|
||||
if user and user.lower() != "root":
|
||||
add(f"用户 {user}", req)
|
||||
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
|
||||
if user.lower() != "default":
|
||||
@@ -363,6 +384,67 @@ def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
|
||||
return out
|
||||
|
||||
|
||||
def _init_db_request_from_redis_url(redis_url: str) -> InitDbRequest | None:
|
||||
"""Parse NEXUS_REDIS_URL into InitDbRequest fields for re-probe / repair."""
|
||||
if not redis_url.strip():
|
||||
return None
|
||||
parsed = urlparse(redis_url)
|
||||
if not parsed.hostname:
|
||||
return None
|
||||
user = unquote_plus(parsed.username or "")
|
||||
password = unquote_plus(parsed.password or "")
|
||||
if not password and user:
|
||||
# Legacy bug: redis://pass@host (password stored as username)
|
||||
password = user
|
||||
user = ""
|
||||
db = (parsed.path or "/0").lstrip("/") or "0"
|
||||
return InitDbRequest(
|
||||
db_pass=".",
|
||||
redis_host=parsed.hostname,
|
||||
redis_port=str(parsed.port or 6379),
|
||||
redis_db=db,
|
||||
redis_user=user,
|
||||
redis_pass=password,
|
||||
)
|
||||
|
||||
|
||||
def _mask_redis_url(redis_url: str) -> str:
|
||||
parsed = urlparse(redis_url)
|
||||
if not parsed.password:
|
||||
return redis_url
|
||||
host = parsed.hostname or ""
|
||||
port = parsed.port
|
||||
user = parsed.username or ""
|
||||
if user:
|
||||
netloc = f"{user}:***@{host}"
|
||||
else:
|
||||
netloc = f":***@{host}"
|
||||
if port:
|
||||
netloc = f"{netloc}:{port}"
|
||||
path = parsed.path or "/0"
|
||||
return f"redis://{netloc}{path}"
|
||||
|
||||
|
||||
def _patch_env_redis_url(new_url: str) -> None:
|
||||
"""Update only NEXUS_REDIS_URL in existing .env (install step 4 repair)."""
|
||||
if not ENV_FILE.is_file():
|
||||
return
|
||||
lines = read_utf8_text(ENV_FILE).splitlines()
|
||||
new_line = f"NEXUS_REDIS_URL={_escape_env_value(new_url)}"
|
||||
replaced = False
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
if line.strip().startswith("NEXUS_REDIS_URL="):
|
||||
out.append(new_line)
|
||||
replaced = True
|
||||
else:
|
||||
out.append(line)
|
||||
if not replaced:
|
||||
out.append(new_line)
|
||||
write_utf8_lf(ENV_FILE, "\n".join(out) + "\n")
|
||||
logger.info("Patched NEXUS_REDIS_URL in %s", ENV_FILE)
|
||||
|
||||
|
||||
def _resolve_redis_url(req: InitDbRequest) -> tuple[bool, str, str | None]:
|
||||
"""Pick first working Redis URL among 1Panel-compatible auth variants."""
|
||||
last_msg = "✗ 未配置 Redis"
|
||||
@@ -753,6 +835,7 @@ async def env_check():
|
||||
|
||||
async def _credential_checks(req: InitDbRequest) -> tuple[list[dict], bool, str | None]:
|
||||
"""Verify MySQL + Redis with credentials from install step 3 (TCP + AUTH)."""
|
||||
req = _sanitize_redis_request(req)
|
||||
db_url = _make_db_url(req)
|
||||
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
|
||||
redis_ok, redis_msg, redis_url = _resolve_redis_url(req)
|
||||
@@ -775,7 +858,7 @@ async def test_credentials(req: InitDbRequest):
|
||||
checks, all_pass, redis_url = await _credential_checks(req)
|
||||
payload: dict = {"checks": checks, "all_pass": all_pass}
|
||||
if redis_url:
|
||||
payload["resolved_redis_url"] = redis_url
|
||||
payload["resolved_redis_url"] = _mask_redis_url(redis_url)
|
||||
return payload
|
||||
|
||||
|
||||
@@ -792,11 +875,30 @@ async def connection_check():
|
||||
|
||||
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
|
||||
redis_ok, redis_msg = _test_redis_url(redis_url)
|
||||
redis_repaired = False
|
||||
resolved_display: str | None = None
|
||||
|
||||
if not redis_ok and redis_url:
|
||||
req_from_env = _init_db_request_from_redis_url(redis_url)
|
||||
if req_from_env:
|
||||
ok, msg, resolved = _resolve_redis_url(_sanitize_redis_request(req_from_env))
|
||||
if ok and resolved:
|
||||
if resolved != redis_url and not _is_locked():
|
||||
_patch_env_redis_url(resolved)
|
||||
redis_repaired = True
|
||||
redis_ok, redis_msg = True, msg
|
||||
resolved_display = _mask_redis_url(resolved)
|
||||
|
||||
checks = [
|
||||
{"name": "MySQL", "required": "可连接", "current": mysql_msg, "pass": mysql_ok},
|
||||
{"name": "Redis", "required": "可连接", "current": redis_msg, "pass": redis_ok},
|
||||
]
|
||||
return {"checks": checks, "all_pass": mysql_ok and redis_ok}
|
||||
payload: dict = {"checks": checks, "all_pass": mysql_ok and redis_ok}
|
||||
if redis_repaired:
|
||||
payload["redis_repaired"] = True
|
||||
if resolved_display:
|
||||
payload["resolved_redis_url"] = resolved_display
|
||||
return payload
|
||||
|
||||
|
||||
@router.post("/init-db")
|
||||
|
||||
Reference in New Issue
Block a user