Клиент обобщён, снята привязка к видеокартам
GPU это лишь один из тарифов, а клиент общего назначения. Примеры переписаны на обычные серверы, добавлен пример с масштабированием под нагрузку. Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu
Этот коммит содержится в:
+75
-46
@@ -2,27 +2,8 @@
|
|||||||
|
|
||||||
Клиент к API облака REG.RU (Рег.облако), точка входа `https://api.cloudvps.reg.ru`.
|
Клиент к API облака REG.RU (Рег.облако), точка входа `https://api.cloudvps.reg.ru`.
|
||||||
|
|
||||||
Само API устроено как у DigitalOcean и особой сложности не представляет. Обёртка нужна
|
Создание и удаление серверов, снимки, SSH-ключи, тарифы, образы, баланс и цены.
|
||||||
из-за неочевидных мест, на которые мы уже наступили в бою, и которые тут обойдены.
|
Само API устроено как у DigitalOcean, сервер там называется reglet.
|
||||||
|
|
||||||
## Что тут учтено
|
|
||||||
|
|
||||||
**Адрес машины лежит в поле `ip`.** Поле `networks`, которое по документации должно
|
|
||||||
его содержать, всегда пустое. Метод `ip_of()` знает про это.
|
|
||||||
|
|
||||||
**Снимок нельзя снять с остановленной машины.** API отвечает `SERVER_IS_STOPPED`.
|
|
||||||
Останавливать её должен он сам, для этого есть параметр `offline`.
|
|
||||||
|
|
||||||
**Сразу после снимка машина сидит в `locked` и удаляться отказывается**, отвечая
|
|
||||||
`RESOURCE_DELETE_ERROR`. Отпускает примерно через полминуты. `destroy()` по умолчанию
|
|
||||||
долбится, пока не поддастся, потому что забытая машина продолжает тикать по деньгам.
|
|
||||||
|
|
||||||
**Тарифы с видеокартами метод `sizes()` не показывает вообще**, хотя создание с ними
|
|
||||||
прекрасно работает. Их список лежит в [docs_slugs.md](docs_slugs.md), там же образы,
|
|
||||||
регионы и правила составления slug'ов.
|
|
||||||
|
|
||||||
**Тарификация идёт от часа.** Двадцать минут работы списываются как полный час,
|
|
||||||
проверено на живой машине. Поднимать машину на короткие задачи невыгодно.
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
@@ -30,9 +11,10 @@
|
|||||||
pip install git+https://git.stackworks.ru/viktor/regcloud-api.git
|
pip install git+https://git.stackworks.ru/viktor/regcloud-api.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Токен берётся в личном кабинете и кладётся в `REGRU_CLOUD_TOKEN`.
|
Токен берётся в личном кабинете, в разделе настроек, и кладётся в `REGRU_CLOUD_TOKEN`.
|
||||||
|
Либо передаётся в конструктор явно.
|
||||||
|
|
||||||
## Как пользоваться
|
## Быстрый старт
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from regcloud_api import RegCloud
|
from regcloud_api import RegCloud
|
||||||
@@ -45,45 +27,92 @@ for s in api.list():
|
|||||||
print(api.balance())
|
print(api.balance())
|
||||||
```
|
```
|
||||||
|
|
||||||
Поднять машину с видеокартой, снять с неё образ и удалить.
|
Поднять сервер и дождаться готовности.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
srv = api.create(size="c8-m32-d128-g1-a4000-gpu",
|
srv = api.create(
|
||||||
image="ubuntu-24-04-amd64-gpu",
|
size="c2-m4-d60-hp", # 2 ядра, 4 ГБ, 60 ГБ, линейка «производительный»
|
||||||
name="worker",
|
image="ubuntu-24-04-amd64",
|
||||||
ssh_keys=["d7:3a:..."],
|
name="web-01",
|
||||||
region="openstack-spb1")
|
region="openstack-msk1",
|
||||||
|
ssh_keys=["d7:3a:b4:..."], # отпечаток ключа из api.ssh_keys()
|
||||||
|
)
|
||||||
|
|
||||||
info = api.wait_active(srv["id"])
|
info = api.wait_active(srv["id"])
|
||||||
print("адрес", RegCloud.ip_of(info))
|
print("адрес:", RegCloud.ip_of(info))
|
||||||
|
|
||||||
api.snapshot(srv["id"], "worker-base", offline=True) # машину остановит сам
|
|
||||||
api.destroy(srv["id"]) # дождётся снятия locked
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Дальше машину можно поднимать уже из снимка, передав его номер вместо имени образа.
|
Снять образ и поднять из него копию.
|
||||||
|
|
||||||
```python
|
```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)` | машины на аккаунте |
|
| `list()`, `get(id)` | серверы на аккаунте |
|
||||||
| `create(...)` | поднять машину, из образа или из снимка |
|
| `create(size, image, name, ssh_keys, region)` | создать сервер, из образа или из снимка |
|
||||||
| `destroy(id)` | удалить, настойчиво |
|
| `destroy(id)` | удалить |
|
||||||
| `stop(id)`, `start(id)` | выключить и включить |
|
| `stop(id)`, `start(id)` | выключить, включить |
|
||||||
| `action(id, type, **kw)` | любое действие из списка API |
|
| `action(id, type, **kw)` | любое действие: reboot, rebuild, resize, clone, restore и прочие |
|
||||||
| `snapshot(id, name)` | снять образ |
|
| `snapshot(id, name, offline)` | снять образ |
|
||||||
| `wait_active(id)`, `wait_action(id)` | дождаться готовности |
|
| `wait_active(id)`, `wait_action(id)` | дождаться готовности сервера или завершения действия |
|
||||||
| `sizes()`, `gpu_sizes()` | тарифы |
|
| `sizes()`, `gpu_sizes()` | тарифы |
|
||||||
| `images()`, `snapshots()` | образы и снимки |
|
| `images()`, `snapshots()` | образы и снимки |
|
||||||
| `ssh_keys()` | ключи доступа |
|
| `ssh_keys()` | ключи доступа |
|
||||||
| `balance()`, `prices()` | деньги |
|
| `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/) — примеры
|
||||||
|
|||||||
@@ -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')} ГБ")
|
||||||
@@ -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("машина удалена, снимок остался")
|
|
||||||
@@ -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()))
|
||||||
@@ -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("сервер удалён, образ остался")
|
||||||
@@ -10,14 +10,13 @@ API устроено как у DigitalOcean, сервер там называе
|
|||||||
print(s["id"], s["name"], s["status"])
|
print(s["id"], s["name"], s["status"])
|
||||||
|
|
||||||
Зачем обёртка, если API простое. Затем, что у него есть неочевидные места,
|
Зачем обёртка, если API простое. Затем, что у него есть неочевидные места,
|
||||||
на которые мы уже наступили и которые тут обойдены.
|
собранные на живых серверах и обойдённые тут.
|
||||||
|
|
||||||
Адрес машины лежит в поле ip, а не в networks, оно всегда пустое.
|
Адрес сервера лежит в поле ip, а не в networks, оно всегда пустое.
|
||||||
Снимок нельзя снять с остановленной машины, API сам её останавливает по offline.
|
Снимок снимается только с работающего сервера, останавливает его платформа сама по offline.
|
||||||
Сразу после снимка машина сидит в locked и удаляться отказывается примерно полминуты,
|
Сразу после снимка сервер сидит в locked и удаляться отказывается примерно полминуты,
|
||||||
поэтому удаление тут настойчивое, иначе забытая машина будет тикать по деньгам.
|
поэтому удаление настойчивое, иначе забытый сервер продолжит тарифицироваться.
|
||||||
Тарифы с видеокартами метод sizes не показывает вовсе, хотя создание с ними работает,
|
Метод sizes возвращает не все линейки тарифов, полный список смотрите в панели управления.
|
||||||
их список лежит в docs_slugs.md.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -50,7 +49,8 @@ class RegCloud:
|
|||||||
return r.json().get("sizes", [])
|
return r.json().get("sizes", [])
|
||||||
|
|
||||||
def gpu_sizes(self) -> list[dict]:
|
def gpu_sizes(self) -> list[dict]:
|
||||||
"""Тарифы с видеокартой. У них в описании или в названии есть упоминание gpu."""
|
"""Отбор тарифов с видеокартой, если они вам нужны.
|
||||||
|
Учтите, что sizes() возвращает не все линейки, полный список в панели управления."""
|
||||||
out = []
|
out = []
|
||||||
for s in self.sizes():
|
for s in self.sizes():
|
||||||
blob = json.dumps(s, ensure_ascii=False).lower()
|
blob = json.dumps(s, ensure_ascii=False).lower()
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user