From 4d7a0f354d08abed4f09aa3ba5ee8adf333fb38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= Date: Sun, 23 Aug 2026 19:34:12 +0900 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D0=B0=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=81=D0=B5=D1=82=D0=B8=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20=D0=BD=D0=B0=20=D0=B6?= =?UTF-8?q?=D0=B8=D0=B2=D0=BE=D0=BC=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раздел переписан по факту поведения платформы, а не по документации. - номер сети лежит в resource_id, а не id, добавлен vpc_id_of() - список сетей приходит голым массивом - при создании принимается только имя, регион и участники только для чтения - подключение сервера асинхронно и блокирует сервер, добавлен vpc_wait_member() - отключение участника и удаление сети настойчивые, ждут снятия блокировки Добавлен пример private_network.py. Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu --- README.md | 35 ++++++++++++--- examples/private_network.py | 33 ++++++++++++++ regcloud_api/client.py | 90 ++++++++++++++++++++++++++++++------- 3 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 examples/private_network.py diff --git a/README.md b/README.md index bc78c97..176026a 100644 --- a/README.md +++ b/README.md @@ -104,20 +104,43 @@ api.delete_snapshot(snap["id"]) api.destroy_server(id) # дождётся снятия блокировки ``` -## Адреса и сети +## Адреса ```python api.ips(server_id) api.add_ip(server_id, ipv4=1) api.set_ptr("1.2.3.4", "mail.example.com") 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 diff --git a/examples/private_network.py b/examples/private_network.py new file mode 100644 index 0000000..c0ff866 --- /dev/null +++ b/examples/private_network.py @@ -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) diff --git a/regcloud_api/client.py b/regcloud_api/client.py index 9395938..eea8925 100644 --- a/regcloud_api/client.py +++ b/regcloud_api/client.py @@ -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 ----------