Приватные сети разобраны на живом API
Раздел переписан по факту поведения платформы, а не по документации. - номер сети лежит в resource_id, а не id, добавлен vpc_id_of() - список сетей приходит голым массивом - при создании принимается только имя, регион и участники только для чтения - подключение сервера асинхронно и блокирует сервер, добавлен vpc_wait_member() - отключение участника и удаление сети настойчивые, ждут снятия блокировки Добавлен пример private_network.py. Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu
Этот коммит содержится в:
+29
-6
@@ -104,20 +104,43 @@ api.delete_snapshot(snap["id"])
|
|||||||
api.destroy_server(id) # дождётся снятия блокировки
|
api.destroy_server(id) # дождётся снятия блокировки
|
||||||
```
|
```
|
||||||
|
|
||||||
## Адреса и сети
|
## Адреса
|
||||||
|
|
||||||
```python
|
```python
|
||||||
api.ips(server_id)
|
api.ips(server_id)
|
||||||
api.add_ip(server_id, ipv4=1)
|
api.add_ip(server_id, ipv4=1)
|
||||||
api.set_ptr("1.2.3.4", "mail.example.com")
|
api.set_ptr("1.2.3.4", "mail.example.com")
|
||||||
api.delete_ip("1.2.3.4")
|
api.delete_ip("1.2.3.4")
|
||||||
|
|
||||||
vpc = api.create_vpc("backoffice", region="openstack-msk1")
|
|
||||||
api.vpc_attach(vpc["id"], server_id)
|
|
||||||
api.vpc_members(vpc["id"])
|
|
||||||
api.vpc_detach(vpc["id"], server_id)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Приватные сети
|
||||||
|
|
||||||
|
Серверы в одной приватной сети видят друг друга напрямую, минуя интернет. Трафик внутри
|
||||||
|
не тарифицируется, а машине-исполнителю можно не выдавать публичный адрес вовсе.
|
||||||
|
|
||||||
|
```python
|
||||||
|
net = api.create_vpc("backoffice") # кроме имени платформа ничего не принимает
|
||||||
|
net_id = RegCloud.vpc_id_of(net) # номер лежит в resource_id, а не в id
|
||||||
|
|
||||||
|
api.vpc_attach(net_id, server_id) # действие асинхронное
|
||||||
|
api.vpc_wait_member(net_id, server_id) # дожидаемся, пока сервер реально войдёт
|
||||||
|
|
||||||
|
api.vpc_members(net_id) # список номеров серверов
|
||||||
|
api.rename_vpc(net_id, "новое имя")
|
||||||
|
|
||||||
|
api.vpc_detach(net_id, server_id)
|
||||||
|
api.delete_vpc(net_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Что выяснено на живом API и чего нет в документации.
|
||||||
|
|
||||||
|
Номер сети лежит в поле `resource_id`, а не `id`. Список сетей приходит голым массивом,
|
||||||
|
без обёртки. При создании принимается только имя, а регион, участники и `resource_id`
|
||||||
|
объявлены только для чтения и проставляются платформой. Подключение сервера к сети
|
||||||
|
асинхронно, и пока оно не доиграет, сервер заблокирован, из-за чего не выйдет ни отключить
|
||||||
|
его, ни удалить сеть. Поэтому `vpc_detach()` и `delete_vpc()` тут настойчивые, повторяют
|
||||||
|
попытки, пока блокировка не снимется.
|
||||||
|
|
||||||
## Деньги
|
## Деньги
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Приватная сеть между серверами.
|
||||||
|
|
||||||
|
Зачем. Серверы в одной приватной сети видят друг друга напрямую, минуя интернет.
|
||||||
|
Трафик внутри не тарифицируется, а машине-исполнителю можно не давать публичный адрес
|
||||||
|
вообще, что и дешевле, и безопаснее. Управляющий сервер разговаривает с ней по локали.
|
||||||
|
|
||||||
|
Порядок такой: создать сеть, подключить в неё серверы, дождаться подключения.
|
||||||
|
"""
|
||||||
|
from regcloud_api import RegCloud
|
||||||
|
|
||||||
|
api = RegCloud()
|
||||||
|
|
||||||
|
print("сети на аккаунте:")
|
||||||
|
for v in api.vpcs():
|
||||||
|
print(f" {RegCloud.vpc_id_of(v)} {v['name']:16s} {v['region_slug']:12s} "
|
||||||
|
f"участников {len(v.get('members') or [])}")
|
||||||
|
|
||||||
|
net = api.create_vpc("backoffice")
|
||||||
|
net_id = RegCloud.vpc_id_of(net)
|
||||||
|
print("создана сеть", net_id)
|
||||||
|
|
||||||
|
# подключаем свои серверы
|
||||||
|
for server in api.servers():
|
||||||
|
api.vpc_attach(net_id, server["id"])
|
||||||
|
api.vpc_wait_member(net_id, server["id"])
|
||||||
|
print(" подключён", server["name"])
|
||||||
|
|
||||||
|
print("в сети сейчас:", api.vpc_members(net_id))
|
||||||
|
|
||||||
|
# отключать и удалять тоже надо уметь
|
||||||
|
# for sid in api.vpc_members(net_id):
|
||||||
|
# api.vpc_detach(net_id, sid)
|
||||||
|
# api.delete_vpc(net_id)
|
||||||
@@ -388,36 +388,96 @@ class RegCloud:
|
|||||||
return self.put(f"/ips/{ip}", json={"ip": ip, "ptr": ptr})
|
return self.put(f"/ips/{ip}", json={"ip": ip, "ptr": ptr})
|
||||||
|
|
||||||
# ---------- приватные сети ----------
|
# ---------- приватные сети ----------
|
||||||
|
#
|
||||||
|
# Приватная сеть связывает серверы напрямую, минуя интернет. Трафик внутри неё
|
||||||
|
# не тарифицируется и не выходит наружу, поэтому машину-исполнителя можно держать
|
||||||
|
# вообще без публичного адреса, а управляющий сервер разговаривает с ней по локали.
|
||||||
|
#
|
||||||
|
# Что выяснено на живом API и чего нет в документации.
|
||||||
|
# Номер сети лежит в поле resource_id, а не id.
|
||||||
|
# Список сетей приходит голым массивом, без обёртки.
|
||||||
|
# При создании принимается только имя. Регион, участники и resource_id
|
||||||
|
# объявлены только для чтения, платформа проставляет их сама.
|
||||||
|
# Подключение сервера к сети это асинхронное действие, в ответ приходит его карточка.
|
||||||
|
# Пока действие не доиграет, сервер заблокирован, и отключить его или удалить сеть
|
||||||
|
# не выйдет, поэтому обе операции тут настойчивые.
|
||||||
|
|
||||||
def vpcs(self) -> list[dict]:
|
def vpcs(self) -> list[dict]:
|
||||||
|
"""Приватные сети. Номер сети лежит в поле resource_id."""
|
||||||
data = self.get("/vpcs")
|
data = self.get("/vpcs")
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
return data.get("vpcs", []) if isinstance(data, dict) else []
|
return data.get("vpcs", []) if isinstance(data, dict) else []
|
||||||
|
|
||||||
def vpc(self, vpc_id: int) -> dict:
|
def vpc(self, vpc_id: int) -> dict:
|
||||||
data = self.get(f"/vpcs/{vpc_id}")
|
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:
|
def create_vpc(self, name: str) -> dict:
|
||||||
payload = {"name": name, "region_slug": region}
|
"""Создать приватную сеть.
|
||||||
if members:
|
|
||||||
payload["members"] = members
|
|
||||||
data = self.post("/vpcs", json=payload)
|
|
||||||
return data.get("vpc", data)
|
|
||||||
|
|
||||||
def delete_vpc(self, vpc_id: int) -> bool:
|
Кроме имени платформа ничего не принимает. Регион она выставляет сама,
|
||||||
self.delete(f"/vpcs/{vpc_id}")
|
участники добавляются потом через vpc_attach()."""
|
||||||
return True
|
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")
|
data = self.get(f"/vpcs/{vpc_id}/members")
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
return data.get("members", []) if isinstance(data, dict) else []
|
return data.get("members", []) if isinstance(data, dict) else []
|
||||||
|
|
||||||
def vpc_attach(self, vpc_id: int, server_id: int) -> dict:
|
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:
|
def vpc_detach(self, vpc_id: int, server_id: int, patient: bool = True,
|
||||||
self.delete(f"/vpcs/{vpc_id}/members/{server_id}")
|
timeout: float = 300) -> bool:
|
||||||
return True
|
"""Отключить сервер от сети. Если он занят другой операцией, ждём."""
|
||||||
|
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 ----------
|
# ---------- kubernetes ----------
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user