twc-cli 2.4.1__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of twc-cli might be problematic. Click here for more details.
- {twc_cli-2.4.1 → twc_cli-2.6.0}/CHANGELOG.md +46 -0
- twc_cli-2.6.0/PKG-INFO +83 -0
- twc_cli-2.6.0/README.md +57 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/pyproject.toml +9 -9
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/__main__.py +2 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/__version__.py +1 -1
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/api/base.py +5 -8
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/api/client.py +95 -11
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/api/types.py +64 -7
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/apiwrap.py +2 -2
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/__init__.py +1 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/common.py +18 -2
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/firewall.py +238 -29
- twc_cli-2.6.0/twc/commands/floating_ip.py +296 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/image.py +10 -7
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/project.py +1 -2
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/server.py +75 -21
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/storage.py +7 -3
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/vpc.py +23 -9
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/fmt.py +1 -1
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/vars.py +3 -5
- twc_cli-2.4.1/PKG-INFO +0 -66
- twc_cli-2.4.1/README.md +0 -40
- {twc_cli-2.4.1 → twc_cli-2.6.0}/COPYING +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/__init__.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/api/__init__.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/api/exceptions.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/account.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/balancer.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/config.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/database.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/domain.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/kubernetes.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/commands/ssh_key.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/typerx.py +0 -0
- {twc_cli-2.4.1 → twc_cli-2.6.0}/twc/utils.py +0 -0
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
|
|
4
4
|
|
|
5
|
+
# Версия 2.6.0 (2024.08.14)
|
|
6
|
+
|
|
7
|
+
## Добавлено
|
|
8
|
+
|
|
9
|
+
- В команды и API-клиент для управления облачным файрволом добавлена поддержка протоколов TCP6, UDP6, ICMP6 и настройка стандартной политики (DROP или ACCEPT).
|
|
10
|
+
- Добавлены новые команды: `twc firewall group get`, `twc firewall group dump` и `twc firewall group restore`.
|
|
11
|
+
|
|
12
|
+
## Изменено
|
|
13
|
+
|
|
14
|
+
- Улучшена валидация параметров и подстановка значений по умолчанию в командах `twc firewall`.
|
|
15
|
+
- Команда `twc firewall rule remove` теперь может принимать список UUID правил через пробел.
|
|
16
|
+
- В команде `twc firewall show` аргумент `all` стал необязательным.
|
|
17
|
+
- Зависимость `typer` заменена на `typer-slim`.
|
|
18
|
+
|
|
19
|
+
## Исправлено
|
|
20
|
+
|
|
21
|
+
- Исправлено определение ендпоинта объектного хранилища при вызове команды `twc storage genconfig`.
|
|
22
|
+
|
|
23
|
+
# Версия 2.5.0 (2024.07.24)
|
|
24
|
+
|
|
25
|
+
## Добавлено
|
|
26
|
+
|
|
27
|
+
- Добавлена поддержка зон доступности. Добавлен параметр конифгурации `availability_zone` (`TWC_AVAILABILITY_ZONE`).
|
|
28
|
+
- Добавлены новые опции для команды `twc server create`: `--availability-zone`, `--user-data`, `--public-ip`, `--no-public-ip`, `--private-ip`.
|
|
29
|
+
- Добавлена новая команда `twc ip` вместо устаревшей `twc server ip`. Поскольку IP-адреса сейчас являются отдельными ресурсами, управление ими вынесено из `twc server`.
|
|
30
|
+
|
|
31
|
+
## Изменено
|
|
32
|
+
|
|
33
|
+
- Минимальная поддерживаемая версия интерпретатора Python повышена до 3.8.
|
|
34
|
+
- Обновлены версии зависимостей.
|
|
35
|
+
- Обновлены команды и методы API-клиента для работы VPC.
|
|
36
|
+
- Удалён параметр `log_response` принимаемый как значение переменной окружения `TWC_LOG`, теперь тело ответа API логируется всегда.
|
|
37
|
+
- Команда `twc server ip` объявлена устаревшей (deprecated) и будет уделена в следующем мажорном релизе. Вместо неё используйте команду `twc ip`.
|
|
38
|
+
- Другие мелкие обновления CLI без нарушения обратной совместимости.
|
|
39
|
+
|
|
40
|
+
## Исправлено
|
|
41
|
+
|
|
42
|
+
- Исправлен метод удаления кластеров Kubernetes во встроенном API-клиенте.
|
|
43
|
+
- Исправлена ошибка разбора JSON при получении объекта сервера без IP.
|
|
44
|
+
|
|
45
|
+
# Версия 2.4.1 (2023.08.30)
|
|
46
|
+
|
|
47
|
+
## Исправлено
|
|
48
|
+
|
|
49
|
+
- Исправлена ошибка парсинга доменного имени в команде `twc domain record add`.
|
|
50
|
+
|
|
5
51
|
# Версия 2.4.0 (2023.08.21)
|
|
6
52
|
|
|
7
53
|
## Добавлено
|
twc_cli-2.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: twc-cli
|
|
3
|
+
Version: 2.6.0
|
|
4
|
+
Summary: Timeweb Cloud Command Line Interface.
|
|
5
|
+
Home-page: https://github.com/timeweb-cloud/twc
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: ge
|
|
8
|
+
Author-email: dev@timeweb.cloud
|
|
9
|
+
Requires-Python: >=3.8.19,<4.0.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
17
|
+
Requires-Dist: pygments (>=2.18.0,<3.0.0)
|
|
18
|
+
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
19
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
20
|
+
Requires-Dist: shellingham (>=1.5.4,<2.0.0)
|
|
21
|
+
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
22
|
+
Requires-Dist: typer-slim (>=0.12.3,<0.13.0)
|
|
23
|
+
Project-URL: Repository, https://github.com/timeweb-cloud/twc
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<picture>
|
|
27
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/dark.svg" type="image/svg+xml">
|
|
28
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/dark.png" type="image/png">
|
|
29
|
+
<source media="(prefers-color-scheme: light)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/light.svg" type="image/svg+xml">
|
|
30
|
+
<source media="(prefers-color-scheme: light)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/light.png" type="image/png">
|
|
31
|
+
<img alt="TWC CLI" src="https://ec650031-twc-cli.s3.timeweb.cloud/light.png">
|
|
32
|
+
</picture>
|
|
33
|
+
|
|
34
|
+
Timeweb Cloud Command Line Interface and simple SDK 💫
|
|
35
|
+
|
|
36
|
+
* [Руководство пользователя](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/README.md) 🇷🇺
|
|
37
|
+
* [Command Line Interface (CLI) Reference](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/CLI_REFERENCE.md)
|
|
38
|
+
|
|
39
|
+
# Installation
|
|
40
|
+
|
|
41
|
+
From PyPI registry via pip:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install twc-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Using [pipx](https://pipx.pypa.io/stable/):
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
pipx install twc-cli
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install [zippap](https://docs.python.org/3/library/zipapp.html) in your PATH.
|
|
54
|
+
Look for prebuilt `.pyz` archives on [releases page](https://github.com/timeweb-cloud/twc/releases/latest).
|
|
55
|
+
|
|
56
|
+
# Getting started
|
|
57
|
+
|
|
58
|
+
Get Timeweb Cloud [access token](https://timeweb.cloud/my/api-keys) and
|
|
59
|
+
configure **twc** with command:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
twc config
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Enter your access token and hit `Enter`.
|
|
66
|
+
|
|
67
|
+
Configuration done! Let's use:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
twc --help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
# Shell completion
|
|
74
|
+
|
|
75
|
+
To install completion script run:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
twc --install-completion
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**twc** automatically detect your shell. Supported: Bash, Zsh, Fish, PowerShell.
|
|
82
|
+
|
|
83
|
+
|
twc_cli-2.6.0/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/dark.svg" type="image/svg+xml">
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/dark.png" type="image/png">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/light.svg" type="image/svg+xml">
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="https://ec650031-twc-cli.s3.timeweb.cloud/light.png" type="image/png">
|
|
6
|
+
<img alt="TWC CLI" src="https://ec650031-twc-cli.s3.timeweb.cloud/light.png">
|
|
7
|
+
</picture>
|
|
8
|
+
|
|
9
|
+
Timeweb Cloud Command Line Interface and simple SDK 💫
|
|
10
|
+
|
|
11
|
+
* [Руководство пользователя](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/README.md) 🇷🇺
|
|
12
|
+
* [Command Line Interface (CLI) Reference](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/CLI_REFERENCE.md)
|
|
13
|
+
|
|
14
|
+
# Installation
|
|
15
|
+
|
|
16
|
+
From PyPI registry via pip:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
pip install twc-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Using [pipx](https://pipx.pypa.io/stable/):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
pipx install twc-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install [zippap](https://docs.python.org/3/library/zipapp.html) in your PATH.
|
|
29
|
+
Look for prebuilt `.pyz` archives on [releases page](https://github.com/timeweb-cloud/twc/releases/latest).
|
|
30
|
+
|
|
31
|
+
# Getting started
|
|
32
|
+
|
|
33
|
+
Get Timeweb Cloud [access token](https://timeweb.cloud/my/api-keys) and
|
|
34
|
+
configure **twc** with command:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
twc config
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Enter your access token and hit `Enter`.
|
|
41
|
+
|
|
42
|
+
Configuration done! Let's use:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
twc --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
# Shell completion
|
|
49
|
+
|
|
50
|
+
To install completion script run:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
twc --install-completion
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**twc** automatically detect your shell. Supported: Bash, Zsh, Fish, PowerShell.
|
|
57
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "twc-cli"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.6.0"
|
|
4
4
|
description = "Timeweb Cloud Command Line Interface."
|
|
5
5
|
authors = ["ge <dev@timeweb.cloud>"]
|
|
6
6
|
homepage = "https://github.com/timeweb-cloud/twc"
|
|
@@ -11,18 +11,18 @@ include = ["CHANGELOG.md", "COPYING"]
|
|
|
11
11
|
packages = [{ include = "twc", from = "." }]
|
|
12
12
|
|
|
13
13
|
[tool.poetry.dependencies]
|
|
14
|
-
python = "^3.
|
|
15
|
-
requests = "^2.
|
|
16
|
-
typer = "^0.
|
|
17
|
-
shellingham = "^1.5.
|
|
14
|
+
python = "^3.8.19"
|
|
15
|
+
requests = "^2.32.3"
|
|
16
|
+
typer-slim = "^0.12.3"
|
|
17
|
+
shellingham = "^1.5.4"
|
|
18
18
|
colorama = "^0.4.6"
|
|
19
19
|
toml = "^0.10.2"
|
|
20
|
-
pyyaml = "^6.0"
|
|
21
|
-
pygments = "^2.
|
|
20
|
+
pyyaml = "^6.0.1"
|
|
21
|
+
pygments = "^2.18.0"
|
|
22
22
|
|
|
23
23
|
[tool.poetry.group.dev.dependencies]
|
|
24
|
-
black = "^
|
|
25
|
-
pylint = "^2.
|
|
24
|
+
black = "^24.4.2"
|
|
25
|
+
pylint = "^3.2.5"
|
|
26
26
|
|
|
27
27
|
[tool.poetry.scripts]
|
|
28
28
|
twc = "twc.__main__:cli"
|
|
@@ -20,6 +20,7 @@ from .commands import (
|
|
|
20
20
|
domain,
|
|
21
21
|
vpc,
|
|
22
22
|
firewall,
|
|
23
|
+
floating_ip,
|
|
23
24
|
)
|
|
24
25
|
from .commands.common import version_callback, version_option, verbose_option
|
|
25
26
|
|
|
@@ -44,6 +45,7 @@ cli.add_typer(
|
|
|
44
45
|
cli.add_typer(domain, name="domain", aliases=["domains", "d"])
|
|
45
46
|
cli.add_typer(vpc, name="vpc", aliases=["vpcs", "network", "networks"])
|
|
46
47
|
cli.add_typer(firewall, name="firewall", aliases=["fw"])
|
|
48
|
+
cli.add_typer(floating_ip, name="ip", aliases=["ips"])
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
@cli.command("version")
|
|
@@ -29,7 +29,6 @@ class TimewebCloudBase:
|
|
|
29
29
|
user_agent: Optional[str] = USER_AGENT,
|
|
30
30
|
timeout: Optional[int] = TIMEOUT,
|
|
31
31
|
hide_token: Optional[bool] = True,
|
|
32
|
-
log_response_body: Optional[bool] = False,
|
|
33
32
|
request_decorator: Optional[Callable] = None,
|
|
34
33
|
):
|
|
35
34
|
self.api_token = api_token
|
|
@@ -44,7 +43,6 @@ class TimewebCloudBase:
|
|
|
44
43
|
self.headers["Authorization"] = f"Bearer {self.api_token}"
|
|
45
44
|
self.log = logging.getLogger("api_client")
|
|
46
45
|
self.hide_token = hide_token
|
|
47
|
-
self.log_response_body = log_response_body
|
|
48
46
|
|
|
49
47
|
if headers:
|
|
50
48
|
self.headers.update(headers)
|
|
@@ -66,10 +64,10 @@ class TimewebCloudBase:
|
|
|
66
64
|
|
|
67
65
|
def _log_request(self, response: requests.Response) -> None:
|
|
68
66
|
"""Log HTTP requests."""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
res_body = response.text or "<NO_BODY>"
|
|
68
|
+
req_body = response.request.body or "<NO_BODY>"
|
|
69
|
+
if isinstance(req_body, (bytes, bytearray)):
|
|
70
|
+
req_body = req_body.decode()
|
|
73
71
|
|
|
74
72
|
self.log.debug(
|
|
75
73
|
textwrap.dedent(
|
|
@@ -89,7 +87,7 @@ class TimewebCloudBase:
|
|
|
89
87
|
req_headers=self._format_headers(
|
|
90
88
|
self._secure_log(response.request.headers)
|
|
91
89
|
),
|
|
92
|
-
req_body=
|
|
90
|
+
req_body=req_body,
|
|
93
91
|
res=response,
|
|
94
92
|
res_headers=self._format_headers(response.headers),
|
|
95
93
|
res_body=res_body,
|
|
@@ -136,7 +134,6 @@ class TimewebCloudBase:
|
|
|
136
134
|
response.raise_for_status()
|
|
137
135
|
self._log_request(response)
|
|
138
136
|
except requests.HTTPError as e:
|
|
139
|
-
self.log_response_body = True # Always log response body on errors
|
|
140
137
|
self._log_request(response)
|
|
141
138
|
|
|
142
139
|
# API issue: Bad response: 401 Unauthorized response haven't body
|
|
@@ -18,6 +18,7 @@ from .types import (
|
|
|
18
18
|
BackupInterval,
|
|
19
19
|
IPVersion,
|
|
20
20
|
ServiceRegion,
|
|
21
|
+
ServiceAvailabilityZone,
|
|
21
22
|
ResourceType,
|
|
22
23
|
DBMS,
|
|
23
24
|
MySQLAuthPlugin,
|
|
@@ -25,6 +26,7 @@ from .types import (
|
|
|
25
26
|
LoadBalancerAlgo,
|
|
26
27
|
FirewallProto,
|
|
27
28
|
FirewallDirection,
|
|
29
|
+
FirewallPolicy,
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
|
|
@@ -70,9 +72,10 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
70
72
|
avatar_id: Optional[str] = None,
|
|
71
73
|
software_id: Optional[int] = None,
|
|
72
74
|
ssh_keys_ids: Optional[List[int]] = None,
|
|
73
|
-
is_local_network: Optional[bool] = None,
|
|
75
|
+
is_local_network: Optional[bool] = None, # deprecated
|
|
74
76
|
is_ddos_guard: bool = False,
|
|
75
77
|
network: Optional[dict] = None,
|
|
78
|
+
availability_zone: Optional[ServiceAvailabilityZone] = None,
|
|
76
79
|
):
|
|
77
80
|
"""Create new Cloud Server. Note:
|
|
78
81
|
|
|
@@ -105,6 +108,11 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
105
108
|
**({"preset_id": preset_id} if preset_id else {}),
|
|
106
109
|
**({"os_id": os_id} if os_id else {}),
|
|
107
110
|
**({"image_id": image_id} if image_id else {}),
|
|
111
|
+
**(
|
|
112
|
+
{"availability_zone": str(availability_zone)}
|
|
113
|
+
if availability_zone
|
|
114
|
+
else {}
|
|
115
|
+
),
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
return self._request("POST", f"{self.api_url}/servers", json=payload)
|
|
@@ -697,7 +705,7 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
697
705
|
)
|
|
698
706
|
|
|
699
707
|
# -----------------------------------------------------------------------
|
|
700
|
-
#
|
|
708
|
+
# Managed databases
|
|
701
709
|
|
|
702
710
|
def get_databases(self, limit: int = 100, offset: int = 0):
|
|
703
711
|
"""Get databases list."""
|
|
@@ -1343,7 +1351,7 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1343
1351
|
def delete_k8s_node(self, cluster_id: int, node_id: int):
|
|
1344
1352
|
"""Delete node from cluster."""
|
|
1345
1353
|
return self._request(
|
|
1346
|
-
"
|
|
1354
|
+
"DELETE",
|
|
1347
1355
|
f"{self.api_url}/k8s/clusters/{cluster_id}/nodes/{node_id}",
|
|
1348
1356
|
)
|
|
1349
1357
|
|
|
@@ -1483,6 +1491,7 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1483
1491
|
name: str,
|
|
1484
1492
|
subnet: IPv4Network,
|
|
1485
1493
|
location: ServiceRegion,
|
|
1494
|
+
availability_zone: Optional[ServiceAvailabilityZone] = None,
|
|
1486
1495
|
description: Optional[str] = None,
|
|
1487
1496
|
):
|
|
1488
1497
|
"""Create new virtual private network."""
|
|
@@ -1490,6 +1499,11 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1490
1499
|
"name": name,
|
|
1491
1500
|
"subnet_v4": subnet,
|
|
1492
1501
|
"location": location,
|
|
1502
|
+
**(
|
|
1503
|
+
{"availability_zone": str(availability_zone)}
|
|
1504
|
+
if availability_zone
|
|
1505
|
+
else {}
|
|
1506
|
+
),
|
|
1493
1507
|
**({"description": description} if description else {}),
|
|
1494
1508
|
}
|
|
1495
1509
|
return self._request("POST", f"{self.api_url_v2}/vpcs", json=payload)
|
|
@@ -1536,14 +1550,20 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1536
1550
|
)
|
|
1537
1551
|
|
|
1538
1552
|
def create_firewall_group(
|
|
1539
|
-
self,
|
|
1553
|
+
self,
|
|
1554
|
+
name: str,
|
|
1555
|
+
description: Optional[str] = None,
|
|
1556
|
+
policy: Optional[FirewallPolicy] = FirewallPolicy.DROP,
|
|
1540
1557
|
):
|
|
1541
1558
|
payload = {
|
|
1542
1559
|
"name": name,
|
|
1543
1560
|
**({"description": description} if description else {}),
|
|
1544
1561
|
}
|
|
1545
1562
|
return self._request(
|
|
1546
|
-
"POST",
|
|
1563
|
+
"POST",
|
|
1564
|
+
f"{self.api_url}/firewall/groups",
|
|
1565
|
+
json=payload,
|
|
1566
|
+
params={"policy": policy},
|
|
1547
1567
|
)
|
|
1548
1568
|
|
|
1549
1569
|
def get_firewall_group(self, group_id: UUID):
|
|
@@ -1623,16 +1643,16 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1623
1643
|
self,
|
|
1624
1644
|
group_id: UUID,
|
|
1625
1645
|
direction: FirewallDirection,
|
|
1626
|
-
|
|
1646
|
+
protocol: FirewallProto,
|
|
1627
1647
|
cidr: Union[IPv4Network, IPv6Network],
|
|
1628
1648
|
port: Optional[str] = None,
|
|
1629
1649
|
description: Optional[str] = None,
|
|
1630
1650
|
):
|
|
1631
1651
|
payload = {
|
|
1632
1652
|
**({"description": description} if description else {}),
|
|
1633
|
-
**({} if
|
|
1653
|
+
**({} if protocol == FirewallProto.ICMP.value else {"port": port}),
|
|
1634
1654
|
"direction": direction,
|
|
1635
|
-
"protocol":
|
|
1655
|
+
"protocol": protocol,
|
|
1636
1656
|
"cidr": cidr,
|
|
1637
1657
|
}
|
|
1638
1658
|
return self._request(
|
|
@@ -1657,16 +1677,16 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1657
1677
|
group_id: UUID,
|
|
1658
1678
|
rule_id: UUID,
|
|
1659
1679
|
direction: FirewallDirection,
|
|
1660
|
-
|
|
1680
|
+
protocol: FirewallProto,
|
|
1661
1681
|
cidr: Union[IPv4Network, IPv6Network],
|
|
1662
1682
|
port: Optional[str] = None,
|
|
1663
1683
|
description: Optional[str] = None,
|
|
1664
1684
|
):
|
|
1665
1685
|
payload = {
|
|
1666
1686
|
**({"description": description} if description else {}),
|
|
1667
|
-
**({} if
|
|
1687
|
+
**({} if protocol == FirewallProto.ICMP.value else {"port": port}),
|
|
1668
1688
|
"direction": direction,
|
|
1669
|
-
"protocol":
|
|
1689
|
+
"protocol": protocol,
|
|
1670
1690
|
"cidr": cidr,
|
|
1671
1691
|
}
|
|
1672
1692
|
return self._request(
|
|
@@ -1690,3 +1710,67 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1690
1710
|
f"{self.api_url}/firewall/service/{resource_type}/{resource_id}",
|
|
1691
1711
|
params=params,
|
|
1692
1712
|
)
|
|
1713
|
+
|
|
1714
|
+
# -----------------------------------------------------------------------
|
|
1715
|
+
# Floating IPs
|
|
1716
|
+
|
|
1717
|
+
def get_floating_ips(self):
|
|
1718
|
+
return self._request("GET", f"{self.api_url_v1}/floating-ips")
|
|
1719
|
+
|
|
1720
|
+
def get_floating_ip(self, floating_ip_id: str):
|
|
1721
|
+
return self._request(
|
|
1722
|
+
"GET", f"{self.api_url_v1}/floating-ips/{floating_ip_id}"
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
def create_floating_ip(
|
|
1726
|
+
self,
|
|
1727
|
+
availability_zone: ServiceAvailabilityZone,
|
|
1728
|
+
ddos_protection: bool = False,
|
|
1729
|
+
):
|
|
1730
|
+
payload = {
|
|
1731
|
+
"is_ddos_guard": ddos_protection,
|
|
1732
|
+
"availability_zone": str(availability_zone),
|
|
1733
|
+
}
|
|
1734
|
+
return self._request(
|
|
1735
|
+
"POST", f"{self.api_url_v1}/floating-ips", json=payload
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
def update_floating_ip(
|
|
1739
|
+
self,
|
|
1740
|
+
floating_ip_id: str,
|
|
1741
|
+
comment: Optional[str] = None,
|
|
1742
|
+
ptr: Optional[str] = None,
|
|
1743
|
+
):
|
|
1744
|
+
payload = {}
|
|
1745
|
+
if comment:
|
|
1746
|
+
payload["comment"] = comment
|
|
1747
|
+
if ptr:
|
|
1748
|
+
payload["ptr"] = ptr
|
|
1749
|
+
return self._request(
|
|
1750
|
+
"PATCH",
|
|
1751
|
+
f"{self.api_url_v1}/floating-ips/{floating_ip_id}",
|
|
1752
|
+
json=payload,
|
|
1753
|
+
)
|
|
1754
|
+
|
|
1755
|
+
def delete_floating_ip(self, floating_ip_id: str):
|
|
1756
|
+
return self._request(
|
|
1757
|
+
"DELETE", f"{self.api_url_v1}/floating-ips/{floating_ip_id}"
|
|
1758
|
+
)
|
|
1759
|
+
|
|
1760
|
+
def attach_floating_ip(
|
|
1761
|
+
self,
|
|
1762
|
+
floating_ip_id: str,
|
|
1763
|
+
resource_type: ResourceType,
|
|
1764
|
+
resource_id: str,
|
|
1765
|
+
):
|
|
1766
|
+
payload = {"resource_type": resource_type, "resource_id": resource_id}
|
|
1767
|
+
return self._request(
|
|
1768
|
+
"POST",
|
|
1769
|
+
f"{self.api_url_v1}/floating-ips/{floating_ip_id}/bind",
|
|
1770
|
+
json=payload,
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
def detach_floating_ip(self, floating_ip_id: str):
|
|
1774
|
+
return self._request(
|
|
1775
|
+
"POST", f"{self.api_url_v1}/floating-ips/{floating_ip_id}/unbind"
|
|
1776
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Custom data types for Timeweb Cloud API entities."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import TypedDict, List, Optional
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
6
6
|
|
|
@@ -9,10 +9,59 @@ class ServiceRegion(str, Enum):
|
|
|
9
9
|
|
|
10
10
|
RU_1 = "ru-1"
|
|
11
11
|
RU_2 = "ru-2"
|
|
12
|
-
|
|
12
|
+
RU_3 = "ru-3"
|
|
13
13
|
KZ_1 = "kz-1"
|
|
14
|
+
PL_1 = "pl-1"
|
|
14
15
|
NL_1 = "nl-1"
|
|
15
16
|
|
|
17
|
+
@classmethod
|
|
18
|
+
def get_zones(cls, region: str) -> List[str]:
|
|
19
|
+
# pylint: disable=too-many-return-statements
|
|
20
|
+
if region == cls.RU_1:
|
|
21
|
+
return ["spb-1", "spb-2", "spb-3", "spb-4"]
|
|
22
|
+
if region == cls.RU_2:
|
|
23
|
+
return ["nsk-1"]
|
|
24
|
+
if region == cls.RU_3:
|
|
25
|
+
return ["msk-1"]
|
|
26
|
+
if region == cls.KZ_1:
|
|
27
|
+
return ["ala-1"]
|
|
28
|
+
if region == cls.PL_1:
|
|
29
|
+
return ["gdn-1"]
|
|
30
|
+
if region == cls.NL_1:
|
|
31
|
+
return ["ams-1"]
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ServiceAvailabilityZone(str, Enum):
|
|
36
|
+
"""Availability zones."""
|
|
37
|
+
|
|
38
|
+
SPB_1 = "spb-1"
|
|
39
|
+
SPB_2 = "spb-2"
|
|
40
|
+
SPB_3 = "spb-3"
|
|
41
|
+
SPB_4 = "spb-4"
|
|
42
|
+
MSK_1 = "msk-1"
|
|
43
|
+
NSK_1 = "nsk-1"
|
|
44
|
+
ALA_1 = "ala-1"
|
|
45
|
+
GDN_1 = "gdn-1"
|
|
46
|
+
AMS_1 = "ams-1"
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_region(cls, zone: str) -> Optional[str]:
|
|
50
|
+
# pylint: disable=too-many-return-statements
|
|
51
|
+
if zone in [cls.SPB_1, cls.SPB_2, cls.SPB_3, cls.SPB_4]:
|
|
52
|
+
return ServiceRegion.RU_1
|
|
53
|
+
if zone == cls.NSK_1:
|
|
54
|
+
return ServiceRegion.RU_2
|
|
55
|
+
if zone == cls.MSK_1:
|
|
56
|
+
return ServiceRegion.RU_3
|
|
57
|
+
if zone == cls.ALA_1:
|
|
58
|
+
return ServiceRegion.KZ_1
|
|
59
|
+
if zone == cls.GDN_1:
|
|
60
|
+
return ServiceRegion.PL_1
|
|
61
|
+
if zone == cls.AMS_1:
|
|
62
|
+
return ServiceRegion.NL_1
|
|
63
|
+
return None
|
|
64
|
+
|
|
16
65
|
|
|
17
66
|
class ServerAction(str, Enum):
|
|
18
67
|
"""Available actions on Cloud Server."""
|
|
@@ -64,13 +113,11 @@ class IPVersion(str, Enum):
|
|
|
64
113
|
IPV6 = "ipv6"
|
|
65
114
|
|
|
66
115
|
|
|
67
|
-
class ServerConfiguration(
|
|
116
|
+
class ServerConfiguration(TypedDict):
|
|
68
117
|
"""
|
|
69
118
|
For `confugurator_id` see `get_server_configurators()`. `disk` and
|
|
70
|
-
`ram` must be in megabytes. Values must
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
TODO: Replace NamedTuple with TypedDict when drop Python 3.7
|
|
119
|
+
`ram` must be in megabytes. Values must comply with the configurator
|
|
120
|
+
constraints.
|
|
74
121
|
"""
|
|
75
122
|
|
|
76
123
|
configurator_id: int
|
|
@@ -181,6 +228,9 @@ class FirewallProto(str, Enum):
|
|
|
181
228
|
TCP = "tcp"
|
|
182
229
|
UDP = "udp"
|
|
183
230
|
ICMP = "icmp"
|
|
231
|
+
TCP6 = "tcp6"
|
|
232
|
+
UDP6 = "udp6"
|
|
233
|
+
ICMP6 = "icmp6"
|
|
184
234
|
|
|
185
235
|
|
|
186
236
|
class FirewallDirection(str, Enum):
|
|
@@ -188,3 +238,10 @@ class FirewallDirection(str, Enum):
|
|
|
188
238
|
|
|
189
239
|
INGRESS = "ingress"
|
|
190
240
|
EGRESS = "egress"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class FirewallPolicy(str, Enum):
|
|
244
|
+
"""Firewall default policy."""
|
|
245
|
+
|
|
246
|
+
DROP = "DROP"
|
|
247
|
+
ACCEPT = "ACCEPT"
|
|
@@ -73,8 +73,8 @@ def create_client(config: Path, profile: str, **kwargs) -> TimewebCloud:
|
|
|
73
73
|
|
|
74
74
|
if log_settings:
|
|
75
75
|
for param in log_settings.split(","):
|
|
76
|
-
if param == "
|
|
77
|
-
|
|
76
|
+
if param.lower() == "debug":
|
|
77
|
+
pass # FUTURE: set logging.DEBUG
|
|
78
78
|
|
|
79
79
|
if token:
|
|
80
80
|
debug("Config: use API token from environment")
|
|
@@ -13,7 +13,7 @@ from typer.core import TyperOption
|
|
|
13
13
|
from click import UsageError
|
|
14
14
|
|
|
15
15
|
from twc.__version__ import __version__
|
|
16
|
-
from twc.api.types import ServiceRegion
|
|
16
|
+
from twc.api.types import ServiceRegion, ServiceAvailabilityZone
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class OutputFormat(str, Enum):
|
|
@@ -109,6 +109,11 @@ def load_from_config_callback(
|
|
|
109
109
|
regions = [v.value for v in ServiceRegion]
|
|
110
110
|
if value not in regions:
|
|
111
111
|
sys.exit(f"Error: Location not in {regions}")
|
|
112
|
+
if param.name == "zone":
|
|
113
|
+
value = value.lower()
|
|
114
|
+
zones = [z.value for z in ServiceAvailabilityZone]
|
|
115
|
+
if value not in zones:
|
|
116
|
+
sys.exit(f"Error: Availability zone not in {zones}")
|
|
112
117
|
return value
|
|
113
118
|
|
|
114
119
|
|
|
@@ -275,5 +280,16 @@ region_option = typer.Option(
|
|
|
275
280
|
envvar="TWC_REGION",
|
|
276
281
|
show_envvar=False,
|
|
277
282
|
callback=load_from_config_callback,
|
|
278
|
-
help="
|
|
283
|
+
help="Region (location).",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# availability_zone: Optional[str] = zone_option,
|
|
287
|
+
|
|
288
|
+
zone_option = typer.Option(
|
|
289
|
+
None,
|
|
290
|
+
metavar="ZONE",
|
|
291
|
+
envvar="TWC_AVAILABILITY_ZONE",
|
|
292
|
+
show_envvar=False,
|
|
293
|
+
callback=load_from_config_callback,
|
|
294
|
+
help="Availability zone.",
|
|
279
295
|
)
|