Refactor public network handling in GPU rental system
- Updated terminology from "public-net" to "public-network" for consistency across the codebase. - Enhanced error handling and logging for public network API interactions. - Introduced a new function to determine the public network pool based on the compute region. - Adjusted API request functions to use the new public network terminology and improved error messages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+14
-3
@@ -83,8 +83,14 @@ def _check_balance(cfg: Config, checks: list[Check]) -> None:
|
||||
|
||||
def _check_direct_net(cfg: Config, conn, checks: list[Check]) -> None:
|
||||
from gpu_rent.os_client import ROUTER_NAME
|
||||
from gpu_rent.public_net import public_net_request
|
||||
from gpu_rent.public_net import public_net_base, public_net_request, public_network_pool
|
||||
|
||||
api_pool = public_network_pool(cfg.os_region_name)
|
||||
pool_note = (
|
||||
f"{cfg.os_region_name}→{api_pool}"
|
||||
if api_pool != cfg.os_region_name
|
||||
else cfg.os_region_name
|
||||
)
|
||||
try:
|
||||
response = public_net_request(conn, cfg.os_region_name, "GET", "/v1/public_ports")
|
||||
if response.status_code >= 400:
|
||||
@@ -93,12 +99,17 @@ def _check_direct_net(cfg: Config, conn, checks: list[Check]) -> None:
|
||||
"прямой IP API",
|
||||
True,
|
||||
False,
|
||||
f"public-net HTTP {response.status_code} в {cfg.os_region_name} — up покажет ошибку",
|
||||
f"public-network HTTP {response.status_code} ({pool_note}) — up покажет ошибку",
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
Check("прямой IP API", True, False, f"public-net ok ({cfg.os_region_name})")
|
||||
Check(
|
||||
"прямой IP API",
|
||||
True,
|
||||
False,
|
||||
f"public-network ok ({pool_note}, {public_net_base(cfg.os_region_name)})",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
checks.append(Check("прямой IP API", True, False, f"не достучались: {exc}"))
|
||||
|
||||
+58
-29
@@ -1,14 +1,14 @@
|
||||
"""Selectel Public Net API: direct public IP (no router, no floating IP).
|
||||
"""Selectel Public Network API: direct public IP (no router, no floating IP).
|
||||
|
||||
Endpoint (Keystone project token):
|
||||
https://<pool>.cloud.api.selcloud.ru/public-net/v1/public_ports
|
||||
Docs: https://docs.selectel.ru/api/cloud-public-network/
|
||||
Go SDK example URL: https://ru-3.cloud.api.selcloud.ru/public-net/
|
||||
https://<api-pool>.cloud.api.selcloud.ru/public-network/v1/public_ports
|
||||
Docs: https://docs.selectel.ru/api/urls/ (public-network, not public-net)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
@@ -21,18 +21,29 @@ from gpu_rent.os_client import NET_NAME, ROUTER_NAME, SUBNET_NAME
|
||||
Log = Callable[[str], None]
|
||||
PORT_DESCRIPTION = "gpu-rent"
|
||||
|
||||
# Compute pool (OS_REGION_NAME) → pool where public-network API is deployed.
|
||||
# ru-6 has compute/multizone but direct-IP API is only on ru-2/ru-7/gis-1 (Moscow).
|
||||
PUBLIC_NETWORK_API_BY_COMPUTE: dict[str, str] = {
|
||||
"ru-6": "ru-7",
|
||||
}
|
||||
|
||||
def public_net_base(region: str) -> str:
|
||||
pool = (region or "").strip()
|
||||
|
||||
def public_network_pool(compute_region: str) -> str:
|
||||
pool = (compute_region or "").strip()
|
||||
if not pool:
|
||||
raise CloudError("OS_REGION_NAME пуст — нет пула для public-net API")
|
||||
return f"https://{pool}.cloud.api.selcloud.ru/public-net"
|
||||
raise CloudError("OS_REGION_NAME пуст — нет пула для public-network API")
|
||||
return PUBLIC_NETWORK_API_BY_COMPUTE.get(pool, pool)
|
||||
|
||||
|
||||
def public_net_base(compute_region: str) -> str:
|
||||
api_pool = public_network_pool(compute_region)
|
||||
return f"https://{api_pool}.cloud.api.selcloud.ru/public-network"
|
||||
|
||||
|
||||
def keystone_token(conn) -> str:
|
||||
sess = getattr(conn, "session", None)
|
||||
if sess is None:
|
||||
raise CloudError("нет Keystone session — не вызвать public-net API")
|
||||
raise CloudError("нет Keystone session — не вызвать public-network API")
|
||||
token = sess.get_token()
|
||||
if not token:
|
||||
authorize = getattr(conn, "authorize", None)
|
||||
@@ -40,7 +51,7 @@ def keystone_token(conn) -> str:
|
||||
authorize()
|
||||
token = sess.get_token()
|
||||
if not token:
|
||||
raise CloudError("Keystone не дал токен для public-net API")
|
||||
raise CloudError("Keystone не дал токен для public-network API")
|
||||
return str(token)
|
||||
|
||||
|
||||
@@ -68,7 +79,7 @@ def _parse_port(raw: dict[str, Any]) -> PublicPort:
|
||||
pid = str(raw.get("id") or "")
|
||||
ip = str(raw.get("ip_address") or raw.get("ipAddress") or "")
|
||||
if not pid or not ip:
|
||||
raise CloudError(f"public-net: неполный port в ответе: {sorted(raw)}")
|
||||
raise CloudError(f"public-network: неполный port в ответе: {sorted(raw)}")
|
||||
return PublicPort(
|
||||
id=pid,
|
||||
ip_address=ip,
|
||||
@@ -78,20 +89,28 @@ def _parse_port(raw: dict[str, Any]) -> PublicPort:
|
||||
|
||||
|
||||
def _raise_http(what: str, response: httpx.Response) -> None:
|
||||
body = (response.text or "")[:300].replace("\n", " ")
|
||||
body = (response.text or "")[:300].replace("\n", " ").strip()
|
||||
if response.status_code == 404 and "<html" in body.lower():
|
||||
raise CloudError(
|
||||
f"{what}: HTTP 404 — проверь public-network API для пула "
|
||||
f"(docs/selectel.ru/api/urls/). Неверный URL или сервис недоступен в регионе."
|
||||
)
|
||||
if body:
|
||||
body = re.sub(r"<[^>]+>", " ", body)
|
||||
body = re.sub(r"\s+", " ", body).strip()[:200]
|
||||
raise CloudError(f"{what}: HTTP {response.status_code} {body}".strip())
|
||||
|
||||
|
||||
def public_net_request(
|
||||
conn,
|
||||
region: str,
|
||||
compute_region: str,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
json: dict[str, Any] | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> httpx.Response:
|
||||
url = public_net_base(region).rstrip("/") + path
|
||||
url = public_net_base(compute_region).rstrip("/") + path
|
||||
headers = {
|
||||
"X-Auth-Token": keystone_token(conn),
|
||||
"Accept": "application/json",
|
||||
@@ -99,26 +118,32 @@ def public_net_request(
|
||||
return httpx.request(method, url, headers=headers, json=json, timeout=timeout)
|
||||
|
||||
|
||||
def get_public_port(conn, region: str, port_id: str) -> PublicPort | None:
|
||||
response = public_net_request(conn, region, "GET", f"/v1/public_ports/{port_id}")
|
||||
def get_public_port(conn, compute_region: str, port_id: str) -> PublicPort | None:
|
||||
response = public_net_request(
|
||||
conn, compute_region, "GET", f"/v1/public_ports/{port_id}"
|
||||
)
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
if response.status_code >= 400:
|
||||
_raise_http("public-net GET port", response)
|
||||
_raise_http("public-network GET port", response)
|
||||
data = response.json()
|
||||
raw = data.get("port") if isinstance(data, dict) else None
|
||||
if not isinstance(raw, dict):
|
||||
raise CloudError("public-net GET: нет объекта port")
|
||||
raise CloudError("public-network GET: нет объекта port")
|
||||
return _parse_port(raw)
|
||||
|
||||
|
||||
def create_public_port(
|
||||
conn,
|
||||
region: str,
|
||||
compute_region: str,
|
||||
*,
|
||||
security_group_ids: list[str] | None = None,
|
||||
description: str = PORT_DESCRIPTION,
|
||||
log: Log | None = None,
|
||||
) -> PublicPort:
|
||||
api_pool = public_network_pool(compute_region)
|
||||
if log and api_pool != (compute_region or "").strip():
|
||||
log(f"public-network API: {compute_region} → {api_pool}")
|
||||
body: dict[str, Any] = {
|
||||
"description": description,
|
||||
"admin_state_up": True,
|
||||
@@ -128,23 +153,25 @@ def create_public_port(
|
||||
pid = _project_id(conn)
|
||||
if pid:
|
||||
body["project_id"] = pid
|
||||
response = public_net_request(conn, region, "POST", "/v1/public_ports", json=body)
|
||||
response = public_net_request(
|
||||
conn, compute_region, "POST", "/v1/public_ports", json=body
|
||||
)
|
||||
if response.status_code not in {200, 201}:
|
||||
_raise_http("public-net создать прямой IP", response)
|
||||
_raise_http("public-network создать прямой IP", response)
|
||||
data = response.json()
|
||||
raw = data.get("port") if isinstance(data, dict) else None
|
||||
if not isinstance(raw, dict):
|
||||
raise CloudError("public-net create: нет объекта port")
|
||||
raise CloudError("public-network create: нет объекта port")
|
||||
return _parse_port(raw)
|
||||
|
||||
|
||||
def delete_public_port(conn, region: str, port_id: str, log: Log) -> None:
|
||||
def delete_public_port(conn, compute_region: str, port_id: str, log: Log) -> None:
|
||||
"""Detach-safe: retry 409 until Nova dropped the NIC (or 404)."""
|
||||
path = f"/v1/public_ports/{port_id}"
|
||||
deadline = time.time() + 90
|
||||
last = 0
|
||||
while time.time() < deadline:
|
||||
response = public_net_request(conn, region, "DELETE", path)
|
||||
response = public_net_request(conn, compute_region, "DELETE", path)
|
||||
last = response.status_code
|
||||
if last in {204, 200, 404}:
|
||||
if last != 404:
|
||||
@@ -153,8 +180,8 @@ def delete_public_port(conn, region: str, port_id: str, log: Log) -> None:
|
||||
if last == 409:
|
||||
time.sleep(2)
|
||||
continue
|
||||
_raise_http("public-net удалить прямой IP", response)
|
||||
raise CloudError(f"public-net DELETE {port_id} всё ещё {last} (порт занят?)")
|
||||
_raise_http("public-network удалить прямой IP", response)
|
||||
raise CloudError(f"public-network DELETE {port_id} всё ещё {last} (порт занят?)")
|
||||
|
||||
|
||||
def is_rfc1918(addr: str) -> bool:
|
||||
@@ -199,7 +226,7 @@ def neutron_port_attached(conn, port_id: str) -> bool:
|
||||
|
||||
def allocate_public_port(
|
||||
conn,
|
||||
region: str,
|
||||
compute_region: str,
|
||||
*,
|
||||
sg_id: str | None,
|
||||
existing_id: str | None,
|
||||
@@ -209,7 +236,7 @@ def allocate_public_port(
|
||||
"""Create or reuse a direct public IP port. Not a Neutron floating IP."""
|
||||
if reuse and existing_id:
|
||||
try:
|
||||
port = get_public_port(conn, region, existing_id)
|
||||
port = get_public_port(conn, compute_region, existing_id)
|
||||
except CloudError as exc:
|
||||
log(f"старый прямой IP {existing_id}: {exc}")
|
||||
port = None
|
||||
@@ -220,7 +247,9 @@ def allocate_public_port(
|
||||
log(f"прямой IP {port.ip_address} уже на NIC — используем")
|
||||
return port
|
||||
sgs = [sg_id] if sg_id else None
|
||||
port = create_public_port(conn, region, security_group_ids=sgs)
|
||||
port = create_public_port(
|
||||
conn, compute_region, security_group_ids=sgs, log=log
|
||||
)
|
||||
log(f"прямой публичный IP {port.ip_address}")
|
||||
return port
|
||||
|
||||
|
||||
Reference in New Issue
Block a user