Refactor public network API handling and enhance connectivity checks
- Updated the public network handling to resolve the base URL dynamically based on Keystone catalog. - Simplified the public network pool function to directly return the compute region. - Enhanced error messages in the doctor command for better clarity on public network status. - Introduced new functions for managing security groups and ensuring visibility of public ports in Neutron. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -83,15 +83,11 @@ 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_base, public_net_request, public_network_pool
|
||||
from gpu_rent.public_net import public_net_base, public_net_request, resolve_public_net_base
|
||||
|
||||
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
|
||||
)
|
||||
pool = cfg.os_region_name
|
||||
try:
|
||||
base = resolve_public_net_base(conn, cfg.os_region_name)
|
||||
response = public_net_request(conn, cfg.os_region_name, "GET", "/v1/public_ports")
|
||||
if response.status_code >= 400:
|
||||
checks.append(
|
||||
@@ -99,7 +95,7 @@ def _check_direct_net(cfg: Config, conn, checks: list[Check]) -> None:
|
||||
"прямой IP API",
|
||||
True,
|
||||
False,
|
||||
f"public-network HTTP {response.status_code} ({pool_note}) — up покажет ошибку",
|
||||
f"public-network HTTP {response.status_code} ({pool}, {base}) — up покажет ошибку",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -108,7 +104,7 @@ def _check_direct_net(cfg: Config, conn, checks: list[Check]) -> None:
|
||||
"прямой IP API",
|
||||
True,
|
||||
False,
|
||||
f"public-network ok ({pool_note}, {public_net_base(cfg.os_region_name)})",
|
||||
f"public-network ok ({pool}, {base})",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -25,12 +25,13 @@ SUBNET_CIDR = "192.168.77.0/24"
|
||||
BOOT_VOLUME_SIZE_GB = 30
|
||||
|
||||
|
||||
def connect(cfg: Config):
|
||||
def connect(cfg: Config, *, region_name: str | None = None):
|
||||
try:
|
||||
import openstack
|
||||
except ImportError as exc:
|
||||
raise CloudError("Нет openstacksdk. Переустанови пакет: pip install -e .") from exc
|
||||
|
||||
region = (region_name or cfg.os_region_name).strip()
|
||||
try:
|
||||
conn = openstack.connect(
|
||||
auth_url=cfg.os_auth_url,
|
||||
@@ -39,7 +40,7 @@ def connect(cfg: Config):
|
||||
password=cfg.os_password,
|
||||
user_domain_name=cfg.os_user_domain_name,
|
||||
project_domain_name=cfg.os_user_domain_name,
|
||||
region_name=cfg.os_region_name,
|
||||
region_name=region,
|
||||
identity_api_version="3",
|
||||
interface="public",
|
||||
compute_api_version=COMPUTE_MICROVERSION,
|
||||
|
||||
+68
-13
@@ -16,28 +16,46 @@ from typing import Any, Callable
|
||||
import httpx
|
||||
|
||||
from gpu_rent.errors import CloudError
|
||||
from gpu_rent.os_client import NET_NAME, ROUTER_NAME, SUBNET_NAME
|
||||
from gpu_rent.os_client import NET_NAME, ROUTER_NAME, SG_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",
|
||||
}
|
||||
# Keystone service types for direct public IP API (Selectel catalog).
|
||||
PUBLIC_NETWORK_SERVICE_TYPES = ("public-net-api", "public-network", "public-net")
|
||||
|
||||
|
||||
def public_network_pool(compute_region: str) -> str:
|
||||
"""Pool where the port must exist — always the compute/OS_REGION_NAME pool."""
|
||||
pool = (compute_region or "").strip()
|
||||
if not pool:
|
||||
raise CloudError("OS_REGION_NAME пуст — нет пула для public-network API")
|
||||
return PUBLIC_NETWORK_API_BY_COMPUTE.get(pool, pool)
|
||||
return pool
|
||||
|
||||
|
||||
def resolve_public_net_base(conn, compute_region: str) -> str:
|
||||
"""Public-network API base URL for compute pool (Keystone catalog, then default host)."""
|
||||
pool = public_network_pool(compute_region)
|
||||
sess = getattr(conn, "session", None)
|
||||
get_ep = getattr(sess, "get_endpoint", None) if sess is not None else None
|
||||
if callable(get_ep):
|
||||
for stype in PUBLIC_NETWORK_SERVICE_TYPES:
|
||||
try:
|
||||
url = get_ep(
|
||||
service_type=stype,
|
||||
interface="public",
|
||||
region_name=pool,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
if url:
|
||||
return str(url).rstrip("/")
|
||||
return f"https://{pool}.cloud.api.selcloud.ru/public-network"
|
||||
|
||||
|
||||
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"
|
||||
pool = public_network_pool(compute_region)
|
||||
return f"https://{pool}.cloud.api.selcloud.ru/public-network"
|
||||
|
||||
|
||||
def keystone_token(conn) -> str:
|
||||
@@ -110,7 +128,7 @@ def public_net_request(
|
||||
json: dict[str, Any] | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> httpx.Response:
|
||||
url = public_net_base(compute_region).rstrip("/") + path
|
||||
url = resolve_public_net_base(conn, compute_region).rstrip("/") + path
|
||||
headers = {
|
||||
"X-Auth-Token": keystone_token(conn),
|
||||
"Accept": "application/json",
|
||||
@@ -118,6 +136,43 @@ def public_net_request(
|
||||
return httpx.request(method, url, headers=headers, json=json, timeout=timeout)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityGroupRef:
|
||||
id: str
|
||||
name: str = SG_NAME
|
||||
|
||||
|
||||
def ensure_public_network_security_group(
|
||||
conn,
|
||||
cidr: str,
|
||||
compute_region: str,
|
||||
log: Log,
|
||||
) -> SecurityGroupRef:
|
||||
"""SG for direct public IP — same Neutron pool as OS_REGION_NAME."""
|
||||
from gpu_rent.cloud import ensure_security_group
|
||||
|
||||
_ = compute_region # port/SG pool == compute pool
|
||||
sg = ensure_security_group(conn, cidr, log)
|
||||
return SecurityGroupRef(id=str(sg.id), name=SG_NAME)
|
||||
|
||||
|
||||
def wait_neutron_port(conn, port_id: str, log: Log, *, timeout: float = 90.0) -> None:
|
||||
"""Ensure Nova/Neutron in compute pool sees the public port before create_server."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
if conn.network.get_port(port_id) is not None:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
region = getattr(conn, "region_name", None) or "?"
|
||||
raise CloudError(
|
||||
f"порт {port_id} создан в public-network, но Neutron ({region}) его не видит — "
|
||||
"порт и VM должны быть в одном пуле (OS_REGION_NAME=ru-6 → порт в ru-6, не ru-7)"
|
||||
)
|
||||
|
||||
|
||||
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}"
|
||||
@@ -141,9 +196,8 @@ def create_public_port(
|
||||
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}")
|
||||
if log:
|
||||
log(f"public-network: {resolve_public_net_base(conn, compute_region)}")
|
||||
body: dict[str, Any] = {
|
||||
"description": description,
|
||||
"admin_state_up": True,
|
||||
@@ -250,6 +304,7 @@ def allocate_public_port(
|
||||
port = create_public_port(
|
||||
conn, compute_region, security_group_ids=sgs, log=log
|
||||
)
|
||||
wait_neutron_port(conn, port.id, log)
|
||||
log(f"прямой публичный IP {port.ip_address}")
|
||||
return port
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from gpu_rent.cloud import (
|
||||
ensure_data_volume,
|
||||
ensure_floating_ip,
|
||||
ensure_keypair,
|
||||
ensure_security_group,
|
||||
guess_operator_cidr,
|
||||
pick_existing_server,
|
||||
server_status,
|
||||
@@ -22,6 +21,7 @@ from gpu_rent.cloud import (
|
||||
from gpu_rent.public_net import (
|
||||
allocate_public_port,
|
||||
drop_legacy_private_network,
|
||||
ensure_public_network_security_group,
|
||||
wait_server_public_ip,
|
||||
)
|
||||
from gpu_rent.bootstrap import run_bootstrap
|
||||
@@ -578,7 +578,7 @@ def cmd_up(
|
||||
log("SG SSH: GPU_RENT_SSH_CIDR=0.0.0.0/0")
|
||||
else:
|
||||
log("не удалось узнать твой IP — SG откроет SSH с 0.0.0.0/0")
|
||||
sg = ensure_security_group(conn, cidr, log)
|
||||
sg = ensure_public_network_security_group(conn, cidr, cfg.os_region_name, log)
|
||||
drop_legacy_private_network(conn, log)
|
||||
pub = allocate_public_port(
|
||||
conn,
|
||||
|
||||
Reference in New Issue
Block a user