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.
208 lines
9.0 KiB
208 lines
9.0 KiB
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from scripts.real_openlist_acceptance import AcceptanceConfig, IronClient, read_openlist_webdav
|
|
|
|
|
|
def main() -> int:
|
|
config = MultiBackendConfig.from_env()
|
|
client = IronClient(config.base)
|
|
|
|
print(f"[1/9] logging into Iron at {config.base.iron_base_url}")
|
|
client.login()
|
|
|
|
print("[2/9] ensuring multi-backend OpenList configuration")
|
|
backends = ensure_backends(client, config)
|
|
for backend in backends:
|
|
check = client.check_backend(backend["id"])
|
|
if check["status"] != "healthy":
|
|
raise RuntimeError(f"backend {backend['name']} unhealthy: {json.dumps(check, ensure_ascii=False)}")
|
|
print(f" {backend['name']}: healthy at {check['detail']}")
|
|
|
|
original_policy = next(policy for policy in client.list_policies() if policy["file_class"] == config.file_class)
|
|
print(f"[3/9] saved original {config.file_class!r} policy")
|
|
|
|
remote_backend_ids = [backend["id"] for backend in backends]
|
|
try:
|
|
print("[4/9] verifying two-remote replica policy")
|
|
client.upsert_policy(
|
|
config.file_class,
|
|
require_local=True,
|
|
stable_replica_count=2,
|
|
opportunistic_replica_count=0,
|
|
preferred_backend_ids=remote_backend_ids,
|
|
excluded_backend_ids=[],
|
|
max_non_local_size_bytes=None,
|
|
)
|
|
first_file_id = client.upload_text_file()
|
|
first_detail = client.wait_for_ready_replica_set(first_file_id, {"bkd_local_default", remote_backend_ids[0], remote_backend_ids[1]})
|
|
if config.verify_direct_webdav:
|
|
assert_remote_bytes(config, backends, first_detail, remote_backend_ids[:2])
|
|
print(f" first file ready on local + {remote_backend_ids[0]} + {remote_backend_ids[1]}")
|
|
|
|
print("[5/9] disabling the first remote backend")
|
|
disabled_backend = client.disable_backend(remote_backend_ids[0])
|
|
print(f" disabled: {disabled_backend['name']}")
|
|
|
|
print("[6/9] verifying new uploads avoid the disabled backend")
|
|
second_filename = config.base.test_filename.replace(".txt", "-after-disable.txt")
|
|
second_content = config.base.test_content.rstrip("\n") + "-after-disable\n"
|
|
second_file_id = upload_named_text_file(client, config.base.directory_id, second_filename, second_content)
|
|
second_detail = client.wait_for_ready_replica_set(second_file_id, {"bkd_local_default", remote_backend_ids[1], remote_backend_ids[2]})
|
|
if config.verify_direct_webdav:
|
|
assert_remote_bytes(config, backends, second_detail, remote_backend_ids[1:3], expected_text=second_content)
|
|
print(f" second file ready on local + {remote_backend_ids[1]} + {remote_backend_ids[2]}")
|
|
|
|
print("[7/9] re-enabling the first remote backend")
|
|
enabled_backend = client.enable_backend(remote_backend_ids[0])
|
|
check = client.check_backend(enabled_backend["id"])
|
|
if check["status"] != "healthy":
|
|
raise RuntimeError(f"re-enabled backend not healthy: {json.dumps(check, ensure_ascii=False)}")
|
|
print(f" re-enabled: {enabled_backend['name']}")
|
|
|
|
print("[8/9] verifying three-remote replica policy")
|
|
client.upsert_policy(
|
|
config.file_class,
|
|
require_local=True,
|
|
stable_replica_count=3,
|
|
opportunistic_replica_count=0,
|
|
preferred_backend_ids=remote_backend_ids,
|
|
excluded_backend_ids=[],
|
|
max_non_local_size_bytes=None,
|
|
)
|
|
third_filename = config.base.test_filename.replace(".txt", "-all-three.txt")
|
|
third_content = config.base.test_content.rstrip("\n") + "-all-three\n"
|
|
third_file_id = upload_named_text_file(client, config.base.directory_id, third_filename, third_content)
|
|
third_detail = client.wait_for_ready_replica_set(third_file_id, {"bkd_local_default", *remote_backend_ids})
|
|
if config.verify_direct_webdav:
|
|
assert_remote_bytes(config, backends, third_detail, remote_backend_ids, expected_text=third_content)
|
|
print(" third file ready on local + all remote backends")
|
|
|
|
finally:
|
|
print("[9/9] restoring original policy")
|
|
client.upsert_policy(
|
|
config.file_class,
|
|
require_local=bool(original_policy["require_local"]),
|
|
stable_replica_count=int(original_policy["stable_replica_count"]),
|
|
opportunistic_replica_count=int(original_policy["opportunistic_replica_count"]),
|
|
preferred_backend_ids=list(original_policy["preferred_backend_ids"]),
|
|
excluded_backend_ids=list(original_policy["excluded_backend_ids"]),
|
|
max_non_local_size_bytes=original_policy["max_non_local_size_bytes"],
|
|
)
|
|
print(" original policy restored")
|
|
|
|
print("Multi-backend acceptance passed.")
|
|
return 0
|
|
|
|
|
|
@dataclass
|
|
class MultiBackendConfig:
|
|
base: AcceptanceConfig
|
|
backend_names: list[str]
|
|
root_paths: list[str]
|
|
file_class: str
|
|
verify_direct_webdav: bool
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "MultiBackendConfig":
|
|
base = AcceptanceConfig.from_env()
|
|
raw_paths = os.getenv("OPENLIST_MULTI_ROOT_PATHS", "aliyun/iron,aliyun/iron-b,aliyun/iron-c")
|
|
root_paths = [item.strip().strip("/") for item in raw_paths.split(",") if item.strip()]
|
|
if len(root_paths) < 3:
|
|
raise RuntimeError("OPENLIST_MULTI_ROOT_PATHS must define at least three root paths.")
|
|
backend_names = [
|
|
"openlist-aliyun",
|
|
"openlist-aliyun-b",
|
|
"openlist-aliyun-c",
|
|
*[
|
|
f"openlist-aliyun-{index}"
|
|
for index in range(4, len(root_paths) + 1)
|
|
],
|
|
][: len(root_paths)]
|
|
return cls(
|
|
base=base,
|
|
backend_names=backend_names,
|
|
root_paths=root_paths,
|
|
file_class=os.getenv("IRON_MULTI_FILE_CLASS", "other"),
|
|
verify_direct_webdav=os.getenv("IRON_MULTI_VERIFY_DIRECT_WEBDAV", "").strip().lower() in {"1", "true", "yes", "on"},
|
|
)
|
|
|
|
|
|
def ensure_backends(client: IronClient, config: MultiBackendConfig) -> list[dict[str, object]]:
|
|
existing = {backend["name"]: backend for backend in client.list_backends()}
|
|
ensured: list[dict[str, object]] = []
|
|
for index, (name, root_path) in enumerate(zip(config.backend_names, config.root_paths, strict=True), start=1):
|
|
backend = existing.get(name)
|
|
if backend is None:
|
|
payload = {
|
|
"name": name,
|
|
"type": "webdav",
|
|
"stability_class": "stable",
|
|
"read_priority": 90 - index,
|
|
"write_priority": 90 - index,
|
|
"config": {
|
|
"endpoint_url": config.base.openlist_dav_url,
|
|
"username": config.base.openlist_username,
|
|
"password": config.base.openlist_password,
|
|
"root_path": root_path,
|
|
"verify_ssl": False,
|
|
},
|
|
}
|
|
backend = client._request("POST", "/api/backends", json_body=payload)["backend"]
|
|
ensured.append(backend)
|
|
return ensured
|
|
|
|
|
|
def upload_named_text_file(client: IronClient, directory_id: str, filename: str, content: str) -> str:
|
|
original_filename = client.config.test_filename
|
|
original_content = client.config.test_content
|
|
try:
|
|
client.config.test_filename = filename
|
|
client.config.test_content = content
|
|
return client.upload_text_file()
|
|
finally:
|
|
client.config.test_filename = original_filename
|
|
client.config.test_content = original_content
|
|
|
|
|
|
def assert_remote_bytes(
|
|
config: MultiBackendConfig,
|
|
backends: list[dict[str, object]],
|
|
detail: dict[str, object],
|
|
backend_ids: list[str],
|
|
*,
|
|
expected_text: str | None = None,
|
|
) -> None:
|
|
expected_bytes = (expected_text or config.base.test_content).encode("utf-8")
|
|
replicas = detail["replicas"]
|
|
backend_lookup = {backend["id"]: backend for backend in backends}
|
|
for backend_id in backend_ids:
|
|
replica = next(rep for rep in replicas if rep["backend_id"] == backend_id and rep["status"] == "ready")
|
|
backend = backend_lookup[backend_id]
|
|
try:
|
|
body = read_openlist_webdav(
|
|
endpoint_url=config.base.openlist_dav_url,
|
|
username=config.base.openlist_username,
|
|
password=config.base.openlist_password,
|
|
relative_path=f"{backend['config']['root_path']}/{replica['storage_key']}",
|
|
)
|
|
except Exception as exc:
|
|
raise RuntimeError(
|
|
f"direct WebDAV read failed for backend {backend['name']} at "
|
|
f"{backend['config']['root_path']}/{replica['storage_key']}: {exc}"
|
|
) from exc
|
|
if body != expected_bytes:
|
|
raise RuntimeError(f"unexpected bytes for backend {backend['name']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|
|
|