Initial commit

This commit is contained in:
2026-06-03 17:33:19 +03:30
commit 9608342428
48 changed files with 3562 additions and 0 deletions
View File
+142
View File
@@ -0,0 +1,142 @@
from __future__ import annotations
import logging
from datetime import timedelta
from pathlib import Path
from app.config import Config
from app.extensions import db
from app.models import ApplyLog, Profile, SystemState
from app.models import utcnow
from app.services import apt, backup, dns, docker_svc, github, pip_npm
from app.services.host import run_on_host
logger = logging.getLogger(__name__)
APPLIERS = {
"dns": dns.apply_dns,
"apt": apt.apply_apt,
"docker": docker_svc.apply_docker,
"github": github.apply_github,
"pip": pip_npm.apply_pip,
"npm": pip_npm.apply_npm,
}
def ensure_initial_backup() -> SystemState:
state = SystemState.query.first()
if not state:
state = SystemState()
db.session.add(state)
db.session.commit()
if not state.initial_backup_done:
path = backup.create_initial_backup()
state.initial_backup_path = str(path)
state.initial_backup_done = True
db.session.commit()
logger.info("Initial backup created at %s", path)
return state
def apply_profile(profile: Profile, rollback_minutes: int | None = None) -> ApplyLog:
state = ensure_initial_backup()
rollback_minutes = rollback_minutes or int(Config.ROLLBACK_MINUTES)
previous_backup = backup.create_backup(f"before_apply_{profile.id}")
apply_log = ApplyLog(
profile_id=profile.id,
status="running",
backup_path=str(previous_backup),
previous_backup_path=state.initial_backup_path,
rollback_at=utcnow() + timedelta(minutes=rollback_minutes),
)
db.session.add(apply_log)
db.session.flush()
details: dict = {"steps": [], "profile": profile.name}
mirrors = profile.mirrors_by_category()
order = ["dns", "apt", "docker", "github", "pip", "npm"]
for category in order:
mirror = mirrors.get(category)
if not mirror:
continue
applier = APPLIERS.get(category)
if not applier:
continue
try:
result = applier(mirror)
details["steps"].append({"category": category, "mirror": mirror.name, **result})
except Exception as exc:
logger.exception("Apply failed for %s", category)
details["steps"].append(
{"category": category, "mirror": mirror.name, "success": False, "error": str(exc)}
)
failed = [s for s in details["steps"] if not s.get("success", True)]
if failed and apply_log.backup_path:
rollback_result = backup.restore_backup(apply_log.backup_path)
details["auto_rollback"] = rollback_result
apply_log.set_details(details)
apply_log.status = "failed" if failed else "pending_confirm"
apply_log.finished_at = utcnow()
state.current_profile_id = profile.id if not failed else state.current_profile_id
state.pending_apply_log_id = apply_log.id if not failed else None
db.session.commit()
return apply_log
def confirm_apply(apply_log_id: int) -> ApplyLog:
apply_log = ApplyLog.query.get_or_404(apply_log_id)
apply_log.confirmed = True
apply_log.status = "confirmed"
apply_log.rollback_at = None
state = SystemState.query.first()
if state:
state.pending_apply_log_id = None
db.session.commit()
return apply_log
def rollback_apply(apply_log_id: int) -> dict:
apply_log = ApplyLog.query.get_or_404(apply_log_id)
if not apply_log.backup_path:
return {"success": False, "error": "مسیر backup یافت نشد"}
result = backup.restore_backup(apply_log.backup_path)
apply_log.status = "rolled_back"
apply_log.finished_at = utcnow()
state = SystemState.query.first()
if state:
state.pending_apply_log_id = None
db.session.commit()
return result
def restore_initial() -> dict:
state = SystemState.query.first()
if not state or not state.initial_backup_path:
return {"success": False, "error": "backup اولیه یافت نشد"}
return backup.restore_backup(state.initial_backup_path)
def get_system_status() -> dict:
codename_result = run_on_host(["lsb_release", "-cs"])
version_result = run_on_host(["lsb_release", "-d"])
return {
"ubuntu_codename": codename_result.stdout.strip() if codename_result.returncode == 0 else "unknown",
"ubuntu_description": version_result.stdout.strip() if version_result.returncode == 0 else "unknown",
"dns": dns.read_current_dns(),
"apt": apt.read_current_apt(),
"docker": docker_svc.read_current_docker(),
"github": github.read_current_github(),
"host_root": Config.HOST_ROOT or "(local mode)",
}
+65
View File
@@ -0,0 +1,65 @@
from __future__ import annotations
import logging
from pathlib import Path
from app.models import Mirror
from app.services.host import get_ubuntu_codename, host_path, run_on_host, validate_codename
logger = logging.getLogger(__name__)
SOURCES_PATH = "etc/apt/sources.list"
SOURCES_D_PATH = "etc/apt/sources.list.d"
MIRROR_MARKER = "mirror-manager"
def read_current_apt() -> dict:
sources = host_path(SOURCES_PATH)
return {
"sources_list": sources.read_text(encoding="utf-8") if sources.exists() else None,
"path": str(sources),
}
def _build_sources_content(base_url: str, codename: str) -> str:
url = base_url.rstrip("/")
return f"""# Managed by Mirror Manager - {MIRROR_MARKER}
deb {url} {codename} main restricted universe multiverse
deb {url} {codename}-updates main restricted universe multiverse
deb {url} {codename}-backports main restricted universe multiverse
deb {url} {codename}-security main restricted universe multiverse
"""
def apply_apt(mirror: Mirror) -> dict:
if not mirror.url:
return {"success": False, "error": "URL میرور APT تعریف نشده"}
codename = validate_codename(get_ubuntu_codename())
sources_path = host_path(SOURCES_PATH)
sources_path.parent.mkdir(parents=True, exist_ok=True)
content = _build_sources_content(mirror.url, codename)
sources_path.write_text(content, encoding="utf-8")
sources_d = host_path(SOURCES_D_PATH)
if sources_d.exists():
for f in sources_d.glob("*.list"):
if MIRROR_MARKER not in f.read_text(encoding="utf-8", errors="ignore"):
backup_name = f.with_suffix(".list.disabled")
f.rename(backup_name)
result = run_on_host(["apt-get", "update", "-qq"], timeout=180)
if result.returncode != 0:
return {
"success": False,
"error": (result.stderr or result.stdout or "apt-get update ناموفق")[:500],
"codename": codename,
}
return {
"success": True,
"message": f"APT mirror اعمال شد: {mirror.name}",
"codename": codename,
"url": mirror.url,
}
+173
View File
@@ -0,0 +1,173 @@
from __future__ import annotations
import json
import logging
import shutil
from datetime import datetime
from pathlib import Path
from app.config import Config
from app.services.host import host_path, run_on_host
logger = logging.getLogger(__name__)
BACKUP_TARGETS = {
"resolved.conf": "etc/systemd/resolved.conf",
"resolved.conf.d": "etc/systemd/resolved.conf.d",
"sources.list": "etc/apt/sources.list",
"sources.list.d": "etc/apt/sources.list.d",
"daemon.json": "etc/docker/daemon.json",
"gitconfig_system": "etc/gitconfig",
}
def _backup_dir(name: str) -> Path:
path = Config.BACKUP_DIR / name
path.mkdir(parents=True, exist_ok=True)
return path
def _copy_if_exists(src: Path, dst: Path) -> None:
if not src.exists():
return
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def _restore_file(relative: str, backup_root: Path) -> None:
key = relative.replace("/", "_").replace(".", "_")
for name, rel in BACKUP_TARGETS.items():
if rel == relative or rel.endswith(relative):
key = name
break
src = backup_root / key
dst = host_path(relative)
if not src.exists():
if dst.exists() and key in ("daemon.json", "gitconfig_system"):
dst.unlink(missing_ok=True)
return
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def create_backup(label: str = "manual") -> Path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_root = _backup_dir(f"{timestamp}_{label}")
manifest: dict = {"label": label, "timestamp": timestamp, "files": {}}
for key, relative in BACKUP_TARGETS.items():
src = host_path(relative)
dst = backup_root / key
_copy_if_exists(src, dst)
manifest["files"][key] = {
"relative": relative,
"existed": src.exists(),
}
gitconfig_global = host_path("root/.gitconfig")
if not gitconfig_global.exists():
gitconfig_global = Path.home() / ".gitconfig"
if gitconfig_global.exists():
shutil.copy2(gitconfig_global, backup_root / "gitconfig_global")
manifest["files"]["gitconfig_global"] = {"path": str(gitconfig_global), "existed": True}
pip_conf = host_path("etc/pip.conf")
if pip_conf.exists():
shutil.copy2(pip_conf, backup_root / "pip.conf")
manifest["files"]["pip.conf"] = {"relative": "etc/pip.conf", "existed": True}
npmrc = host_path("root/.npmrc")
if npmrc.exists():
shutil.copy2(npmrc, backup_root / "npmrc")
manifest["files"]["npmrc"] = {"relative": "root/.npmrc", "existed": True}
mirror_dropin = host_path("etc/systemd/resolved.conf.d/mirror-manager.conf")
if mirror_dropin.exists():
shutil.copy2(mirror_dropin, backup_root / "mirror_manager_resolved.conf")
with open(backup_root / "manifest.json", "w", encoding="utf-8") as f:
json.dump(manifest, f, ensure_ascii=False, indent=2)
logger.info("Backup created at %s", backup_root)
return backup_root
def restore_backup(backup_root: Path | str) -> dict:
backup_root = Path(backup_root)
if not backup_root.exists():
return {"success": False, "error": "مسیر backup یافت نشد"}
results: list[str] = []
for key, relative in BACKUP_TARGETS.items():
src = backup_root / key
if src.exists():
dst = host_path(relative)
_copy_if_exists(src, dst)
results.append(f"بازگردانی {relative}")
gitconfig_backup = backup_root / "gitconfig_global"
if gitconfig_backup.exists():
for target in [host_path("root/.gitconfig"), host_path("home/.gitconfig")]:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(gitconfig_backup, target)
results.append("بازگردانی gitconfig global")
pip_backup = backup_root / "pip.conf"
if pip_backup.exists():
dst = host_path("etc/pip.conf")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(pip_backup, dst)
results.append("بازگردانی pip.conf")
npmrc_backup = backup_root / "npmrc"
if npmrc_backup.exists():
dst = host_path("root/.npmrc")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(npmrc_backup, dst)
results.append("بازگردانی .npmrc")
dropin = host_path("etc/systemd/resolved.conf.d/mirror-manager.conf")
mirror_backup = backup_root / "mirror_manager_resolved.conf"
if mirror_backup.exists():
dropin.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(mirror_backup, dropin)
elif dropin.exists():
dropin.unlink()
run_on_host(["systemctl", "restart", "systemd-resolved"])
run_on_host(["systemctl", "restart", "docker"])
return {"success": True, "restored": results}
def create_initial_backup() -> Path:
return create_backup("initial")
def list_backups() -> list[dict]:
backups = []
if not Config.BACKUP_DIR.exists():
return backups
for path in sorted(Config.BACKUP_DIR.iterdir(), reverse=True):
if path.is_dir():
manifest_path = path / "manifest.json"
label = path.name
if manifest_path.exists():
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
label = manifest.get("label", path.name)
backups.append({"path": str(path), "name": path.name, "label": label})
return backups
+65
View File
@@ -0,0 +1,65 @@
from __future__ import annotations
import logging
from pathlib import Path
from app.config import Config
from app.models import Mirror
from app.services.host import host_path, run_on_host
logger = logging.getLogger(__name__)
DROPIN_FILENAME = "mirror-manager.conf"
def read_current_dns() -> dict:
dropin = host_path("etc/systemd/resolved.conf.d", DROPIN_FILENAME)
main_conf = host_path("etc/systemd/resolved.conf")
return {
"dropin_exists": dropin.exists(),
"dropin_path": str(dropin),
"main_conf_exists": main_conf.exists(),
"content": dropin.read_text(encoding="utf-8") if dropin.exists() else None,
}
def apply_dns(mirror: Mirror) -> dict:
ips = mirror.get_ips()
if not ips:
return {"success": False, "error": "آدرس DNS تعریف نشده"}
dropin_dir = host_path("etc/systemd/resolved.conf.d")
dropin_dir.mkdir(parents=True, exist_ok=True)
dropin_path = dropin_dir / DROPIN_FILENAME
dns_line = " ".join(ips)
content = f"""# Managed by Mirror Manager - do not edit manually
[Resolve]
DNS={dns_line}
FallbackDNS=1.1.1.1 8.8.8.8
DNSStubListener=yes
"""
dropin_path.write_text(content, encoding="utf-8")
result = run_on_host(["systemctl", "restart", "systemd-resolved"])
if result.returncode != 0:
return {
"success": False,
"error": result.stderr or "خطا در restart systemd-resolved",
"applied_content": content,
}
return {
"success": True,
"message": f"DNS اعمال شد: {mirror.name}",
"ips": ips,
"path": str(dropin_path),
}
def remove_dns_dropin() -> None:
dropin = host_path("etc/systemd/resolved.conf.d", DROPIN_FILENAME)
if dropin.exists():
dropin.unlink()
run_on_host(["systemctl", "restart", "systemd-resolved"])
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
from app.models import Mirror
from app.services.host import host_path, run_on_host
logger = logging.getLogger(__name__)
DAEMON_JSON = "etc/docker/daemon.json"
MIRROR_MANAGER_KEY = "mirror-manager"
def read_current_docker() -> dict:
path = host_path(DAEMON_JSON)
if not path.exists():
return {"exists": False, "config": {}}
try:
return {"exists": True, "config": json.loads(path.read_text(encoding="utf-8"))}
except json.JSONDecodeError:
return {"exists": True, "config": {}, "parse_error": True}
def apply_docker(mirror: Mirror) -> dict:
if not mirror.url:
return {"success": False, "error": "URL رجیstry داکر تعریف نشده"}
path = host_path(DAEMON_JSON)
path.parent.mkdir(parents=True, exist_ok=True)
config: dict = {}
if path.exists():
try:
config = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
config = {}
config["registry-mirrors"] = [mirror.url.rstrip("/")]
config["_mirror_manager"] = MIRROR_MANAGER_KEY
path.write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
result = run_on_host(["systemctl", "restart", "docker"], timeout=120)
if result.returncode != 0:
return {
"success": False,
"error": (result.stderr or "خطا در restart docker")[:500],
}
return {
"success": True,
"message": f"Docker registry mirror اعمال شد: {mirror.name}",
"url": mirror.url,
"warning": "سرویس Docker restart شد — containerها موقتاً قطع می‌شوند",
}
+107
View File
@@ -0,0 +1,107 @@
from __future__ import annotations
import configparser
import logging
from io import StringIO
from pathlib import Path
from app.models import Mirror
from app.services.host import host_path, run_on_host
logger = logging.getLogger(__name__)
MARKER = "mirror-manager"
INSTEAD_SECTION = f'url "https://github.com/" managed by {MARKER}'
def _gitconfig_paths() -> list[Path]:
paths = []
for rel in ("root/.gitconfig", "home/.gitconfig"):
p = host_path(rel)
if p not in paths:
paths.append(p)
local = Path.home() / ".gitconfig"
if local not in paths:
paths.append(local)
system = host_path("etc/gitconfig")
if system not in paths:
paths.append(system)
return paths
def read_current_github() -> dict:
configs = {}
for path in _gitconfig_paths():
if path.exists():
configs[str(path)] = path.read_text(encoding="utf-8")
return configs
def _parse_gitconfig(content: str) -> configparser.ConfigParser:
parser = configparser.ConfigParser()
parser.read_string(content if content.strip() else "[core]\n")
return parser
def _serialize_gitconfig(parser: configparser.ConfigParser) -> str:
buf = StringIO()
parser.write(buf)
return buf.getvalue()
def _remove_mirror_sections(parser: configparser.ConfigParser) -> None:
to_remove = []
for section in parser.sections():
if not section.startswith('url "'):
continue
instead_of = parser.get(section, "insteadOf", fallback="")
if instead_of in ("https://github.com/", "git@github.com:"):
to_remove.append(section)
for section in to_remove:
parser.remove_section(section)
def apply_github(mirror: Mirror) -> dict:
meta = mirror.get_meta()
instead_prefix = meta.get("instead_prefix") or mirror.url
if not instead_prefix:
return {"success": False, "error": "prefix میرور GitHub تعریف نشده"}
instead_prefix = instead_prefix.rstrip("/") + "/"
section_name = f'url "{instead_prefix}"'
applied_paths = []
for path in _gitconfig_paths():
content = path.read_text(encoding="utf-8") if path.exists() else ""
parser = _parse_gitconfig(content)
_remove_mirror_sections(parser)
if not parser.has_section(section_name):
parser.add_section(section_name)
parser.set(section_name, "insteadOf", "https://github.com/")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(_serialize_gitconfig(parser), encoding="utf-8")
applied_paths.append(str(path))
test = run_on_host(
["git", "ls-remote", "https://github.com/octocat/Hello-World.git", "HEAD"],
timeout=60,
)
return {
"success": test.returncode == 0,
"message": f"GitHub mirror اعمال شد: {mirror.name}",
"paths": applied_paths,
"test_output": (test.stdout or test.stderr)[:300] if test.returncode != 0 else "git ls-remote موفق",
"error": test.stderr[:300] if test.returncode != 0 else None,
}
def remove_github_config() -> None:
for path in _gitconfig_paths():
if not path.exists():
continue
parser = _parse_gitconfig(path.read_text(encoding="utf-8"))
_remove_mirror_sections(parser)
path.write_text(_serialize_gitconfig(parser), encoding="utf-8")
+60
View File
@@ -0,0 +1,60 @@
from __future__ import annotations
import logging
import os
import shutil
import subprocess
from datetime import datetime
from pathlib import Path
from app.config import Config
logger = logging.getLogger(__name__)
def host_path(*parts: str) -> Path:
return Config.host_path(*parts)
def is_container_mode() -> bool:
return bool(Config.HOST_ROOT) and Path(Config.HOST_ROOT).exists()
def run_on_host(command: list[str], timeout: int = 120) -> subprocess.CompletedProcess:
"""Run a command on the host when inside a privileged container."""
if is_container_mode():
full_cmd = ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "-p", "--"] + command
else:
full_cmd = command
return subprocess.run(
full_cmd,
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
def get_ubuntu_codename() -> str:
result = run_on_host(["lsb_release", "-cs"])
if result.returncode == 0:
return result.stdout.strip()
return "noble"
def get_ubuntu_version() -> str:
result = run_on_host(["lsb_release", "-rs"])
if result.returncode == 0:
return result.stdout.strip()
return "24.04"
SUPPORTED_CODENAMES = {"noble", "jammy", "bionic"}
def validate_codename(codename: str) -> str:
if codename in SUPPORTED_CODENAMES:
return codename
mapping = {"24.04": "noble", "22.04": "jammy", "18.04": "bionic"}
version = get_ubuntu_version()
return mapping.get(version, "noble")
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
import logging
from pathlib import Path
from app.models import Mirror
from app.services.host import host_path
logger = logging.getLogger(__name__)
def apply_pip(mirror: Mirror) -> dict:
if not mirror.url:
return {"success": False, "error": "URL میرور pip تعریف نشده"}
conf_path = host_path("etc/pip.conf")
conf_path.parent.mkdir(parents=True, exist_ok=True)
content = f"""# Managed by Mirror Manager
[global]
index-url = {mirror.url.rstrip("/")}
trusted-host = {mirror.url.split("//")[-1].split("/")[0]}
"""
conf_path.write_text(content, encoding="utf-8")
return {"success": True, "message": f"pip mirror اعمال شد: {mirror.name}", "path": str(conf_path)}
def apply_npm(mirror: Mirror) -> dict:
if not mirror.url:
return {"success": False, "error": "URL رجیstry npm تعریف نشده"}
npmrc = host_path("root/.npmrc")
npmrc.parent.mkdir(parents=True, exist_ok=True)
content = f"# Managed by Mirror Manager\nregistry={mirror.url.rstrip('/')}\n"
npmrc.write_text(content, encoding="utf-8")
return {"success": True, "message": f"npm registry اعمال شد: {mirror.name}", "path": str(npmrc)}
+111
View File
@@ -0,0 +1,111 @@
from __future__ import annotations
import logging
from datetime import datetime, timezone
from app.extensions import db
from app.models import ApplyLog, Mirror, Profile, ProfileItem, Setting, SystemState
from app.models import utcnow
from app.services.applier import apply_profile, rollback_apply
from app.services.tester import get_best_mirror, test_mirror
logger = logging.getLogger(__name__)
_scheduler = None
def _auto_switch_enabled(category: str) -> bool:
return Setting.get(f"auto_switch_{category}", "false").lower() == "true"
def _get_switch_count_today() -> int:
key = f"switch_count_{utcnow().strftime('%Y%m%d')}"
return int(Setting.get(key, "0") or "0")
def _increment_switch_count() -> None:
key = f"switch_count_{utcnow().strftime('%Y%m%d')}"
Setting.set(key, str(_get_switch_count_today() + 1))
def check_pending_rollbacks(app) -> None:
with app.app_context():
now = utcnow()
pending = ApplyLog.query.filter(
ApplyLog.status == "pending_confirm",
ApplyLog.confirmed.is_(False),
ApplyLog.rollback_at.isnot(None),
ApplyLog.rollback_at <= now,
).all()
for log in pending:
logger.info("Auto rollback for apply log %s", log.id)
rollback_apply(log.id)
def run_auto_switch(app) -> None:
with app.app_context():
max_switches = int(Setting.get("max_switches_per_day", "3") or "3")
if _get_switch_count_today() >= max_switches:
logger.info("Max switches per day reached")
return
state = SystemState.query.first()
if not state or not state.current_profile_id:
return
profile = Profile.query.get(state.current_profile_id)
if not profile:
return
for category in ("dns", "apt", "docker", "github", "pip", "npm"):
if not _auto_switch_enabled(category):
continue
item = ProfileItem.query.filter_by(profile_id=profile.id, category=category).first()
if not item or not item.mirror:
continue
results = test_mirror(item.mirror)
if results and all(r.success for r in results):
continue
best = get_best_mirror(category)
if not best or best.id == item.mirror_id:
if Setting.get("rollback_on_all_fail", "true").lower() == "true":
logger.warning("All mirrors failed for %s", category)
continue
item.mirror_id = best.id
db.session.commit()
apply_profile(profile)
_increment_switch_count()
logger.info("Auto-switched %s to %s", category, best.name)
break
def init_scheduler(app) -> None:
global _scheduler
from apscheduler.schedulers.background import BackgroundScheduler
if _scheduler is not None:
return
interval = int(app.config.get("AUTO_TEST_INTERVAL_MINUTES", 30))
_scheduler = BackgroundScheduler(daemon=True)
_scheduler.add_job(
check_pending_rollbacks,
"interval",
minutes=1,
args=[app],
id="rollback_checker",
)
_scheduler.add_job(
run_auto_switch,
"interval",
minutes=interval,
args=[app],
id="auto_switch",
)
_scheduler.start()
logger.info("Scheduler started (interval=%s min)", interval)
+157
View File
@@ -0,0 +1,157 @@
from __future__ import annotations
import socket
import time
from urllib.parse import urlparse
import requests
from app.extensions import db
from app.models import Mirror, TestResult
from app.services.host import get_ubuntu_codename, run_on_host, validate_codename
def _record(mirror: Mirror, test_type: str, success: bool, latency_ms: float | None, error: str | None) -> TestResult:
result = TestResult(
mirror_id=mirror.id,
test_type=test_type,
success=success,
latency_ms=latency_ms,
error=error,
)
db.session.add(result)
db.session.commit()
return result
def test_dns(mirror: Mirror) -> TestResult:
ips = mirror.get_ips()
if not ips:
return _record(mirror, "dns_resolve", False, None, "IP تعریف نشده")
start = time.perf_counter()
try:
for domain in ("github.com", "docker.io"):
socket.getaddrinfo(domain, 443, type=socket.SOCK_STREAM)
latency = (time.perf_counter() - start) * 1000
return _record(mirror, "dns_resolve", True, latency, None)
except socket.gaierror as exc:
latency = (time.perf_counter() - start) * 1000
return _record(mirror, "dns_resolve", False, latency, str(exc))
def test_tcp(url: str, timeout: int = 10) -> tuple[bool, float | None, str | None]:
parsed = urlparse(url if "://" in url else f"https://{url}")
host = parsed.hostname
port = parsed.port or (443 if parsed.scheme == "https" else 80)
if not host:
return False, None, "host نامعتبر"
start = time.perf_counter()
try:
sock = socket.create_connection((host, port), timeout=timeout)
sock.close()
return True, (time.perf_counter() - start) * 1000, None
except OSError as exc:
return False, (time.perf_counter() - start) * 1000, str(exc)
def test_http(url: str, timeout: int = 15) -> tuple[bool, float | None, str | None]:
start = time.perf_counter()
try:
response = requests.head(url.rstrip("/"), timeout=timeout, allow_redirects=True)
if response.status_code >= 500:
return False, (time.perf_counter() - start) * 1000, f"HTTP {response.status_code}"
return True, (time.perf_counter() - start) * 1000, None
except requests.RequestException as exc:
return False, (time.perf_counter() - start) * 1000, str(exc)
def test_apt(mirror: Mirror) -> TestResult:
if not mirror.url:
return _record(mirror, "apt_release", False, None, "URL تعریف نشده")
codename = validate_codename(get_ubuntu_codename())
release_url = f"{mirror.url.rstrip('/')}/dists/{codename}/Release"
ok, latency, error = test_http(release_url)
return _record(mirror, "apt_release", ok, latency, error)
def test_docker(mirror: Mirror) -> TestResult:
if not mirror.url:
return _record(mirror, "docker_registry", False, None, "URL تعریف نشده")
api_url = f"{mirror.url.rstrip('/')}/v2/"
ok, latency, error = test_http(api_url)
return _record(mirror, "docker_registry", ok, latency, error)
def test_github(mirror: Mirror) -> TestResult:
meta = mirror.get_meta()
prefix = meta.get("instead_prefix") or mirror.url
if not prefix:
return _record(mirror, "github_ls_remote", False, None, "prefix تعریف نشده")
test_url = f"{prefix.rstrip('/')}/octocat/Hello-World.git"
start = time.perf_counter()
result = run_on_host(["git", "ls-remote", test_url, "HEAD"], timeout=45)
latency = (time.perf_counter() - start) * 1000
if result.returncode == 0:
return _record(mirror, "github_ls_remote", True, latency, None)
return _record(mirror, "github_ls_remote", False, latency, (result.stderr or result.stdout)[:300])
def test_pip(mirror: Mirror) -> TestResult:
if not mirror.url:
return _record(mirror, "pip_index", False, None, "URL تعریف نشده")
ok, latency, error = test_http(mirror.url)
return _record(mirror, "pip_index", ok, latency, error)
def test_npm(mirror: Mirror) -> TestResult:
if not mirror.url:
return _record(mirror, "npm_registry", False, None, "URL تعریف نشده")
ok, latency, error = test_http(mirror.url)
return _record(mirror, "npm_registry", ok, latency, error)
def test_mirror(mirror: Mirror) -> list[TestResult]:
testers = {
"dns": [test_dns],
"apt": [test_apt],
"docker": [test_docker],
"github": [test_github],
"pip": [test_pip],
"npm": [test_npm],
}
results = []
for fn in testers.get(mirror.category, []):
results.append(fn(mirror))
return results
def test_all_enabled(category: str | None = None) -> list[TestResult]:
query = Mirror.query.filter_by(enabled=True)
if category:
query = query.filter_by(category=category)
all_results: list[TestResult] = []
for mirror in query.order_by(Mirror.priority).all():
all_results.extend(test_mirror(mirror))
return all_results
def get_best_mirror(category: str) -> Mirror | None:
mirrors = Mirror.query.filter_by(category=category, enabled=True).order_by(Mirror.priority).all()
best: Mirror | None = None
best_latency = float("inf")
for mirror in mirrors:
results = test_mirror(mirror)
if not results:
continue
if all(r.success for r in results):
avg_latency = sum(r.latency_ms or 9999 for r in results) / len(results)
if avg_latency < best_latency:
best_latency = avg_latency
best = mirror
return best