From bb66bc1681a4ea817f171abcc516cac4972ad958 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:13:57 +0900 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D0=BA?= =?UTF-8?q?=20API=20=D0=BE=D0=B1=D0=BB=D0=B0=D0=BA=D0=B0=20REG.RU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обёртка над api.cloudvps.reg.ru. Само API несложное, но у него есть неочевидные места, на которые мы наступили на живых машинах, и которые тут обойдены. - адрес машины лежит в поле ip, networks всегда пустое - снимок нельзя снять с остановленной машины, API останавливает её сам - после снимка машина сидит в locked и не удаляется, удаление настойчивое - тарифы с картами метод sizes не показывает, их список в docs_slugs.md - тарификация идёт от часа, короткие запуски невыгодны Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu --- .gitignore | 8 ++ README.md | 89 +++++++++++++++ docs_slugs.md | 61 ++++++++++ examples/gpu_lifecycle.py | 28 +++++ openapi_v1.json | 1 + pyproject.toml | 14 +++ regcloud_api/__init__.py | 5 + regcloud_api/client.py | 227 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 433 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs_slugs.md create mode 100644 examples/gpu_lifecycle.py create mode 100644 openapi_v1.json create mode 100644 pyproject.toml create mode 100644 regcloud_api/__init__.py create mode 100644 regcloud_api/client.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6db26d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +.venv/ +dist/ +build/ +*.egg-info/ +.env +regcloud_state.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8e1ca9 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# regcloud-api + +Клиент к 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'ов. + +**Тарификация идёт от часа.** Двадцать минут работы списываются как полный час, +проверено на живой машине. Поднимать машину на короткие задачи невыгодно. + +## Установка + +```bash +pip install git+https://git.stackworks.ru/viktor/regcloud-api.git +``` + +Токен берётся в личном кабинете и кладётся в `REGRU_CLOUD_TOKEN`. + +## Как пользоваться + +```python +from regcloud_api import RegCloud + +api = RegCloud() + +for s in api.list(): + print(s["id"], s["name"], s["status"], RegCloud.ip_of(s)) + +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") + +info = api.wait_active(srv["id"]) +print("адрес", RegCloud.ip_of(info)) + +api.snapshot(srv["id"], "worker-base", offline=True) # машину остановит сам +api.destroy(srv["id"]) # дождётся снятия locked +``` + +Дальше машину можно поднимать уже из снимка, передав его номер вместо имени образа. + +```python +api.create(size="c8-m32-d128-g1-a4000-gpu", image=7887485, region="openstack-spb1") +``` + +## Что умеет + +| Метод | Что делает | +|---|---| +| `list()`, `get(id)` | машины на аккаунте | +| `create(...)` | поднять машину, из образа или из снимка | +| `destroy(id)` | удалить, настойчиво | +| `stop(id)`, `start(id)` | выключить и включить | +| `action(id, type, **kw)` | любое действие из списка API | +| `snapshot(id, name)` | снять образ | +| `wait_active(id)`, `wait_action(id)` | дождаться готовности | +| `sizes()`, `gpu_sizes()` | тарифы | +| `images()`, `snapshots()` | образы и снимки | +| `ssh_keys()` | ключи доступа | +| `balance()`, `prices()` | деньги | + +## Полезное рядом + +- [docs_slugs.md](docs_slugs.md) — тарифы с картами, образы, регионы, правила slug'ов +- [openapi_v1.json](openapi_v1.json) — спецификация API, снятая с их сервера diff --git a/docs_slugs.md b/docs_slugs.md new file mode 100644 index 0000000..0c1fb7f --- /dev/null +++ b/docs_slugs.md @@ -0,0 +1,61 @@ +# Справочник slug'ов Рег.облака + +Собрано 23.08.2026. Важная особенность: `GET /v1/sizes` тарифы с картами **не показывает**, +хотя создание с ними работает. Проверено косвенно, API отвечает `WRONG_SIZE_SLUG` на выдуманный +тариф и пропускает настоящий. Так что ориентироваться надо на этот список, а не на выдачу API. + +## Тарифы с видеокартой + +| slug | Карта | Ядра | RAM | Диск | ₽/час | +|---|---|---|---|---|---| +| `c8-m32-d128-g1-a4000-gpu` | A4000 16 ГБ | 8 | 32 ГБ | 128 ГБ | 37.80 | +| `c16-m64-d256-g1-a4000-gpu` | A4000 16 ГБ | 16 | 64 ГБ | 256 ГБ | 51.87 | +| `c16-m64-d256-g1-a5000-gpu` | A5000 24 ГБ | 16 | 64 ГБ | 256 ГБ | 57.00 | +| `c24-m128-d512-g1-a5000-gpu` | A5000 24 ГБ | 24 | 128 ГБ | 512 ГБ | 82.79 | +| `c24-m128-d512-g2-a5000-gpu` | 2x A5000 | 24 | 128 ГБ | 512 ГБ | 111.65 | +| `c32-m256-d1024-g1-a5000-gpu` | A5000 24 ГБ | 32 | 256 ГБ | 1 ТБ | 132.01 | +| `c48-m384-d1024-g2-a5000-gpu` | 2x A5000 | 48 | 384 ГБ | 1 ТБ | 203.30 | +| `c16-m128-d512-g1-a100-gpu` | A100 80 ГБ | 16 | 128 ГБ | 512 ГБ | 222.14 | +| `c32-m256-d1024-g1-a100-gpu` | A100 80 ГБ | 32 | 256 ГБ | 1 ТБ | 273.72 | +| `c64-m512-d1024-g2-a100-gpu` | 2x A100 | 64 | 512 ГБ | 1 ТБ | 529.14 | +| `c24-m96-d384-g1-rtx6000-gpu` | RTX 6000 Blackwell | 24 | 96 ГБ | 384 ГБ | 205.07 | +| `c32-m128-d512-g1-rtx6000-gpu` | RTX 6000 Blackwell | 32 | 128 ГБ | 512 ГБ | 223.43 | +| `c64-m192-d1024-g1-rtx6000-gpu` | RTX 6000 Blackwell | 64 | 192 ГБ | 1 ТБ | 271.15 | +| `c32-m128-d1024-g2-rtx6000-gpu` | 2x RTX 6000 | 32 | 128 ГБ | 1 ТБ | 382.58 | + +Свои конфигурации для карт собирать нельзя, только готовые slug'и. + +## Образы под карту + +| slug | Что внутри | +|---|---| +| `ubuntu-24-04-amd64-gpu` | Ubuntu 24.04 с драйверами NVIDIA | +| `ubuntu-22-04-amd64-gpu` | Ubuntu 22.04 с драйверами | +| `ubuntu-26-04-amd64-gpu` | Ubuntu 26.04 с драйверами | +| `ubuntu-24-04-amd64-vllm` | готовый инференс vLLM | +| `ubuntu-24-04-amd64-ollama` | готовый ИИ-ассистент на Ollama | +| `ubuntu-24-04-amd64-jupyterhub` | JupyterHub | + +## Регионы + +| slug | Где | +|---|---| +| `openstack-spb1` | Санкт-Петербург, **тут стоят карты** | +| `openstack-msk1` | Москва | +| `openstack-msk2` | Москва-2 | +| `openstack-msk3` | Москва-3 | +| `openstack-sam1` | Самара | +| `openstack-fz1` | Москва ФЗ-152 | +| `msk1` | Москва, архивные тарифы | + +## Обычные тарифы, шаблон + +`c<ядра>-m<память>-d<диск>-<линейка>`, например `c4-m8-d120-hp`. + +Линейки: `std` стандартный, `stdp` стандартный плюс, `hp` производительный, +`hps` производительный ФЗ-152, `ddc` выделенный CPU, `high` высокочастотный. + +## Terraform + +Провайдер `tf.reg.cloud/regru/regcloud`, точка входа `https://api.cloudvps.reg.ru`, +токен тот же, что у API. Умеет серверы, серверы с GPU, снимки и SSH-ключи. diff --git a/examples/gpu_lifecycle.py b/examples/gpu_lifecycle.py new file mode 100644 index 0000000..9098034 --- /dev/null +++ b/examples/gpu_lifecycle.py @@ -0,0 +1,28 @@ +"""Полный цикл работы с машиной под видеокарту. + +Поднимаем из готового снимка, работаем, снимаем новый образ, удаляем. +Именно так экономятся деньги: платим за часы счёта, а не за месяц простоя. +""" +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/openapi_v1.json b/openapi_v1.json new file mode 100644 index 0000000..630429b --- /dev/null +++ b/openapi_v1.json @@ -0,0 +1 @@ +{"components":{"parameters":{"action_id":{"description":"Action unique identificator","in":"path","name":"action_id","required":true,"schema":{"minimum":1,"type":"integer"}},"group":{"description":"Image grouping","in":"query","name":"group","required":false,"schema":{"pattern":"^[a-zA-Z_0-9\\-]+$","type":"string"}},"image_id":{"description":"Image identificator","in":"path","name":"image_id","required":true,"schema":{"minLength":1,"type":"integer"}},"ip":{"description":"Ip address.","in":"path","name":"ip","required":true,"schema":{"pattern":"^(?:([0-9]{1,3}\\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:\\.]+)$","type":"string"}},"k8s_cluster_id":{"description":"K8s cluster unique identificator","in":"path","name":"k8s_cluster_id","required":true,"schema":{"type":"integer"}},"key_id":{"description":"SSH-key identificator","in":"path","name":"key_id","required":true,"schema":{"minLength":1,"pattern":"^[0-9A-Za-z:]+$","type":"string"}},"limit":{"description":"limit of rows","in":"query","name":"limit","schema":{"maximum":50,"minimum":1,"type":"integer"}},"notify_id":{"description":"Notify identificator","in":"path","name":"notify_id","required":true,"schema":{"type":"string"}},"offset":{"description":"offset of rows","in":"query","name":"offset","schema":{"minimum":0,"type":"integer"}},"private":{"description":"Image private","in":"query","name":"private","required":false,"schema":{"type":"boolean"}},"region":{"description":"Region slug","in":"query","name":"region","required":false,"schema":{"enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3"],"type":"string"}},"reglet_id":{"description":"Server unique identificator","in":"query","name":"reglet_id","schema":{"minimum":1,"type":"integer"}},"resource_id":{"description":"Resource unique identificator","in":"path","name":"resource_id","required":true,"schema":{"type":"integer"}},"resource_name":{"description":"Name of","in":"query","name":"resource_name","schema":{"$ref":"#/components/schemas/NameString"}},"settings_key":{"description":"Settings key.","in":"path","name":"settings_key","required":true,"schema":{"enum":["balance_limit_notify"],"type":"string"}},"snapshot_id":{"description":"Snapshot id","in":"path","name":"snapshot_id","required":true,"schema":{"minLength":1,"pattern":"^[a-zA-Z_0-9\\-]+$","type":"string"}},"type":{"description":"Image type","in":"query","name":"type","required":false,"schema":{"minLength":1,"pattern":"^[0-9A-Za-z]+$","type":"string"}},"vpcs_id":{"description":"Vpcs unique identificator","in":"path","name":"vpcs_id","required":true,"schema":{"type":"integer"}}},"responses":{"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The specified resource was not found"},"NotImplemented":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not implemented error"},"UnauthorizedError":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Access token is missing or invalid"},"ValidationError":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"}},"schemas":{"Action":{"nullable":true,"properties":{"completed_at":{"nullable":true,"type":"string"},"created_at":{"description":"Action create date","type":"string"},"id":{"description":"Action identificator","type":"integer"},"region_slug":{"description":"Region for this action","enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3","service","srs-mojo-api","billing-api"],"type":"string"},"resource_id":{"description":"Identificator resource for this action","type":"integer"},"resource_type":{"description":"Type resource for this action","type":"string"},"status":{"description":"Action status","enum":["wait","new","in-progress","completed","errored"],"type":"string"},"type":{"description":"Action type","type":"string"}},"required":["completed_at","created_at","id","region_slug","resource_id","resource_type","status","type"],"type":"object"},"ActionByStr":{"nullable":true,"properties":{"completed_at":{"nullable":true,"type":"string"},"created_at":{"description":"Action create date","type":"string"},"id":{"description":"Action identificator","type":"string"},"region_slug":{"description":"Region for this action","enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3","service","srs-mojo-api","billing-api"],"type":"string"},"resource_id":{"description":"Identificator resource for this action","type":"integer"},"resource_type":{"description":"Type resource for this action","type":"string"},"status":{"description":"Action status","enum":["wait","new","in-progress","in_progress","failed","completed","errored"],"type":"string"},"type":{"description":"Action type","type":"string"}},"required":["completed_at","created_at","id","region_slug","resource_id","resource_type","status","type"],"type":"object"},"ActionLink":{"properties":{"action_id":{"description":"Action identificator","example":123556,"readOnly":true,"type":"integer"}},"required":["action_id"],"type":"object"},"AddKey":{"properties":{"name":{"$ref":"#/components/schemas/NameString"},"public_key":{"description":"public_key","example":"ssh-rsa AAAAB3(...)dQ7Ay9 root@DESKTOP-F24V59S","pattern":"^(ssh-(rsa|dss|ed25519)|(ecdsa-sha2-\\w+)) AAAA[0-9A-Za-z+\\/]+[=]{0,3}(\\s+[-a-zA-Z0-9@\\._]+)?$","type":"string"}},"required":["name","public_key"],"type":"object"},"BalanceData":{"description":"Full scope of billing information","properties":{"balance":{"example":0,"type":"number"},"bonus_balance":{"example":0,"type":"number"},"days_left":{"example":0,"type":"integer"},"detalization":{"default":[],"description":"Array with resource detalization","items":{"$ref":"#/components/schemas/BilledResources"},"type":"array"},"hourly_cost":{"example":0,"type":"number"},"hours_left":{"example":0,"type":"integer"},"monthly_cost":{"example":0,"type":"number"},"state":{"enum":["active","suspended","stopped"],"example":"active","type":"string"}},"required":["balance","bonus_balance","days_left","hours_left","hourly_cost","monthly_cost","detalization"],"type":"object"},"BilledResources":{"properties":{"linked":{"items":{"$ref":"#/components/schemas/BilledResources"},"type":"array"},"name":{"example":"Best server ever","type":"string"},"plan":{"example":"cloud-121412","type":"string"},"price":{"example":"144.19","type":"string"},"price_month":{"example":"15000","type":"string"},"resource_id":{"example":123,"type":"integer"},"state":{"enum":["active","suspended","stopped"],"example":"active","type":"string"},"type":{"example":"reglet","type":"string"}},"required":["plan","type","price","price_month"]},"Error":{"properties":{"code":{"example":"Error code","type":"string"},"message":{"example":"Error message","type":"string"}},"required":["code","message"],"type":"object"},"Feedback":{"properties":{"message":{"description":"Text message","pattern":"^[0-9a-zA-Z\u0430-\u044f\u0410-\u042f\u0451\u0401% _!*=;:#\u2116\u2013\\/\\\\\\\\\\{\\}&\\[\\]<>+\\$~`?|\u00ab\u00bb()\\@^\\\"\\',.\\r\\n\\t-\u20bd]+$","type":"string"},"user_email":{"description":"User email address","pattern":"^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$","type":"string"}},"required":["user_email","message"],"type":"object"},"HistoryItem":{"properties":{"action":{"description":"User or system action","example":"SERVER_CREATE","type":"string"},"data":{"description":"Additional data for action","example":{"backups":0,"id":123,"image":124,"name":"White Zincum","region_slug":"msk1","size":"cloud-0","ssh_keys":[]},"type":"object"},"insert_time":{"example":"2020-09-10T15:29:59+00:00","type":"string"},"resource_id":{"description":"Operation for resource_id","example":123,"nullable":true,"type":"integer"}},"required":["action","data","insert_time","resource_id"],"type":"object"},"IP":{"properties":{"ipv4_count":{"description":"Number of IPv4 addresses.","type":"integer"},"ipv6_count":{"description":"Number of IPv6 addresses.","type":"integer"},"reglet_id":{"description":"Server unique identificator.","type":"integer"}},"type":"object"},"IPs":{"properties":{"created_at":{"description":"Create ip time.","pattern":"^[0-9\\-\\s\\:]+$","readOnly":true,"type":"string"},"id":{"description":"Ip identificator.","readOnly":true,"type":"integer"},"ip":{"description":"Ip address.","pattern":"^(?:([0-9]{1,3}\\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:\\.]+)$","readOnly":true,"type":"string"},"ptr":{"description":"Ptr address","maxLength":63,"minLength":6,"pattern":"^(?!..--)([-a-zA-Z\u0430-\u044f\u0451\u0410-\u042f\u04010-9]+)(\\.(?!..--)[-a-zA-Z\u0430-\u044f\u0451\u0410-\u042f\u04010-9]+)+\\.?$","type":"string"},"region_slug":{"description":"Region for this ip.","enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3"],"readOnly":true,"type":"string"},"reglet_id":{"description":"Server unique identificator.","readOnly":true,"type":"integer"},"status":{"description":"Resource status.","enum":["new","active","suspended"],"readOnly":true,"type":"string"},"type":{"description":"Ip type.","enum":["ipv4","ipv6"],"readOnly":true,"type":"string"}},"type":"object"},"Image":{"properties":{"created_at":{"description":"Create image time","pattern":"^[0-9\\-\\s\\:]+$","readOnly":true,"type":"string"},"distribution":{"description":"Distribution name","readOnly":true,"type":"string"},"id":{"description":"Image identificator","readOnly":true,"type":"integer"},"isp_license_size":{"description":"ISPManager license size if need to order","enum":["isp_lite5","isp_lite6","isp_pro6","isp_host6"],"example":"isp_lite6","nullable":true,"readOnly":true,"type":"string"},"min_disk_size":{"description":"Minimum disk size for installing the image","pattern":"^[0-9\\.]+$","readOnly":true,"type":"string"},"name":{"$ref":"#/components/schemas/NameString"},"private":{"description":"Private image","readOnly":true,"type":"boolean"},"region_slug":{"description":"Region for this image","enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3"],"readOnly":true,"type":"string"},"size_gigabytes":{"description":"Gigabyte image size","pattern":"^[0-9\\.]+$","readOnly":true,"type":"string"},"slug":{"description":"Image slug","pattern":"^[-0-9a-zA-Z\\.]{1,50}$","readOnly":true,"type":"string"},"type":{"description":"Image type","enum":["distribution","application","snapshot","backup","restore","clone"],"readOnly":true,"type":"string"},"version":{"items":{"$ref":"#/components/schemas/Image"},"readOnly":true,"type":"array"}},"type":"object"},"Key":{"properties":{"fingerprint":{"description":"Fingerprint for ssh key","readOnly":true,"type":"string"},"id":{"description":"Key identificator","readOnly":true,"type":"integer"},"name":{"$ref":"#/components/schemas/NameString"},"public_key":{"description":"public_key","readOnly":true,"type":"string"},"service_id":{"description":"service_id environment to which ssh-key belongs","readOnly":true,"type":"integer"}},"required":["name"],"type":"object"},"NameString":{"description":"Name for render into web-interface","example":"Some name","pattern":"^[-0-9a-zA-Z\u0430-\u044f\u0410-\u042f\u0451\u0401_:\u201c\u201d\u2018\u2019\u201e\u201c\u00ab,\u00bb!\"'\\s.#@`\\[\\]?\u2116=*^{}|+\u00ae()]{1,60}$","type":"string"},"Notify":{"properties":{"action":{"description":"action of notify","type":"string"},"created_at":{"description":"Notify create date","type":"string"},"id":{"description":"Notify identificator","example":1,"type":"integer"},"params":{"description":"params of action","example":{},"nullable":true,"type":"object"},"service_id":{"description":"User identificator","example":1213123,"type":"integer"}},"required":["id","service_id","created_at","action","params"],"type":"object"},"PriceList":{"properties":{"prices":{"items":{"$ref":"#/components/schemas/ResourcePrice"},"type":"array"}},"type":"object"},"RefillHistory":{"properties":{"billing_history":{"items":{"$ref":"#/components/schemas/RefillHistoryItem"},"type":"array"}},"type":"object"},"RefillHistoryItem":{"properties":{"amount":{"description":"Amount of refill","example":"100.00","type":"string"},"date":{"example":"2018-06-18 11:22:09","type":"string"},"description_params":{"example":{},"nullable":true,"type":"object"},"type":{"enum":["refill_bonus","refill"],"example":"refill","type":"string"}},"type":"object"},"Reglet":{"properties":{"backups_enabled":{"description":"Enabled/Disabled backups","example":true,"type":"boolean"},"created_at":{"description":"Datetime, when this reglet has been created","example":"2020-03-29 23:00:02","type":"string"},"disk":{"description":"Disk size","example":80,"type":"integer"},"hostname":{"description":"Hostname","example":"reg.ru","nullable":true,"type":"string"},"id":{"description":"Resource id","example":546,"type":"integer"},"image":{"$ref":"#/components/schemas/Image"},"image_id":{"description":"image_id","example":123,"type":"integer"},"ip":{"description":"IPv4","example":"127.0.0.3","nullable":true,"type":"string"},"ipv6":{"description":"IPv6","example":"FE80::C001:37FF:FE6C:0","nullable":true,"type":"string"},"last_backup_date":{"description":"Last backup date","example":"2020-03-29 23:00:02","nullable":true,"type":"string"},"locked":{"description":"Lock status","example":0,"type":"integer"},"memory":{"description":"Memory size","example":4096,"type":"integer"},"name":{"description":"Reglet name","example":"Name for Reglet","type":"string"},"ptr":{"description":"PTR record this reglet","example":"192-165-21-58.cloudvps.regruhosting.ru","nullable":true,"type":"string"},"region_slug":{"description":"Region for this reglet","example":"msk1","type":"string"},"service_id":{"description":"Service owner this reglet","example":7777777,"type":"integer"},"size":{"$ref":"#/components/schemas/Size"},"size_slug":{"description":"Size slug","example":"cloud-4","type":"string"},"status":{"description":"Reglet status. Only active statuses","enum":["new","active","ordered","off","suspended","archive"],"type":"string"},"sub_status":{"description":"Reglet sub status","example":"by_user","nullable":true,"type":"string"},"vcpus":{"description":"vCPU count","example":2,"type":"integer"},"vpcs":{"description":"Private server nets","example":[1,2,3],"items":{"type":"integer"},"type":"array"}},"required":["size","size_slug","image","image_id","status","ptr","ip","ipv6","backups_enabled","created_at","disk","memory","name","region_slug","service_id"],"type":"object"},"RegletAction":{"properties":{"image":{"anyOf":[{"type":"string"},{"type":"integer"}],"description":"Image id or slug.","example":"ubuntu-16-04-amd64"},"isp_license_size":{"description":"Change ISPManager license size","enum":["isp_lite6","isp_pro6","isp_host6"],"example":"isp_lite6","type":"string"},"name":{"$ref":"#/components/schemas/NameString"},"offline":{"description":"Param of creating snapshot or clone.","example":false,"oneOf":[{"type":"boolean"},{"maximum":1,"minimum":0,"type":"integer"}]},"size":{"description":"Size slug of server.","example":"cloud-121412","type":"string"},"ssh_keys":{"default":[],"description":"List of ssh keys ids or fingerprint","example":["50:8c:26:58:b0:3c:96:24:14:e7:39:cb:2e:d8:5e:cd",123],"items":{"oneOf":[{"pattern":"^\\d+|[a-z\\d:]{47}$","type":"string"},{"minimum":1,"type":"integer"}]},"type":"array"},"type":{"description":"Action type.","enum":["start","stop","reboot","rebuild","password_reset","resize","generate_vnc_link","snapshot","enable_backups","disable_backups","restore","clone","resize_isp_license"],"type":"string"}},"required":["type"],"type":"object"},"RegletCreate":{"properties":{"backups":{"description":"Enable or disable backups for server","example":false,"oneOf":[{"type":"boolean"},{"maximum":1,"minimum":0,"type":"integer"}]},"floating_ip":{"default":true,"description":"Enable or disable floating_ip for server","example":false,"type":"boolean"},"image":{"anyOf":[{"example":"ubuntu-20-04-amd64","minLength":1,"pattern":"^[a-zA-Z_0-9\\-]+$","type":"string"},{"example":10001,"type":"integer"}],"example":"ubuntu-20-04-amd64"},"isp_license_size":{"description":"Order ISPManager license with this size","enum":["isp_lite6","isp_pro6","isp_host6"],"example":"isp_lite6","type":"string"},"name":{"$ref":"#/components/schemas/NameString"},"promocode":{"description":"Promo code","example":"some_promo_code","type":"string"},"region_slug":{"description":"DC region of server","enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3"],"example":"msk1","type":"string"},"size":{"description":"Size slug","example":"cloud-2","type":"string"},"ssh_keys":{"default":[],"description":"List of ssh keys ids","example":["50:8c:26:58:b0:3c:96:24:14:e7:39:cb:2e:d8:5e:cd",123],"items":{"oneOf":[{"pattern":"^\\d+|[a-z\\d:]{47}$","type":"string"},{"minimum":1,"type":"integer"}]},"type":"array"}},"required":["size","image"],"type":"object"},"Removed":{"description":"Scope of removed servers information.","items":{"properties":{"image_id":{"type":"integer"},"name":{"example":"clone","type":"string"},"reglet_id":{"type":"integer"},"remove_date":{"example":"2018-06-18 11:22:09","type":"string"},"remove_reason":{"example":"by_user","type":"string"},"size":{"$ref":"#/components/schemas/Size"}},"type":"object"},"type":"array"},"Resource":{"properties":{"resource_id":{"description":"Id","example":312,"type":"integer"}},"required":["resource_id"],"type":"object"},"ResourcePrice":{"properties":{"plan":{"description":"tariff plan","example":"cloud-5","type":"string"},"price":{"description":"price of hour","example":"120.40","type":"string"},"price_month":{"description":"price of price_month","example":4000,"type":"number"},"type":{"description":"type of resource","example":"reglet","type":"string"},"unit":{"enum":["minute","hour","month"],"example":"hour","type":"string"}},"type":"object"},"Size":{"properties":{"disk":{"type":"integer"},"id":{"type":"integer"},"memory":{"example":512,"type":"integer"},"name":{"example":"cloud-1","type":"string"},"price":{"example":"0.00","type":"string"},"price_month":{"example":550,"type":"integer"},"regions":{"default":[],"items":{"type":"string"},"type":"array"},"slug":{"example":"cloud-2b","type":"string"},"vcpus":{"example":1,"type":"integer"},"weight":{"example":31,"type":"integer"}},"type":"object"},"Vpcs":{"properties":{"members":{"default":[],"items":{"description":"Identificator resource","example":1111,"type":"integer"},"readOnly":true,"type":"array"},"name":{"$ref":"#/components/schemas/NameString"},"region_slug":{"default":"msk1","description":"Region of vpcs","enum":["msk1","openstack-msk1","openstack-spb1","openstack-sam1","openstack-fz1","openstack-msk2","openstack-msk3"],"example":"msk1","readOnly":true,"type":"string"},"resource_id":{"description":"Unique identifier","example":"123","readOnly":true,"type":"integer"}},"required":["name"],"type":"object"}},"securitySchemes":{"bearerAuth":{"scheme":"bearer","type":"http","x-bearerInfoFunc":"src.apps.api.security.auth"},"jwtCookieAuth":{"in":"cookie","name":"JWT","type":"apiKey","x-apikeyInfoFunc":"src.apps.api.security.panel_auth"}}},"info":{"title":"api cloudvps v1","version":"1.0"},"openapi":"3.0.3","paths":{"/account/feedback":{"post":{"operationId":"src.controller.v1.feedback.add","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feedback"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"result":{"type":"string"}}}}},"description":"Feedback sending status"},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Add feedback","tags":["Feedback"]}},"/account/keys":{"get":{"operationId":"src.controller.v1.keys.get_list","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"ssh_key":{"items":{"$ref":"#/components/schemas/Key"},"type":"array"}}}}},"description":"Return keys."},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get keys","tags":["Keys"]},"post":{"operationId":"src.controller.v1.keys.add","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddKey"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"ssh_keys":{"$ref":"#/components/schemas/Key"}}}}},"description":"Return ssh key."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Add keys","tags":["Keys"]}},"/account/keys/{key_id}":{"delete":{"operationId":"src.controller.v1.keys.delete","parameters":[{"$ref":"#/components/parameters/key_id"}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Return delete ssh key."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Delete ssh-key by id","tags":["Keys"]},"put":{"operationId":"src.controller.v1.keys.update","parameters":[{"$ref":"#/components/parameters/key_id"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Key"}}},"description":"Body with name field"},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"ssh_keys":{"$ref":"#/components/schemas/Key"}}}}},"description":"Renamed ssh key."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Update name ssh-key","tags":["Keys"]}},"/account/settings/{settings_key}":{"get":{"operationId":"src.controller.v1.environment.get_settings_key","parameters":[{"$ref":"#/components/parameters/settings_key"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"balance_limit_notify":{"example":30,"type":"integer"}}}}},"description":"Return environment params."},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get environment settings params.","tags":["Environment"]},"put":{"operationId":"src.controller.v1.environment.update_settings_key","parameters":[{"$ref":"#/components/parameters/settings_key"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"value":{"example":50,"maximum":1000000,"minimum":50,"type":"integer"}},"required":["value"]}}},"description":"Body with setting environment"},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"result":{"type":"string"}}}}},"description":"Return status setting update."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Update settings with key.","tags":["Environment"]}},"/actions/{action_id}":{"get":{"operationId":"src.controller.v1.action.get","parameters":[{"description":"Action unique identificator","in":"path","name":"action_id","required":true,"schema":{"pattern":"^(chain_\\d+)|(\\d+)$","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"action":{"oneOf":[{"$ref":"#/components/schemas/Action"},{"$ref":"#/components/schemas/ActionByStr"}]}},"type":"object"}}},"description":"Return action."},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get single action","tags":["Action"]}},"/balance_data":{"get":{"operationId":"src.controller.v1.billing.get_balance_data","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"balance_data":{"$ref":"#/components/schemas/BalanceData"}}}}},"description":"Account balance detalization"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get account balance info","tags":["Billing"]}},"/billing_history":{"get":{"operationId":"src.controller.v1.billing.get_history","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefillHistory"}}},"description":"get list of refills"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get refill history","tags":["Billing"]}},"/feedback":{"post":{"operationId":"src.controller.v1.feedback.hook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feedback"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"result":{"type":"string"}}}}},"description":"Feedback sending status"},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Add feedback hook","tags":["Feedback"]}},"/history":{"get":{"operationId":"src.controller.v1.history.get_history","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"history":{"items":{"$ref":"#/components/schemas/HistoryItem"},"type":"array"}}}}},"description":"List of history operations"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get history of operations","tags":["History"]}},"/images":{"get":{"operationId":"src.controller.v1.images.get_list","parameters":[{"$ref":"#/components/parameters/type"},{"$ref":"#/components/parameters/private"},{"$ref":"#/components/parameters/reglet_id"},{"$ref":"#/components/parameters/group"},{"$ref":"#/components/parameters/region"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"images":{"items":{"$ref":"#/components/schemas/Image"},"type":"array"}}}}},"description":"Return images."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]},{}],"summary":"Get images list","tags":["Images"]}},"/ips":{"get":{"operationId":"src.controller.v1.ips.get_list","parameters":[{"$ref":"#/components/parameters/reglet_id"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"snapshots":{"items":{"$ref":"#/components/schemas/IPs"},"type":"array"}}}}},"description":"Return IPs."},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get IPs list.","tags":["IPs"]},"post":{"operationId":"src.controller.v1.ips.create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IP"}}},"description":"Ips object create"},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"ips":{"type":"object"},"links":{"properties":{"actions":{"items":{"$ref":"#/components/schemas/ActionByStr"},"type":"array"}}}}}}},"description":"Ips created."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Create ip","tags":["IPs"]}},"/ips/{ip}":{"delete":{"operationId":"src.controller.v1.ips.delete","parameters":[{"$ref":"#/components/parameters/ip"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"links":{"properties":{"actions":{"items":{"$ref":"#/components/schemas/ActionByStr"},"type":"array"}}}}}}},"description":"Ip delete."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Delete ip address.","tags":["IPs"]},"put":{"description":"Change ptr","operationId":"src.controller.v1.ips.update","parameters":[{"$ref":"#/components/parameters/ip"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IPs"}}},"description":"Body with ip field"},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"ip":{"properties":{"ptr":{"type":"string"}}}}}}},"description":"Return a renamed ptr"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Rename ptr","tags":["IPs"]}},"/k8s_clusters/{k8s_cluster_id}/get_kubeconfig":{"get":{"operationId":"src.controller.v1.k8s_clusters.get_kubeconfig","parameters":[{"$ref":"#/components/parameters/k8s_cluster_id"}],"responses":{"200":{"content":{"text/plain":{"schema":{"type":"object"}}},"description":"Successful response."},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"jwtCookieAuth":[]}],"summary":"Return kubeconfig","tags":["K8s"]}},"/prices":{"get":{"operationId":"src.controller.v1.billing.get_prices","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceList"}}},"description":"get list of prices"}},"summary":"Get prices","tags":["Billing"]}},"/random_reglet_name":{"get":{"operationId":"src.controller.v1.common.random_reglet_name","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"reglet_name":{"example":"Random reglet name","type":"string"}},"type":"object"}}},"description":"Successful response."}},"summary":"Return random reglet name.","tags":["Common"]}},"/reglets":{"get":{"operationId":"src.controller.v1.reglets.get_servers_list","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"links":{"properties":{"actions":{"description":"Active actions to this service_id","items":{"oneOf":[{"$ref":"#/components/schemas/Action"},{"$ref":"#/components/schemas/ActionByStr"}]},"type":"array"}},"type":"object"},"reglet":{"items":{"$ref":"#/components/schemas/Reglet"},"type":"array"}},"type":"object"}}},"description":"Successful response."},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get servers list.","tags":["Reglets"]},"post":{"operationId":"src.controller.v1.reglets.create_server","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegletCreate"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"links":{"properties":{"actions":{"description":"Active action to this server create","items":{"oneOf":[{"$ref":"#/components/schemas/Action"},{"$ref":"#/components/schemas/ActionByStr"}]},"type":"array"}},"type":"object"},"reglet":{"$ref":"#/components/schemas/Reglet"}},"type":"object"}}},"description":"Successful created server."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Create server.","tags":["Reglets"]}},"/reglets/{resource_id}":{"delete":{"operationId":"src.controller.v1.reglets.operations.remove_server.remove_server","parameters":[{"$ref":"#/components/parameters/resource_id"}],"responses":{"204":{"description":"Successful response."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Delete reglet","tags":["Reglets"]},"get":{"operationId":"src.controller.v1.reglets.get_server_info","parameters":[{"$ref":"#/components/parameters/resource_id"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"reglet":{"allOf":[{"$ref":"#/components/schemas/Reglet"},{"properties":{"disk_usage":{"description":"Used disk space","example":1.4,"type":"number"}},"type":"object"}]}},"type":"object"}}},"description":"Successful response with server info."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get server info","tags":["Reglets"]},"put":{"operationId":"src.controller.v1.reglets.rename_server","parameters":[{"$ref":"#/components/parameters/resource_id"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"$ref":"#/components/schemas/NameString"}},"required":["name"]}}},"description":"Body with reglet rename."},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"links":{"properties":{"actions":{"description":"Active action to this server create","items":{"$ref":"#/components/schemas/Action"},"type":"array"}},"type":"object"},"reglet":{"$ref":"#/components/schemas/Reglet"}},"type":"object"}}},"description":"Return rename reglets settings."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Update reglet name.","tags":["Reglets"]}},"/reglets/{resource_id}/actions":{"post":{"operationId":"src.controller.v1.reglets.operations.process_operation","parameters":[{"$ref":"#/components/parameters/resource_id"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegletAction"}}},"description":"reglet action object"},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Action"}}},"description":"Success action created"},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Reglet operations","tags":["Reglets"]}},"/reglets_for_snapshot/{snapshot_id}":{"get":{"operationId":"src.controller.v1.common.get_reglets_for_snapshot","parameters":[{"$ref":"#/components/parameters/snapshot_id"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"reglets":{"items":{"description":"Identificator reglets.","example":1111,"type":"integer"},"type":"array"}}}}},"description":"Reglets id list."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get reglets_id for snapshot.","tags":["Common"]}},"/removed_servers":{"get":{"operationId":"src.controller.v1.removed_servers.get_list","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Removed"}}},"description":"Removed servers list."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get removed servers list.","tags":["Removed"]}},"/sizes":{"get":{"operationId":"src.controller.v1.common.sizes","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"sizes":{"description":"scope of sizes","items":{"$ref":"#/components/schemas/Size"},"type":"array"}},"type":"object"}}},"description":"Successful response."}},"summary":"Return available sizes","tags":["Common"]}},"/snapshots":{"get":{"operationId":"src.controller.v1.snapshots.get_list","parameters":[{"$ref":"#/components/parameters/region"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"snapshots":{"items":{"$ref":"#/components/schemas/Image"},"type":"array"}}}}},"description":"Return snapshots."},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get snapshots list.","tags":["Snapshots"]}},"/snapshots/{image_id}":{"delete":{"operationId":"src.controller.v1.snapshots.delete","parameters":[{"$ref":"#/components/parameters/image_id"}],"responses":{"204":{"description":"Deleted snapshot."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Delete snapshot action","tags":["Snapshots"]}},"/vpcs":{"get":{"operationId":"src.controller.v1.vpcs.get_list","responses":{"200":{"content":{"application/json":{"schema":{"description":"List of vpcs","items":{"$ref":"#/components/schemas/Vpcs"},"type":"array"}}},"description":"Successful response"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Get vpcs","tags":["Vpcs"]},"post":{"operationId":"src.controller.v1.vpcs.create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vpcs"}}},"description":"vpcs object create"},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vpcs"}}},"description":"vpcs created"},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}},"security":[{"bearerAuth":[]}],"summary":"Create vpcs","tags":["Vpcs"]}},"/vpcs/{vpcs_id}":{"delete":{"operationId":"src.controller.v1.vpcs.delete","parameters":[{"$ref":"#/components/parameters/vpcs_id"}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}},"security":[{"bearerAuth":[]}],"summary":"Delete vpcs","tags":["Vpcs"]},"get":{"operationId":"src.controller.v1.vpcs.get","parameters":[{"$ref":"#/components/parameters/vpcs_id"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vpcs"}}},"description":"Return a vpcs object"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Get a single vpcs","tags":["Vpcs"]},"put":{"description":"Change vpcs name","operationId":"src.controller.v1.vpcs.rename","parameters":[{"$ref":"#/components/parameters/vpcs_id"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vpcs"}}},"description":"Body with name field"},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vpcs"}}},"description":"Return a renamed vpcs object"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Rename vpcs","tags":["Vpcs"]}},"/vpcs/{vpcs_id}/members":{"get":{"operationId":"src.controller.v1.vpcs.get_members","parameters":[{"$ref":"#/components/parameters/vpcs_id"}],"responses":{"200":{"content":{"application/json":{"schema":{"default":[],"items":{"example":123,"type":"integer"},"type":"array"}}},"description":"Return a list of servers attached to this vpcs"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Get list of the members attached to vpcs","tags":["Vpcs"]},"post":{"operationId":"src.controller.v1.vpcs.attach_member","parameters":[{"$ref":"#/components/parameters/vpcs_id"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"action":{"oneOf":[{"$ref":"#/components/schemas/Action"},{"$ref":"#/components/schemas/ActionByStr"}]}},"type":"object"}}},"description":"Return action_id"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Attach resource to vpcs","tags":["Vpcs"]}},"/vpcs/{vpcs_id}/members/{resource_id}":{"delete":{"operationId":"src.controller.v1.vpcs.detach_member","parameters":[{"$ref":"#/components/parameters/vpcs_id"},{"$ref":"#/components/parameters/resource_id"}],"responses":{"204":{"description":"Return action_id"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Detach resource from vpcs","tags":["Vpcs"]}}},"servers":[{"url":"/v1"}]} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ddaecdb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "regcloud-api" +version = "0.1.0" +description = "Клиент к API облака REG.RU (Рег.облако)" +readme = "README.md" +requires-python = ">=3.10" +dependencies = ["httpx>=0.27"] + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["regcloud_api"] diff --git a/regcloud_api/__init__.py b/regcloud_api/__init__.py new file mode 100644 index 0000000..97af898 --- /dev/null +++ b/regcloud_api/__init__.py @@ -0,0 +1,5 @@ +"""Клиент к API облака REG.RU.""" +from .client import RegCloud, load_state, save_state + +__all__ = ["RegCloud", "load_state", "save_state"] +__version__ = "0.1.0" diff --git a/regcloud_api/client.py b/regcloud_api/client.py new file mode 100644 index 0000000..3b7b350 --- /dev/null +++ b/regcloud_api/client.py @@ -0,0 +1,227 @@ +"""Клиент к API облака REG.RU (Рег.облако). + +API устроено как у DigitalOcean, сервер там называется reglet. +Токен берётся в личном кабинете, в разделе настроек. + + from regcloud_api import RegCloud + + api = RegCloud() # токен из REGRU_CLOUD_TOKEN + for s in api.list(): + print(s["id"], s["name"], s["status"]) + +Зачем обёртка, если API простое. Затем, что у него есть неочевидные места, +на которые мы уже наступили и которые тут обойдены. + +Адрес машины лежит в поле ip, а не в networks, оно всегда пустое. +Снимок нельзя снять с остановленной машины, API сам её останавливает по offline. +Сразу после снимка машина сидит в locked и удаляться отказывается примерно полминуты, +поэтому удаление тут настойчивое, иначе забытая машина будет тикать по деньгам. +Тарифы с видеокартами метод sizes не показывает вовсе, хотя создание с ними работает, +их список лежит в docs_slugs.md. +""" +from __future__ import annotations + +import json +import os +import pathlib +import time + +import httpx + +BASE = "https://api.cloudvps.reg.ru/v1" +STATE_FILE = pathlib.Path(os.environ.get("REGCLOUD_STATE_DIR", ".")) / "regcloud_state.json" + + +class RegCloud: + def __init__(self, token: str | None = None, timeout: float = 60.0): + self.token = token or os.environ.get("REGRU_CLOUD_TOKEN", "") + if not self.token: + raise RuntimeError("нет токена, положи REGRU_CLOUD_TOKEN в окружение") + self.http = httpx.Client( + base_url=BASE, timeout=timeout, + headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}, + ) + + # ---------- справочники ---------- + + def sizes(self) -> list[dict]: + r = self.http.get("/sizes") + r.raise_for_status() + return r.json().get("sizes", []) + + def gpu_sizes(self) -> list[dict]: + """Тарифы с видеокартой. У них в описании или в названии есть упоминание gpu.""" + out = [] + for s in self.sizes(): + blob = json.dumps(s, ensure_ascii=False).lower() + if "gpu" in blob or "nvidia" in blob or "a4000" in blob or "a5000" in blob or "a100" in blob: + out.append(s) + return out + + def images(self) -> list[dict]: + r = self.http.get("/images") + r.raise_for_status() + return r.json().get("images", []) + + def snapshots(self) -> list[dict]: + r = self.http.get("/snapshots") + r.raise_for_status() + return r.json().get("snapshots", []) + + def ssh_keys(self) -> list[dict]: + r = self.http.get("/account/keys") + r.raise_for_status() + return r.json().get("ssh_keys", []) + + def balance(self) -> dict: + r = self.http.get("/balance_data") + return r.json() if r.status_code == 200 else {"error": r.status_code, "text": r.text[:200]} + + def prices(self) -> dict: + r = self.http.get("/prices") + return r.json() if r.status_code == 200 else {} + + # ---------- машины ---------- + + def list(self) -> list[dict]: + r = self.http.get("/reglets") + r.raise_for_status() + return r.json().get("reglets", []) + + def get(self, reglet_id: int) -> dict: + r = self.http.get(f"/reglets/{reglet_id}") + r.raise_for_status() + return r.json().get("reglet", {}) + + def create(self, size: str, image: str | int, name: str = "gpuhub-worker", + ssh_keys: list[str] | None = None, region: str = "") -> dict: + payload: dict = {"name": name, "size": size, "image": image} + if ssh_keys: + payload["ssh_keys"] = ssh_keys + if region: + payload["region_slug"] = region + r = self.http.post("/reglets", json=payload) + if r.status_code >= 400: + raise RuntimeError(f"не создалась машина, {r.status_code} {r.text[:300]}") + return r.json().get("reglet", {}) + + def destroy(self, reglet_id: int, patient: bool = True, timeout: float = 900) -> bool: + """Удаление машины. + + Сразу после снятия снимка машина сидит в locked и удаляться отказывается, + отвечая RESOURCE_DELETE_ERROR. Проверено на живой машине, отпускает примерно + через полминуты. Поэтому по умолчанию долбимся, пока не поддастся, иначе + забытая машина будет тикать по деньгам. + """ + deadline = time.time() + timeout + while True: + r = self.http.delete(f"/reglets/{reglet_id}") + if r.status_code < 400 or r.status_code == 404: + return True + body = r.text[:200] + if "NO_SUCH_REGLET" in body: + return True # её уже нет, это успех + if not patient or time.time() > deadline: + raise RuntimeError(f"не удалилась машина, {r.status_code} {body}") + time.sleep(20) + + def action(self, reglet_id: int, action_type: str, **extra) -> dict: + r = self.http.post(f"/reglets/{reglet_id}/actions", json={"type": action_type, **extra}) + if r.status_code >= 400: + raise RuntimeError(f"действие {action_type} не прошло, {r.status_code} {r.text[:200]}") + return r.json() + + def snapshot(self, reglet_id: int, name: str, offline: bool = True) -> dict: + """Снимок машины. Из него потом поднимаются готовые к работе клоны. + Снимаем с выключенной, так образ выходит целостным.""" + return self.action(reglet_id, "snapshot", name=name, offline=offline) + + def stop(self, reglet_id: int) -> dict: + """Остановка. Важно понимать, что у большинства провайдеров остановленная машина + всё равно занимает диск и продолжает тарифицироваться. Для экономии её нужно удалять.""" + return self.action(reglet_id, "stop") + + def start(self, reglet_id: int) -> dict: + return self.action(reglet_id, "start") + + def wait_action(self, action_id: int, timeout: float = 900) -> dict: + """Ждём завершения долгого действия, например снятия снимка.""" + deadline = time.time() + timeout + while time.time() < deadline: + r = self.http.get(f"/actions/{action_id}") + if r.status_code == 200: + data = r.json().get("action", {}) + if data.get("status") in ("completed", "errored"): + return data + time.sleep(5) + raise TimeoutError(f"действие {action_id} не завершилось за {timeout} секунд") + + def wait_active(self, reglet_id: int, timeout: float = 600) -> dict: + """Ждём, пока машина поднимется и получит адрес.""" + deadline = time.time() + timeout + while time.time() < deadline: + info = self.get(reglet_id) + if info.get("status") == "active" and self.ip_of(info): + return info + time.sleep(5) + raise TimeoutError(f"машина {reglet_id} не поднялась за {timeout} секунд") + + @staticmethod + def ip_of(reglet: dict) -> str: + """Адрес лежит прямо в поле ip, а networks у этого провайдера пустует.""" + if reglet.get("ip"): + return reglet["ip"] + nets = (reglet.get("networks") or {}).get("v4") or [] + for n in nets: + if n.get("type") == "public": + return n.get("ip_address", "") + return nets[0].get("ip_address", "") if nets else "" + + +# ---------- память о поднятых машинах ---------- + +def load_state() -> dict: + if STATE_FILE.exists(): + try: + return json.loads(STATE_FILE.read_text()) + except json.JSONDecodeError: + pass + return {"reglet_id": None, "ip": "", "started_at": 0, "idle_since": 0} + + +def save_state(state: dict) -> None: + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=1)) + + +if __name__ == "__main__": + import sys + api = RegCloud() + cmd = sys.argv[1] if len(sys.argv) > 1 else "sizes" + + if cmd == "sizes": + for s in api.gpu_sizes(): + price = s.get("price_hourly") or s.get("price_monthly") or "?" + print(f"{s.get('slug','?'):28s} {s.get('description','')[:50]:52s} {price}") + elif cmd == "all-sizes": + for s in api.sizes(): + print(f"{s.get('slug','?'):28s} {s.get('description','')[:60]}") + elif cmd == "images": + for i in api.images()[:40]: + print(f"{str(i.get('id','')):10s} {i.get('slug','') or i.get('name',''):35s} {i.get('distribution','')}") + elif cmd == "snapshots": + for s in api.snapshots(): + print(f"{s.get('id')} {s.get('name')} {s.get('size_gigabytes')} ГБ") + elif cmd == "list": + for r in api.list(): + print(f"{r.get('id')} {r.get('name'):25s} {r.get('status'):10s} {RegCloud.ip_of(r)}") + elif cmd == "keys": + for k in api.ssh_keys(): + print(f"{k.get('id')} {k.get('name')} {k.get('fingerprint')}") + elif cmd == "balance": + print(json.dumps(api.balance(), ensure_ascii=False, indent=1)) + elif cmd == "prices": + data = api.prices() + print(json.dumps(data, ensure_ascii=False, indent=1)[:3000]) + else: + print("sizes | all-sizes | images | snapshots | list | keys | balance | prices")