Приватные сети разобраны на живом API

Раздел переписан по факту поведения платформы, а не по документации.

- номер сети лежит в resource_id, а не id, добавлен vpc_id_of()
- список сетей приходит голым массивом
- при создании принимается только имя, регион и участники только для чтения
- подключение сервера асинхронно и блокирует сервер, добавлен vpc_wait_member()
- отключение участника и удаление сети настойчивые, ждут снятия блокировки

Добавлен пример private_network.py.

Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu
Этот коммит содержится в:
2026-08-23 19:34:12 +09:00
родитель 7e798bb68f
Коммит 4d7a0f354d
3 изменённых файлов: 137 добавлений и 21 удалений
+75 -15
Просмотреть файл
@@ -388,36 +388,96 @@ class RegCloud:
return self.put(f"/ips/{ip}", json={"ip": ip, "ptr": ptr})
# ---------- приватные сети ----------
#
# Приватная сеть связывает серверы напрямую, минуя интернет. Трафик внутри неё
# не тарифицируется и не выходит наружу, поэтому машину-исполнителя можно держать
# вообще без публичного адреса, а управляющий сервер разговаривает с ней по локали.
#
# Что выяснено на живом API и чего нет в документации.
# Номер сети лежит в поле resource_id, а не id.
# Список сетей приходит голым массивом, без обёртки.
# При создании принимается только имя. Регион, участники и resource_id
# объявлены только для чтения, платформа проставляет их сама.
# Подключение сервера к сети это асинхронное действие, в ответ приходит его карточка.
# Пока действие не доиграет, сервер заблокирован, и отключить его или удалить сеть
# не выйдет, поэтому обе операции тут настойчивые.
def vpcs(self) -> list[dict]:
"""Приватные сети. Номер сети лежит в поле resource_id."""
data = self.get("/vpcs")
if isinstance(data, list):
return data
return data.get("vpcs", []) if isinstance(data, dict) else []
def vpc(self, vpc_id: int) -> dict:
data = self.get(f"/vpcs/{vpc_id}")
return data.get("vpc", data)
return data.get("vpc", data) if isinstance(data, dict) else data
def create_vpc(self, name: str, region: str, members: list[int] | None = None) -> dict:
payload = {"name": name, "region_slug": region}
if members:
payload["members"] = members
data = self.post("/vpcs", json=payload)
return data.get("vpc", data)
def create_vpc(self, name: str) -> dict:
"""Создать приватную сеть.
def delete_vpc(self, vpc_id: int) -> bool:
self.delete(f"/vpcs/{vpc_id}")
return True
Кроме имени платформа ничего не принимает. Регион она выставляет сама,
участники добавляются потом через vpc_attach()."""
data = self.post("/vpcs", json={"name": name})
return data.get("vpc", data) if isinstance(data, dict) else data
def vpc_members(self, vpc_id: int) -> list[dict]:
def rename_vpc(self, vpc_id: int, name: str) -> dict:
return self.put(f"/vpcs/{vpc_id}", json={"name": name})
def delete_vpc(self, vpc_id: int, patient: bool = True, timeout: float = 300) -> bool:
"""Удалить сеть. Если её участники заняты, ждём и повторяем."""
deadline = time.time() + timeout
while True:
try:
self.delete(f"/vpcs/{vpc_id}")
return True
except NotFound:
return True
except (Locked, ValidationError):
if not patient or time.time() > deadline:
raise
time.sleep(15)
def vpc_members(self, vpc_id: int) -> list[int]:
"""Номера серверов в сети."""
data = self.get(f"/vpcs/{vpc_id}/members")
if isinstance(data, list):
return data
return data.get("members", []) if isinstance(data, dict) else []
def vpc_attach(self, vpc_id: int, server_id: int) -> dict:
return self.post(f"/vpcs/{vpc_id}/members", json={"resource_id": server_id})
"""Подключить сервер к сети. Действие асинхронное, сервер на время блокируется."""
data = self.post(f"/vpcs/{vpc_id}/members", json={"resource_id": server_id})
return data.get("action", data) if isinstance(data, dict) else data
def vpc_detach(self, vpc_id: int, server_id: int) -> bool:
self.delete(f"/vpcs/{vpc_id}/members/{server_id}")
return True
def vpc_detach(self, vpc_id: int, server_id: int, patient: bool = True,
timeout: float = 300) -> bool:
"""Отключить сервер от сети. Если он занят другой операцией, ждём."""
deadline = time.time() + timeout
while True:
try:
self.delete(f"/vpcs/{vpc_id}/members/{server_id}")
return True
except NotFound:
return True
except (Locked, ValidationError):
if not patient or time.time() > deadline:
raise
time.sleep(15)
def vpc_wait_member(self, vpc_id: int, server_id: int, timeout: float = 300) -> bool:
"""Дождаться, пока сервер действительно окажется в сети."""
deadline = time.time() + timeout
while time.time() < deadline:
if server_id in self.vpc_members(vpc_id):
return True
time.sleep(5)
raise Timeout(f"сервер {server_id} не появился в сети {vpc_id}")
@staticmethod
def vpc_id_of(vpc: dict) -> int:
"""Номер сети. Платформа кладёт его в resource_id, а не в id."""
return int(vpc.get("resource_id") or vpc.get("id") or 0)
# ---------- kubernetes ----------