From a106ba5b6bd46b8f800e1eae4d1f67867853b176 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 18:18:27 +0900 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BE=D0=B1=D1=89=D1=91=D0=BD,=20=D1=81=D0=BD=D1=8F?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BF=D1=80=D0=B8=D0=B2=D1=8F=D0=B7=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BA=20=D0=B2=D0=B8=D0=B4=D0=B5=D0=BE=D0=BA=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPU это лишь один из тарифов, а клиент общего назначения. Примеры переписаны на обычные серверы, добавлен пример с масштабированием под нагрузку. Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu --- README.md | 121 ++++++++++++++++++++++------------- examples/basic.py | 18 ++++++ examples/gpu_lifecycle.py | 28 -------- examples/scale_by_load.py | 37 +++++++++++ examples/server_lifecycle.py | 29 +++++++++ regcloud_api/client.py | 16 ++--- 6 files changed, 167 insertions(+), 82 deletions(-) create mode 100644 examples/basic.py delete mode 100644 examples/gpu_lifecycle.py create mode 100644 examples/scale_by_load.py create mode 100644 examples/server_lifecycle.py diff --git a/README.md b/README.md index f8e1ca9..2870130 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,8 @@ Клиент к API облака REG.RU (Рег.облако), точка входа `https://api.cloudvps.reg.ru`. -Само API устроено как у DigitalOcean и особой сложности не представляет. Обёртка нужна -из-за неочевидных мест, на которые мы уже наступили в бою, и которые тут обойдены. - -## Что тут учтено - -**Адрес машины лежит в поле `ip`.** Поле `networks`, которое по документации должно -его содержать, всегда пустое. Метод `ip_of()` знает про это. - -**Снимок нельзя снять с остановленной машины.** API отвечает `SERVER_IS_STOPPED`. -Останавливать её должен он сам, для этого есть параметр `offline`. - -**Сразу после снимка машина сидит в `locked` и удаляться отказывается**, отвечая -`RESOURCE_DELETE_ERROR`. Отпускает примерно через полминуты. `destroy()` по умолчанию -долбится, пока не поддастся, потому что забытая машина продолжает тикать по деньгам. - -**Тарифы с видеокартами метод `sizes()` не показывает вообще**, хотя создание с ними -прекрасно работает. Их список лежит в [docs_slugs.md](docs_slugs.md), там же образы, -регионы и правила составления slug'ов. - -**Тарификация идёт от часа.** Двадцать минут работы списываются как полный час, -проверено на живой машине. Поднимать машину на короткие задачи невыгодно. +Создание и удаление серверов, снимки, SSH-ключи, тарифы, образы, баланс и цены. +Само API устроено как у DigitalOcean, сервер там называется reglet. ## Установка @@ -30,9 +11,10 @@ pip install git+https://git.stackworks.ru/viktor/regcloud-api.git ``` -Токен берётся в личном кабинете и кладётся в `REGRU_CLOUD_TOKEN`. +Токен берётся в личном кабинете, в разделе настроек, и кладётся в `REGRU_CLOUD_TOKEN`. +Либо передаётся в конструктор явно. -## Как пользоваться +## Быстрый старт ```python from regcloud_api import RegCloud @@ -45,45 +27,92 @@ for s in api.list(): print(api.balance()) ``` -Поднять машину с видеокартой, снять с неё образ и удалить. +Поднять сервер и дождаться готовности. ```python -srv = api.create(size="c8-m32-d128-g1-a4000-gpu", - image="ubuntu-24-04-amd64-gpu", - name="worker", - ssh_keys=["d7:3a:..."], - region="openstack-spb1") +srv = api.create( + size="c2-m4-d60-hp", # 2 ядра, 4 ГБ, 60 ГБ, линейка «производительный» + image="ubuntu-24-04-amd64", + name="web-01", + region="openstack-msk1", + ssh_keys=["d7:3a:b4:..."], # отпечаток ключа из api.ssh_keys() +) info = api.wait_active(srv["id"]) -print("адрес", RegCloud.ip_of(info)) - -api.snapshot(srv["id"], "worker-base", offline=True) # машину остановит сам -api.destroy(srv["id"]) # дождётся снятия locked +print("адрес:", RegCloud.ip_of(info)) ``` -Дальше машину можно поднимать уже из снимка, передав его номер вместо имени образа. +Снять образ и поднять из него копию. ```python -api.create(size="c8-m32-d128-g1-a4000-gpu", image=7887485, region="openstack-spb1") +api.snapshot(srv["id"], "web-01-base", offline=True) # сервер остановит сам +copy = api.create(size="c2-m4-d60-hp", image=7887485, region="openstack-msk1") ``` -## Что умеет +Удалить. + +```python +api.destroy(srv["id"]) +``` + +## Методы | Метод | Что делает | |---|---| -| `list()`, `get(id)` | машины на аккаунте | -| `create(...)` | поднять машину, из образа или из снимка | -| `destroy(id)` | удалить, настойчиво | -| `stop(id)`, `start(id)` | выключить и включить | -| `action(id, type, **kw)` | любое действие из списка API | -| `snapshot(id, name)` | снять образ | -| `wait_active(id)`, `wait_action(id)` | дождаться готовности | +| `list()`, `get(id)` | серверы на аккаунте | +| `create(size, image, name, ssh_keys, region)` | создать сервер, из образа или из снимка | +| `destroy(id)` | удалить | +| `stop(id)`, `start(id)` | выключить, включить | +| `action(id, type, **kw)` | любое действие: reboot, rebuild, resize, clone, restore и прочие | +| `snapshot(id, name, offline)` | снять образ | +| `wait_active(id)`, `wait_action(id)` | дождаться готовности сервера или завершения действия | | `sizes()`, `gpu_sizes()` | тарифы | | `images()`, `snapshots()` | образы и снимки | | `ssh_keys()` | ключи доступа | | `balance()`, `prices()` | деньги | +| `RegCloud.ip_of(server)` | вытащить адрес из карточки сервера | -## Полезное рядом +## Особенности API, которые тут уже обойдены -- [docs_slugs.md](docs_slugs.md) — тарифы с картами, образы, регионы, правила slug'ов -- [openapi_v1.json](openapi_v1.json) — спецификация API, снятая с их сервера +Это не претензии к платформе, просто места, где поведение отличается от ожидаемого. +Собрано на живых серверах, чтобы вы на них не наступали. + +**Адрес сервера лежит в поле `ip`.** Поле `networks`, которое по документации должно +его содержать, на практике всегда пустое. `ip_of()` смотрит в оба места. + +**Снимок снимается только с работающего сервера.** У остановленного API отвечает +`SERVER_IS_STOPPED`. Останавливать его для снимка должна сама платформа, +для этого есть параметр `offline`. + +**Сразу после снимка сервер сидит в `locked`** и на удаление отвечает +`RESOURCE_DELETE_ERROR`. Отпускает примерно через полминуты. Поэтому `destroy()` +по умолчанию повторяет попытки, пока не получится, и считает `NO_SUCH_REGLET` +успехом. Забытый сервер продолжает тарифицироваться, так что молча сдаваться тут нельзя. + +**`sizes()` показывает не все тарифы.** Часть линеек метод не возвращает, хотя создание +с ними работает нормально. Полный список смотрите в панели управления, в разделе +настроек, пункт со slug'ами для API и Terraform. Наша выборка лежит в [docs_slugs.md](docs_slugs.md). + +**Тарификация идёт от часа.** Двадцать минут работы списываются как полный час. +Короткие запуски невыгодны, задачи лучше копить и выполнять пачкой. + +## Составление slug тарифа + +`c<ядра>-m<память>-d<диск>-<линейка>`, например `c4-m8-d120-hp`. + +Линейки: `std` стандартный, `stdp` стандартный плюс, `hp` производительный, +`hps` производительный ФЗ-152, `ddc` выделенный CPU, `high` высокочастотный. +Допустимые сочетания ядер, памяти и диска смотрите в [docs_slugs.md](docs_slugs.md). + +## Регионы + +`openstack-msk1`, `openstack-msk2`, `openstack-msk3`, `openstack-spb1`, +`openstack-sam1`, `openstack-fz1` и архивный `msk1`. + +Не все образы и тарифы доступны в каждом регионе. + +## Рядом + +- [docs_slugs.md](docs_slugs.md) — тарифы, образы, регионы, правила составления slug'ов +- [openapi_v1.json](openapi_v1.json) — спецификация API, снятая с сервера платформы +- [examples/](examples/) — примеры diff --git a/examples/basic.py b/examples/basic.py new file mode 100644 index 0000000..cbfb2c1 --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,18 @@ +"""Основные операции. Токен положите в REGRU_CLOUD_TOKEN.""" +from regcloud_api import RegCloud + +api = RegCloud() + +print("баланс:", api.balance().get("balance_data", {}).get("balance"), "р") + +print("\nсерверы:") +for s in api.list(): + print(f" {s['id']} {s['name']:20s} {s['status']:8s} {RegCloud.ip_of(s)}") + +print("\nключи доступа:") +for k in api.ssh_keys(): + print(f" {k['id']} {k['name']:15s} {k['fingerprint']}") + +print("\nснимки:") +for s in api.snapshots(): + print(f" {s['id']} {s['name']:20s} {s.get('size_gigabytes')} ГБ") diff --git a/examples/gpu_lifecycle.py b/examples/gpu_lifecycle.py deleted file mode 100644 index 9098034..0000000 --- a/examples/gpu_lifecycle.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Полный цикл работы с машиной под видеокарту. - -Поднимаем из готового снимка, работаем, снимаем новый образ, удаляем. -Именно так экономятся деньги: платим за часы счёта, а не за месяц простоя. -""" -import os -from regcloud_api import RegCloud - -SIZE = "c8-m32-d128-g1-a4000-gpu" # A4000 16 ГБ, 37.80 р/час -REGION = "openstack-spb1" # карты стоят только в Питере -KEY_FINGERPRINT = os.environ.get("SSH_FINGERPRINT", "") - -api = RegCloud() - -print("баланс:", api.balance().get("balance_data", {}).get("balance"), "р") - -srv = api.create(size=SIZE, image="ubuntu-24-04-amd64-gpu", name="worker", - ssh_keys=[KEY_FINGERPRINT] if KEY_FINGERPRINT else None, region=REGION) -info = api.wait_active(srv["id"]) -print("машина готова:", RegCloud.ip_of(info)) - -# ... тут работа на машине по ssh ... - -api.snapshot(srv["id"], "worker-ready", offline=True) -print("снимок поставлен в очередь") - -api.destroy(srv["id"]) -print("машина удалена, снимок остался") diff --git a/examples/scale_by_load.py b/examples/scale_by_load.py new file mode 100644 index 0000000..fb57595 --- /dev/null +++ b/examples/scale_by_load.py @@ -0,0 +1,37 @@ +"""Поднимать серверы под нагрузку и гасить, когда она спала. + +Тарификация идёт от часа, поэтому есть смысл держать машину не меньше часа +и не поднимать её ради пары минут работы. +""" +import time +from regcloud_api import RegCloud + +api = RegCloud() +POOL_NAME = "worker-" +SIZE = "c4-m8-d120-hp" +REGION = "openstack-msk1" + + +def workers(): + return [s for s in api.list() if s["name"].startswith(POOL_NAME)] + + +def scale_to(count: int): + now = workers() + while len(now) < count: + n = len(now) + 1 + srv = api.create(size=SIZE, image="ubuntu-24-04-amd64", + name=f"{POOL_NAME}{n:02d}", region=REGION) + print("поднял", srv["id"]) + now = workers() + while len(now) > count: + victim = now[-1] + api.destroy(victim["id"]) + print("погасил", victim["id"]) + now = workers() + + +if __name__ == "__main__": + scale_to(2) + time.sleep(5) + print("сейчас работает:", len(workers())) diff --git a/examples/server_lifecycle.py b/examples/server_lifecycle.py new file mode 100644 index 0000000..c56f6c0 --- /dev/null +++ b/examples/server_lifecycle.py @@ -0,0 +1,29 @@ +"""Полный цикл: создать сервер, снять с него образ, удалить. + +Приём годится для чего угодно: подготовить эталонную машину, снять образ +и дальше поднимать из него копии за пару минут вместо повторной настройки. +""" +import os +from regcloud_api import RegCloud + +SIZE = os.environ.get("SIZE", "c2-m4-d60-hp") +IMAGE = os.environ.get("IMAGE", "ubuntu-24-04-amd64") +REGION = os.environ.get("REGION", "openstack-msk1") +KEY = os.environ.get("SSH_FINGERPRINT", "") + +api = RegCloud() + +srv = api.create(size=SIZE, image=IMAGE, name="example-01", + ssh_keys=[KEY] if KEY else None, region=REGION) +print("создан сервер", srv["id"]) + +info = api.wait_active(srv["id"]) +print("готов, адрес", RegCloud.ip_of(info)) + +# ... тут настройка машины по ssh ... + +api.snapshot(srv["id"], "example-base", offline=True) +print("образ поставлен в очередь, платформа сама остановит сервер") + +api.destroy(srv["id"]) +print("сервер удалён, образ остался") diff --git a/regcloud_api/client.py b/regcloud_api/client.py index 3b7b350..58d0773 100644 --- a/regcloud_api/client.py +++ b/regcloud_api/client.py @@ -10,14 +10,13 @@ API устроено как у DigitalOcean, сервер там называе print(s["id"], s["name"], s["status"]) Зачем обёртка, если API простое. Затем, что у него есть неочевидные места, -на которые мы уже наступили и которые тут обойдены. +собранные на живых серверах и обойдённые тут. -Адрес машины лежит в поле ip, а не в networks, оно всегда пустое. -Снимок нельзя снять с остановленной машины, API сам её останавливает по offline. -Сразу после снимка машина сидит в locked и удаляться отказывается примерно полминуты, -поэтому удаление тут настойчивое, иначе забытая машина будет тикать по деньгам. -Тарифы с видеокартами метод sizes не показывает вовсе, хотя создание с ними работает, -их список лежит в docs_slugs.md. +Адрес сервера лежит в поле ip, а не в networks, оно всегда пустое. +Снимок снимается только с работающего сервера, останавливает его платформа сама по offline. +Сразу после снимка сервер сидит в locked и удаляться отказывается примерно полминуты, +поэтому удаление настойчивое, иначе забытый сервер продолжит тарифицироваться. +Метод sizes возвращает не все линейки тарифов, полный список смотрите в панели управления. """ from __future__ import annotations @@ -50,7 +49,8 @@ class RegCloud: return r.json().get("sizes", []) def gpu_sizes(self) -> list[dict]: - """Тарифы с видеокартой. У них в описании или в названии есть упоминание gpu.""" + """Отбор тарифов с видеокартой, если они вам нужны. + Учтите, что sizes() возвращает не все линейки, полный список в панели управления.""" out = [] for s in self.sizes(): blob = json.dumps(s, ensure_ascii=False).lower()