twc-cli 2.5.0__tar.gz → 2.7.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.5.0 → twc_cli-2.7.0}/CHANGELOG.md +39 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/PKG-INFO +22 -5
- twc_cli-2.7.0/README.md +57 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/pyproject.toml +2 -2
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/__main__.py +2 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/__version__.py +1 -1
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/api/client.py +20 -12
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/api/types.py +10 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/apiwrap.py +6 -1
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/__init__.py +1 -1
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/account.py +30 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/domain.py +28 -12
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/firewall.py +234 -25
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/project.py +2 -8
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/server.py +1 -3
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/storage.py +13 -9
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/fmt.py +11 -4
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/vars.py +1 -3
- twc_cli-2.5.0/README.md +0 -40
- {twc_cli-2.5.0 → twc_cli-2.7.0}/COPYING +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/__init__.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/api/__init__.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/api/base.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/api/exceptions.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/balancer.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/common.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/config.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/database.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/floating_ip.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/image.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/kubernetes.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/ssh_key.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/commands/vpc.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/typerx.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.7.0}/twc/utils.py +0 -0
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
|
|
4
4
|
|
|
5
|
+
# Версия 2.7.0 (2024.11.02)
|
|
6
|
+
|
|
7
|
+
## Добавлено
|
|
8
|
+
|
|
9
|
+
- Добавлена новая команда `twc whoami`, которая показывает логин аккаунта, в котором в текущий момент авторизован CLI.
|
|
10
|
+
|
|
11
|
+
## Изменено
|
|
12
|
+
|
|
13
|
+
- Команда `twc account status` теперь также отображает логин аккаунта.
|
|
14
|
+
|
|
15
|
+
## Исправлено
|
|
16
|
+
|
|
17
|
+
- Устранено падение программы при листинге образов (`twc image list`) и при создании сервера из образа.
|
|
18
|
+
- Исправлена ошибка при создании сервера с приватной сетью в зоне доступности SPB-3.
|
|
19
|
+
- Исправлены ошибки в командах `twc storage subdomain gencert` и `twc storage subdomain remove`, возникшие из-за нарушения обратной совместимости эндпоинтов API.
|
|
20
|
+
- Исправлена ошибка валидации приватного адреса при созаднии сервера — ошибочно запрещалось использовать 4-й по порядку адрес в подсети.
|
|
21
|
+
- Исправлена ошибка разбора JSON при выводе списка ресурсов в проекте.
|
|
22
|
+
- Исправлены ошибки добавления TXT записи для домена в команде `twc domain record add`.
|
|
23
|
+
- Исправлена ошибка парсинка субдомена в команде `twc domain record add`.
|
|
24
|
+
- Исправлены ошибки обновления DNS-записей на поддоменах.
|
|
25
|
+
|
|
26
|
+
# Версия 2.6.0 (2024.08.14)
|
|
27
|
+
|
|
28
|
+
## Добавлено
|
|
29
|
+
|
|
30
|
+
- В команды и API-клиент для управления облачным файрволом добавлена поддержка протоколов TCP6, UDP6, ICMP6 и настройка стандартной политики (DROP или ACCEPT).
|
|
31
|
+
- Добавлены новые команды: `twc firewall group get`, `twc firewall group dump` и `twc firewall group restore`.
|
|
32
|
+
|
|
33
|
+
## Изменено
|
|
34
|
+
|
|
35
|
+
- Улучшена валидация параметров и подстановка значений по умолчанию в командах `twc firewall`.
|
|
36
|
+
- Команда `twc firewall rule remove` теперь может принимать список UUID правил через пробел.
|
|
37
|
+
- В команде `twc firewall show` аргумент `all` стал необязательным.
|
|
38
|
+
- Зависимость `typer` заменена на `typer-slim`.
|
|
39
|
+
|
|
40
|
+
## Исправлено
|
|
41
|
+
|
|
42
|
+
- Исправлено определение ендпоинта объектного хранилища при вызове команды `twc storage genconfig`.
|
|
43
|
+
|
|
5
44
|
# Версия 2.5.0 (2024.07.24)
|
|
6
45
|
|
|
7
46
|
## Добавлено
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: twc-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Timeweb Cloud Command Line Interface.
|
|
5
5
|
Home-page: https://github.com/timeweb-cloud/twc
|
|
6
6
|
License: MIT
|
|
@@ -19,23 +19,40 @@ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
|
19
19
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
20
20
|
Requires-Dist: shellingham (>=1.5.4,<2.0.0)
|
|
21
21
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
22
|
-
Requires-Dist: typer (>=0.12.3,<0.13.0)
|
|
22
|
+
Requires-Dist: typer-slim (>=0.12.3,<0.13.0)
|
|
23
23
|
Project-URL: Repository, https://github.com/timeweb-cloud/twc
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
|
-
|
|
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>
|
|
27
33
|
|
|
28
34
|
Timeweb Cloud Command Line Interface and simple SDK 💫
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
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)
|
|
32
38
|
|
|
33
39
|
# Installation
|
|
34
40
|
|
|
41
|
+
From PyPI registry via pip:
|
|
42
|
+
|
|
35
43
|
```
|
|
36
44
|
pip install twc-cli
|
|
37
45
|
```
|
|
38
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
|
+
|
|
39
56
|
# Getting started
|
|
40
57
|
|
|
41
58
|
Get Timeweb Cloud [access token](https://timeweb.cloud/my/api-keys) and
|
twc_cli-2.7.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.7.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"
|
|
@@ -13,7 +13,7 @@ packages = [{ include = "twc", from = "." }]
|
|
|
13
13
|
[tool.poetry.dependencies]
|
|
14
14
|
python = "^3.8.19"
|
|
15
15
|
requests = "^2.32.3"
|
|
16
|
-
typer = "^0.12.3"
|
|
16
|
+
typer-slim = "^0.12.3"
|
|
17
17
|
shellingham = "^1.5.4"
|
|
18
18
|
colorama = "^0.4.6"
|
|
19
19
|
toml = "^0.10.2"
|
|
@@ -21,6 +21,7 @@ from .commands import (
|
|
|
21
21
|
vpc,
|
|
22
22
|
firewall,
|
|
23
23
|
floating_ip,
|
|
24
|
+
whoami,
|
|
24
25
|
)
|
|
25
26
|
from .commands.common import version_callback, version_option, verbose_option
|
|
26
27
|
|
|
@@ -46,6 +47,7 @@ cli.add_typer(domain, name="domain", aliases=["domains", "d"])
|
|
|
46
47
|
cli.add_typer(vpc, name="vpc", aliases=["vpcs", "network", "networks"])
|
|
47
48
|
cli.add_typer(firewall, name="firewall", aliases=["fw"])
|
|
48
49
|
cli.add_typer(floating_ip, name="ip", aliases=["ips"])
|
|
50
|
+
cli.add_typer(whoami, name="whoami", aliases=[])
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
@cli.command("version")
|
|
@@ -26,6 +26,7 @@ from .types import (
|
|
|
26
26
|
LoadBalancerAlgo,
|
|
27
27
|
FirewallProto,
|
|
28
28
|
FirewallDirection,
|
|
29
|
+
FirewallPolicy,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
|
|
@@ -468,14 +469,11 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
468
469
|
# -----------------------------------------------------------------------
|
|
469
470
|
# Images
|
|
470
471
|
|
|
471
|
-
def get_images(
|
|
472
|
-
self, limit: int = 100, offset: int = 0, with_deleted: bool = False
|
|
473
|
-
):
|
|
472
|
+
def get_images(self, limit: int = 100, offset: int = 0):
|
|
474
473
|
"""Get list of images."""
|
|
475
474
|
params = {
|
|
476
475
|
"limit": limit,
|
|
477
476
|
"offset": offset,
|
|
478
|
-
"with_deleted": with_deleted,
|
|
479
477
|
}
|
|
480
478
|
return self._request("GET", f"{self.api_url}/images", params=params)
|
|
481
479
|
|
|
@@ -1415,6 +1413,8 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1415
1413
|
value: str,
|
|
1416
1414
|
subdomain: Optional[str] = None,
|
|
1417
1415
|
priority: Optional[int] = None,
|
|
1416
|
+
*,
|
|
1417
|
+
null_subdomain: bool = False,
|
|
1418
1418
|
):
|
|
1419
1419
|
"""Add DNS record to domain."""
|
|
1420
1420
|
payload = {
|
|
@@ -1423,6 +1423,8 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1423
1423
|
**({"subdomain": subdomain} if subdomain else {}),
|
|
1424
1424
|
**({"priority": priority} if priority else {}),
|
|
1425
1425
|
}
|
|
1426
|
+
if null_subdomain:
|
|
1427
|
+
payload["subdomain"] = None
|
|
1426
1428
|
return self._request(
|
|
1427
1429
|
"POST",
|
|
1428
1430
|
f"{self.api_url}/domains/{fqdn}/dns-records",
|
|
@@ -1549,14 +1551,20 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1549
1551
|
)
|
|
1550
1552
|
|
|
1551
1553
|
def create_firewall_group(
|
|
1552
|
-
self,
|
|
1554
|
+
self,
|
|
1555
|
+
name: str,
|
|
1556
|
+
description: Optional[str] = None,
|
|
1557
|
+
policy: Optional[FirewallPolicy] = FirewallPolicy.DROP,
|
|
1553
1558
|
):
|
|
1554
1559
|
payload = {
|
|
1555
1560
|
"name": name,
|
|
1556
1561
|
**({"description": description} if description else {}),
|
|
1557
1562
|
}
|
|
1558
1563
|
return self._request(
|
|
1559
|
-
"POST",
|
|
1564
|
+
"POST",
|
|
1565
|
+
f"{self.api_url}/firewall/groups",
|
|
1566
|
+
json=payload,
|
|
1567
|
+
params={"policy": policy},
|
|
1560
1568
|
)
|
|
1561
1569
|
|
|
1562
1570
|
def get_firewall_group(self, group_id: UUID):
|
|
@@ -1636,16 +1644,16 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1636
1644
|
self,
|
|
1637
1645
|
group_id: UUID,
|
|
1638
1646
|
direction: FirewallDirection,
|
|
1639
|
-
|
|
1647
|
+
protocol: FirewallProto,
|
|
1640
1648
|
cidr: Union[IPv4Network, IPv6Network],
|
|
1641
1649
|
port: Optional[str] = None,
|
|
1642
1650
|
description: Optional[str] = None,
|
|
1643
1651
|
):
|
|
1644
1652
|
payload = {
|
|
1645
1653
|
**({"description": description} if description else {}),
|
|
1646
|
-
**({} if
|
|
1654
|
+
**({} if protocol == FirewallProto.ICMP.value else {"port": port}),
|
|
1647
1655
|
"direction": direction,
|
|
1648
|
-
"protocol":
|
|
1656
|
+
"protocol": protocol,
|
|
1649
1657
|
"cidr": cidr,
|
|
1650
1658
|
}
|
|
1651
1659
|
return self._request(
|
|
@@ -1670,16 +1678,16 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1670
1678
|
group_id: UUID,
|
|
1671
1679
|
rule_id: UUID,
|
|
1672
1680
|
direction: FirewallDirection,
|
|
1673
|
-
|
|
1681
|
+
protocol: FirewallProto,
|
|
1674
1682
|
cidr: Union[IPv4Network, IPv6Network],
|
|
1675
1683
|
port: Optional[str] = None,
|
|
1676
1684
|
description: Optional[str] = None,
|
|
1677
1685
|
):
|
|
1678
1686
|
payload = {
|
|
1679
1687
|
**({"description": description} if description else {}),
|
|
1680
|
-
**({} if
|
|
1688
|
+
**({} if protocol == FirewallProto.ICMP.value else {"port": port}),
|
|
1681
1689
|
"direction": direction,
|
|
1682
|
-
"protocol":
|
|
1690
|
+
"protocol": protocol,
|
|
1683
1691
|
"cidr": cidr,
|
|
1684
1692
|
}
|
|
1685
1693
|
return self._request(
|
|
@@ -228,6 +228,9 @@ class FirewallProto(str, Enum):
|
|
|
228
228
|
TCP = "tcp"
|
|
229
229
|
UDP = "udp"
|
|
230
230
|
ICMP = "icmp"
|
|
231
|
+
TCP6 = "tcp6"
|
|
232
|
+
UDP6 = "udp6"
|
|
233
|
+
ICMP6 = "icmp6"
|
|
231
234
|
|
|
232
235
|
|
|
233
236
|
class FirewallDirection(str, Enum):
|
|
@@ -235,3 +238,10 @@ class FirewallDirection(str, Enum):
|
|
|
235
238
|
|
|
236
239
|
INGRESS = "ingress"
|
|
237
240
|
EGRESS = "egress"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class FirewallPolicy(str, Enum):
|
|
244
|
+
"""Firewall default policy."""
|
|
245
|
+
|
|
246
|
+
DROP = "DROP"
|
|
247
|
+
ACCEPT = "ACCEPT"
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
import textwrap
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from logging import debug
|
|
7
|
+
from logging import debug, warning
|
|
8
8
|
|
|
9
9
|
from .api import TimewebCloud
|
|
10
10
|
from .api import exceptions as exc
|
|
@@ -70,6 +70,11 @@ def create_client(config: Path, profile: str, **kwargs) -> TimewebCloud:
|
|
|
70
70
|
"""
|
|
71
71
|
token = os.getenv("TWC_TOKEN")
|
|
72
72
|
log_settings = os.getenv("TWC_LOG")
|
|
73
|
+
api_endpoint = os.getenv("TWC_ENDPOINT")
|
|
74
|
+
|
|
75
|
+
if api_endpoint:
|
|
76
|
+
warning("Using API URL from environment: %s", api_endpoint)
|
|
77
|
+
kwargs["api_base_url"] = api_endpoint
|
|
73
78
|
|
|
74
79
|
if log_settings:
|
|
75
80
|
for param in log_settings.split(","):
|
|
@@ -18,6 +18,35 @@ from .common import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
whoami = TyperAlias(help="Display current login.", no_args_is_help=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------- #
|
|
25
|
+
# $ twc whoami #
|
|
26
|
+
# ------------------------------------------------------------- #
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@whoami.callback(invoke_without_command=True)
|
|
30
|
+
def whoami_callback(
|
|
31
|
+
verbose: Optional[bool] = verbose_option,
|
|
32
|
+
config: Optional[Path] = config_option,
|
|
33
|
+
profile: Optional[str] = profile_option,
|
|
34
|
+
output_format: Optional[str] = output_format_option,
|
|
35
|
+
):
|
|
36
|
+
"""Display current login."""
|
|
37
|
+
client = create_client(config, profile)
|
|
38
|
+
response = client.get_account_status()
|
|
39
|
+
login = response.json()["status"]["login"]
|
|
40
|
+
fmt.printer(
|
|
41
|
+
{"login": login, "profile": profile},
|
|
42
|
+
output_format=output_format,
|
|
43
|
+
func=lambda data: print(data["login"]),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------- #
|
|
48
|
+
|
|
49
|
+
|
|
21
50
|
account = TyperAlias(help=__doc__)
|
|
22
51
|
account_access = TyperAlias(help="Manage account access restrictions.")
|
|
23
52
|
account.add_typer(account_access, name="access")
|
|
@@ -33,6 +62,7 @@ def print_account_status(response: Response):
|
|
|
33
62
|
status = response.json()["status"]
|
|
34
63
|
output = dedent(
|
|
35
64
|
f"""
|
|
65
|
+
Login: {status["login"]}
|
|
36
66
|
Provider: {status["company_info"]["name"]}
|
|
37
67
|
Yandex.Metrika ID: {status["ym_client_id"]}
|
|
38
68
|
Blocked: {status["is_blocked"]}
|
|
@@ -195,7 +195,7 @@ def domain_delete(
|
|
|
195
195
|
# ------------------------------------------------------------- #
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
@domain.command("add")
|
|
198
|
+
@domain.command("add", "create")
|
|
199
199
|
def domain_add(
|
|
200
200
|
domain_name: str,
|
|
201
201
|
verbose: Optional[bool] = verbose_option,
|
|
@@ -316,7 +316,7 @@ def domain_remove_dns_record(
|
|
|
316
316
|
# ------------------------------------------------------------- #
|
|
317
317
|
|
|
318
318
|
|
|
319
|
-
@domain_record.command("add")
|
|
319
|
+
@domain_record.command("add", "create")
|
|
320
320
|
def domain_add_dns_record(
|
|
321
321
|
domain_name: str,
|
|
322
322
|
verbose: Optional[bool] = verbose_option,
|
|
@@ -346,29 +346,39 @@ def domain_add_dns_record(
|
|
|
346
346
|
"""Add dns record for domain or subdomain."""
|
|
347
347
|
client = create_client(config, profile)
|
|
348
348
|
|
|
349
|
+
null_subdomain = False
|
|
350
|
+
|
|
349
351
|
if second_ld:
|
|
350
352
|
offset = 3
|
|
351
353
|
else:
|
|
352
354
|
offset = 2
|
|
353
355
|
|
|
354
356
|
subdomain = domain_name
|
|
357
|
+
original_domain_name = domain_name
|
|
355
358
|
domain_name = ".".join(domain_name.split(".")[-offset:])
|
|
356
359
|
|
|
357
360
|
if subdomain == domain_name:
|
|
358
361
|
subdomain = None
|
|
359
362
|
|
|
360
|
-
# API issue: see text below
|
|
361
|
-
# API can add TXT record (only TXT, why?) with non-existent subdomain,
|
|
362
|
-
# but 'subdomain' option must not be passed as FQDN!
|
|
363
|
-
# API issue: You cannot create subdomains with underscore. Why?
|
|
364
|
-
# Use previous described bug for this! Pass your subdomain with
|
|
365
|
-
# underscores to this function.
|
|
366
363
|
if record_type.lower() == "txt":
|
|
367
|
-
|
|
368
|
-
|
|
364
|
+
if subdomain is None:
|
|
365
|
+
null_subdomain = True
|
|
366
|
+
else:
|
|
367
|
+
# 'ftp.example.org' --> 'ftp'
|
|
368
|
+
subdomain = ".".join(subdomain.split(".")[:-offset])
|
|
369
|
+
else:
|
|
370
|
+
subdomain = ".".join(original_domain_name.split(".")[:-offset])
|
|
371
|
+
if subdomain != "":
|
|
372
|
+
domain_name = original_domain_name
|
|
373
|
+
subdomain = None
|
|
369
374
|
|
|
370
375
|
response = client.add_domain_dns_record(
|
|
371
|
-
domain_name,
|
|
376
|
+
domain_name,
|
|
377
|
+
record_type,
|
|
378
|
+
value,
|
|
379
|
+
subdomain,
|
|
380
|
+
priority,
|
|
381
|
+
null_subdomain=null_subdomain,
|
|
372
382
|
)
|
|
373
383
|
fmt.printer(
|
|
374
384
|
response,
|
|
@@ -424,6 +434,12 @@ def domain_update_dns_records(
|
|
|
424
434
|
if subdomain == domain_name:
|
|
425
435
|
subdomain = None
|
|
426
436
|
|
|
437
|
+
if record_type.lower() == "txt" and subdomain is not None:
|
|
438
|
+
subdomain = ".".join(subdomain.split(".")[:-offset])
|
|
439
|
+
elif subdomain is not None:
|
|
440
|
+
domain_name = subdomain
|
|
441
|
+
subdomain = None
|
|
442
|
+
|
|
427
443
|
response = client.update_domain_dns_record(
|
|
428
444
|
domain_name, record_id, record_type, value, subdomain, priority
|
|
429
445
|
)
|
|
@@ -439,7 +455,7 @@ def domain_update_dns_records(
|
|
|
439
455
|
# ------------------------------------------------------------- #
|
|
440
456
|
|
|
441
457
|
|
|
442
|
-
@domain_subdomain.command("add")
|
|
458
|
+
@domain_subdomain.command("add", "create")
|
|
443
459
|
def domain_add_subdomain(
|
|
444
460
|
subdomain: str = typer.Argument(..., metavar="FQDN"),
|
|
445
461
|
verbose: Optional[bool] = verbose_option,
|
|
@@ -18,7 +18,7 @@ import yaml
|
|
|
18
18
|
from requests import Response
|
|
19
19
|
|
|
20
20
|
from twc import fmt
|
|
21
|
-
from twc.api import TimewebCloud, FirewallProto
|
|
21
|
+
from twc.api import TimewebCloud, FirewallProto, FirewallPolicy
|
|
22
22
|
from twc.typerx import TyperAlias
|
|
23
23
|
from twc.apiwrap import create_client
|
|
24
24
|
from .common import (
|
|
@@ -73,7 +73,7 @@ def print_firewall_status(data: list):
|
|
|
73
73
|
rules_total += len(group["rules"])
|
|
74
74
|
print("Rules total:", rules_total)
|
|
75
75
|
for group in data:
|
|
76
|
-
info = f"Group: {group['name']} ({group['id']})"
|
|
76
|
+
info = f"Group: {group['name']} ({group['id']}) {group['policy']}"
|
|
77
77
|
for rule in group["rules"]:
|
|
78
78
|
info += "\n" + textwrap.indent(
|
|
79
79
|
textwrap.dedent(
|
|
@@ -140,7 +140,7 @@ def print_rules_by_service(rules, filters):
|
|
|
140
140
|
@firewall.command("show")
|
|
141
141
|
def firewall_status(
|
|
142
142
|
resource_type: _ResourceType2 = typer.Argument(
|
|
143
|
-
|
|
143
|
+
_ResourceType2.ALL,
|
|
144
144
|
metavar="(server|database|balancer|all)",
|
|
145
145
|
),
|
|
146
146
|
resource_id: str = typer.Argument(None),
|
|
@@ -184,6 +184,7 @@ def firewall_status(
|
|
|
184
184
|
{
|
|
185
185
|
"id": group["id"],
|
|
186
186
|
"name": group["name"],
|
|
187
|
+
"policy": group["policy"],
|
|
187
188
|
"rules": rules,
|
|
188
189
|
"resources": resources,
|
|
189
190
|
}
|
|
@@ -212,18 +213,19 @@ def firewall_status(
|
|
|
212
213
|
def print_firewall_groups(response: Response):
|
|
213
214
|
groups = response.json()["groups"]
|
|
214
215
|
table = fmt.Table()
|
|
215
|
-
table.header(["ID", "NAME"])
|
|
216
|
+
table.header(["ID", "POLICY", "NAME"])
|
|
216
217
|
for group in groups:
|
|
217
218
|
table.row(
|
|
218
219
|
[
|
|
219
220
|
group["id"],
|
|
221
|
+
group["policy"],
|
|
220
222
|
group["name"],
|
|
221
223
|
]
|
|
222
224
|
)
|
|
223
225
|
table.print()
|
|
224
226
|
|
|
225
227
|
|
|
226
|
-
@firewall_group.command("list")
|
|
228
|
+
@firewall_group.command("list", "ls")
|
|
227
229
|
def firewall_group_list(
|
|
228
230
|
verbose: Optional[bool] = verbose_option,
|
|
229
231
|
config: Optional[Path] = config_option,
|
|
@@ -240,6 +242,43 @@ def firewall_group_list(
|
|
|
240
242
|
)
|
|
241
243
|
|
|
242
244
|
|
|
245
|
+
# ------------------------------------------------------------- #
|
|
246
|
+
# $ twc firewall group get #
|
|
247
|
+
# ------------------------------------------------------------- #
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def print_firewall_group(response: Response):
|
|
251
|
+
group = response.json()["group"]
|
|
252
|
+
table = fmt.Table()
|
|
253
|
+
table.header(["ID", "POLICY", "NAME"])
|
|
254
|
+
table.row(
|
|
255
|
+
[
|
|
256
|
+
group["id"],
|
|
257
|
+
group["policy"],
|
|
258
|
+
group["name"],
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
table.print()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@firewall_group.command("get")
|
|
265
|
+
def firewall_group_get(
|
|
266
|
+
group_id: UUID,
|
|
267
|
+
verbose: Optional[bool] = verbose_option,
|
|
268
|
+
config: Optional[Path] = config_option,
|
|
269
|
+
profile: Optional[str] = profile_option,
|
|
270
|
+
output_format: Optional[str] = output_format_option,
|
|
271
|
+
):
|
|
272
|
+
"""Get firewall fules group."""
|
|
273
|
+
client = create_client(config, profile)
|
|
274
|
+
response = client.get_firewall_group(group_id)
|
|
275
|
+
fmt.printer(
|
|
276
|
+
response,
|
|
277
|
+
output_format=output_format,
|
|
278
|
+
func=print_firewall_group,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
243
282
|
# ------------------------------------------------------------- #
|
|
244
283
|
# $ twc firewall group create #
|
|
245
284
|
# ------------------------------------------------------------- #
|
|
@@ -253,12 +292,18 @@ def firewall_group_create(
|
|
|
253
292
|
output_format: Optional[str] = output_format_option,
|
|
254
293
|
name: str = typer.Option(..., help="Group display name."),
|
|
255
294
|
desc: Optional[str] = typer.Option(None, help="Description."),
|
|
295
|
+
policy: Optional[FirewallPolicy] = typer.Option(
|
|
296
|
+
FirewallPolicy.DROP,
|
|
297
|
+
case_sensitive=False,
|
|
298
|
+
help="Default firewall policy",
|
|
299
|
+
),
|
|
256
300
|
):
|
|
257
301
|
"""Create new group of firewall rules."""
|
|
258
302
|
client = create_client(config, profile)
|
|
259
303
|
response = client.create_firewall_group(
|
|
260
304
|
name=name,
|
|
261
305
|
description=desc,
|
|
306
|
+
policy=policy,
|
|
262
307
|
)
|
|
263
308
|
fmt.printer(
|
|
264
309
|
response,
|
|
@@ -324,6 +369,149 @@ def firewall_group_set(
|
|
|
324
369
|
)
|
|
325
370
|
|
|
326
371
|
|
|
372
|
+
# ------------------------------------------------------------- #
|
|
373
|
+
# $ twc firewall group dump #
|
|
374
|
+
# ------------------------------------------------------------- #
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@firewall_group.command("dump")
|
|
378
|
+
def firewall_group_dump(
|
|
379
|
+
group_id: UUID,
|
|
380
|
+
verbose: Optional[bool] = verbose_option,
|
|
381
|
+
config: Optional[Path] = config_option,
|
|
382
|
+
profile: Optional[str] = profile_option,
|
|
383
|
+
):
|
|
384
|
+
"""Dump firewall rules."""
|
|
385
|
+
client = create_client(config, profile)
|
|
386
|
+
group = client.get_firewall_group(group_id).json()["group"]
|
|
387
|
+
rules = client.get_firewall_rules(group_id, limit=1000).json()["rules"]
|
|
388
|
+
dump = {"group": group, "rules": rules}
|
|
389
|
+
fmt.print_colored(json.dumps(dump), lang="json")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ------------------------------------------------------------- #
|
|
393
|
+
# $ twc firewall group restore #
|
|
394
|
+
# ------------------------------------------------------------- #
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _get_rules_diff(
|
|
398
|
+
rules_local: List[dict], rules_remote: List[dict]
|
|
399
|
+
) -> Tuple[List[dict], List[dict]]:
|
|
400
|
+
loc = [rule.copy() for rule in rules_local]
|
|
401
|
+
rem = [rule.copy() for rule in rules_remote]
|
|
402
|
+
|
|
403
|
+
for l in loc:
|
|
404
|
+
del l["id"]
|
|
405
|
+
del l["group_id"]
|
|
406
|
+
for r in rem:
|
|
407
|
+
del r["id"]
|
|
408
|
+
del r["group_id"]
|
|
409
|
+
|
|
410
|
+
# Rules from rules_local that not present in rules_remote
|
|
411
|
+
to_create = []
|
|
412
|
+
for idx, rule in enumerate(loc):
|
|
413
|
+
if rule not in rem:
|
|
414
|
+
to_create.append(rules_local[idx])
|
|
415
|
+
|
|
416
|
+
# Rules from rules_remote that not present in rules_local
|
|
417
|
+
to_delete = []
|
|
418
|
+
for idx, rule in enumerate(rem):
|
|
419
|
+
if rule not in loc:
|
|
420
|
+
to_delete.append(rules_remote[idx])
|
|
421
|
+
|
|
422
|
+
return to_create, to_delete
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@firewall_group.command("restore")
|
|
426
|
+
def firewall_group_restore(
|
|
427
|
+
group_id: UUID,
|
|
428
|
+
verbose: Optional[bool] = verbose_option,
|
|
429
|
+
config: Optional[Path] = config_option,
|
|
430
|
+
profile: Optional[str] = profile_option,
|
|
431
|
+
output_format: Optional[str] = output_format_option,
|
|
432
|
+
dump_file: Optional[typer.FileText] = typer.Option(
|
|
433
|
+
None, "-f", "--file", help="Firewall rules dump in JSON format."
|
|
434
|
+
),
|
|
435
|
+
rules_only: Optional[bool] = typer.Option(
|
|
436
|
+
False,
|
|
437
|
+
"--rules-only",
|
|
438
|
+
help="Do not restore group name and description.",
|
|
439
|
+
),
|
|
440
|
+
dry_run: Optional[bool] = typer.Option(
|
|
441
|
+
False, "--dry-run", help="Does not make any changes."
|
|
442
|
+
),
|
|
443
|
+
):
|
|
444
|
+
"""Restore firewall rules group from dump file."""
|
|
445
|
+
try:
|
|
446
|
+
dump = json.load(dump_file)
|
|
447
|
+
except json.JSONDecodeError as e:
|
|
448
|
+
sys.exit(f"Error: Cannot load dump file: {dump_file.name}: {e}")
|
|
449
|
+
|
|
450
|
+
client = create_client(config, profile)
|
|
451
|
+
group = client.get_firewall_group(group_id).json()["group"]
|
|
452
|
+
rules = client.get_firewall_rules(group_id, limit=1000).json()["rules"]
|
|
453
|
+
if group["policy"].lower() != dump["group"]["policy"].lower():
|
|
454
|
+
sys.exit(
|
|
455
|
+
f"Error: Cannot restore rules to group with {group['policy']} policy. "
|
|
456
|
+
"Create new rules group instead."
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Make list of rules to be created, updated or deleted
|
|
460
|
+
# fmt: off
|
|
461
|
+
rules_to_add, rules_to_del = _get_rules_diff(dump['rules'], rules)
|
|
462
|
+
rules_to_upd = [r for r in rules_to_add if r['id'] in [r['id'] for r in rules]]
|
|
463
|
+
rules_to_add = [r for r in rules_to_add if r not in rules_to_upd]
|
|
464
|
+
rules_to_del = [r for r in rules_to_del if r['id'] not in [r['id'] for r in rules_to_upd]]
|
|
465
|
+
# fmt: on
|
|
466
|
+
|
|
467
|
+
if rules_to_add == rules_to_upd == rules_to_del == []:
|
|
468
|
+
sys.exit("Nothing to do")
|
|
469
|
+
|
|
470
|
+
if dry_run:
|
|
471
|
+
fstring = "{sign} {id:<37} {direction:<8} {portproto:<18} {cidr}"
|
|
472
|
+
rules_lists = [
|
|
473
|
+
(rules_to_add, "Following new rules will be created:", "+"),
|
|
474
|
+
(rules_to_upd, "Following rules will be updated:", "+"),
|
|
475
|
+
(rules_to_del, "Following rules will be deleted:", "-"),
|
|
476
|
+
]
|
|
477
|
+
for rules_list in rules_lists:
|
|
478
|
+
if rules_list[0]:
|
|
479
|
+
print(rules_list[1])
|
|
480
|
+
for rule in rules_list[0]:
|
|
481
|
+
rule_id = rule["id"]
|
|
482
|
+
if rules_list == rules_lists[0]:
|
|
483
|
+
rule_id = "known-after-create"
|
|
484
|
+
print(
|
|
485
|
+
" "
|
|
486
|
+
+ fstring.format(
|
|
487
|
+
sign=rules_list[2],
|
|
488
|
+
id=rule_id,
|
|
489
|
+
direction=rule["direction"],
|
|
490
|
+
portproto=f"{rule['port']}/{rule['protocol']}",
|
|
491
|
+
cidr=rule["cidr"],
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
if rules_only is False and dry_run is False:
|
|
497
|
+
client.update_firewall_group(
|
|
498
|
+
group_id,
|
|
499
|
+
name=dump["group"]["name"],
|
|
500
|
+
description=dump["group"]["description"],
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
for rule in rules_to_add:
|
|
504
|
+
del rule["id"]
|
|
505
|
+
del rule["group_id"]
|
|
506
|
+
client.create_firewall_rule(group_id, **rule)
|
|
507
|
+
|
|
508
|
+
for rule in rules_to_upd:
|
|
509
|
+
client.update_firewall_rule(**rule)
|
|
510
|
+
|
|
511
|
+
for rule in rules_to_del:
|
|
512
|
+
client.delete_firewall_rule(group_id, rule["id"])
|
|
513
|
+
|
|
514
|
+
|
|
327
515
|
# ------------------------------------------------------------- #
|
|
328
516
|
# $ twc firewall link #
|
|
329
517
|
# ------------------------------------------------------------- #
|
|
@@ -460,16 +648,17 @@ def filrewall_rule_list(
|
|
|
460
648
|
def port_proto_callback(values) -> List[Tuple[Optional[str], str]]:
|
|
461
649
|
new_values = []
|
|
462
650
|
for value in values:
|
|
463
|
-
if not re.match(r"((^\d+(-\d+)
|
|
651
|
+
if not re.match(r"((^\d+(-\d+)?\/)?((tcp|udp|icmp)6?)$)", value, re.I):
|
|
464
652
|
sys.exit(
|
|
465
653
|
f"Error: Malformed argument: '{value}': "
|
|
466
654
|
"correct patterns: '22/TCP', '2000-3000/UDP', 'ICMP', etc."
|
|
467
655
|
)
|
|
468
|
-
|
|
469
|
-
|
|
656
|
+
pair = value.split("/")
|
|
657
|
+
if len(pair) == 1:
|
|
658
|
+
ports, proto = None, pair[0]
|
|
470
659
|
else:
|
|
471
|
-
ports, proto =
|
|
472
|
-
|
|
660
|
+
ports, proto = pair
|
|
661
|
+
new_values.append((ports, proto.lower()))
|
|
473
662
|
return new_values
|
|
474
663
|
|
|
475
664
|
|
|
@@ -510,14 +699,19 @@ def firewall_rule_create(
|
|
|
510
699
|
None,
|
|
511
700
|
help="Rules group name, can be used with '--make-group'",
|
|
512
701
|
),
|
|
702
|
+
group_policy: Optional[FirewallPolicy] = typer.Option(
|
|
703
|
+
FirewallPolicy.DROP,
|
|
704
|
+
case_sensitive=False,
|
|
705
|
+
help="Default firewall policy, can be used with '--make-group'",
|
|
706
|
+
),
|
|
513
707
|
direction_: bool = typer.Option(
|
|
514
708
|
True, "--ingress/--egress", help="Traffic direction."
|
|
515
709
|
),
|
|
516
710
|
cidr: Optional[str] = typer.Option(
|
|
517
|
-
|
|
711
|
+
None,
|
|
518
712
|
metavar="IP_NETWORK",
|
|
519
713
|
callback=validate_cidr_callback,
|
|
520
|
-
help="IPv4 or IPv6 CIDR.",
|
|
714
|
+
help="IPv4 or IPv6 CIDR. [default: 0.0.0.0/0 or ::/0]",
|
|
521
715
|
),
|
|
522
716
|
):
|
|
523
717
|
"""Create new firewall rule."""
|
|
@@ -535,7 +729,9 @@ def firewall_rule_create(
|
|
|
535
729
|
group_name = "Firewall Group " + datetime.now().strftime(
|
|
536
730
|
"%Y.%m.%d-%H:%M:%S"
|
|
537
731
|
)
|
|
538
|
-
group_resp = client.create_firewall_group(
|
|
732
|
+
group_resp = client.create_firewall_group(
|
|
733
|
+
group_name, policy=group_policy
|
|
734
|
+
)
|
|
539
735
|
group = group_resp.json()["group"]["id"]
|
|
540
736
|
logging.debug("New firewall rules group: %s", group)
|
|
541
737
|
fmt.printer(
|
|
@@ -543,7 +739,17 @@ def firewall_rule_create(
|
|
|
543
739
|
output_format=output_format,
|
|
544
740
|
func=lambda x: print("Created rules group:", group),
|
|
545
741
|
)
|
|
546
|
-
for
|
|
742
|
+
for rule in ports:
|
|
743
|
+
port, proto = rule
|
|
744
|
+
if not cidr:
|
|
745
|
+
if proto in [
|
|
746
|
+
FirewallProto.TCP6,
|
|
747
|
+
FirewallProto.UDP6,
|
|
748
|
+
FirewallProto.ICMP6,
|
|
749
|
+
]:
|
|
750
|
+
cidr = "::/0"
|
|
751
|
+
else:
|
|
752
|
+
cidr = "0.0.0.0/0"
|
|
547
753
|
if direction_ is True:
|
|
548
754
|
direction = "ingress"
|
|
549
755
|
else:
|
|
@@ -551,8 +757,8 @@ def firewall_rule_create(
|
|
|
551
757
|
response = client.create_firewall_rule(
|
|
552
758
|
group,
|
|
553
759
|
direction=direction,
|
|
554
|
-
port=port
|
|
555
|
-
proto
|
|
760
|
+
port=port,
|
|
761
|
+
protocol=proto,
|
|
556
762
|
cidr=cidr,
|
|
557
763
|
)
|
|
558
764
|
fmt.printer(
|
|
@@ -579,19 +785,20 @@ def get_group_id_by_rule(client: TimewebCloud, rule_id: UUID) -> str:
|
|
|
579
785
|
|
|
580
786
|
@firewall_rule.command("remove", "rm")
|
|
581
787
|
def firewall_rule_remove(
|
|
582
|
-
|
|
788
|
+
rules_ids: List[UUID] = typer.Argument(..., metavar="RULE_ID..."),
|
|
583
789
|
verbose: Optional[bool] = verbose_option,
|
|
584
790
|
config: Optional[Path] = config_option,
|
|
585
791
|
profile: Optional[str] = profile_option,
|
|
586
792
|
):
|
|
587
793
|
"""Remove firewall rule."""
|
|
588
794
|
client = create_client(config, profile)
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
795
|
+
for rule_id in rules_ids:
|
|
796
|
+
group_id = get_group_id_by_rule(client, rule_id)
|
|
797
|
+
response = client.delete_firewall_rule(group_id, rule_id)
|
|
798
|
+
if response.status_code == 204:
|
|
799
|
+
print(rule_id)
|
|
800
|
+
else:
|
|
801
|
+
sys.exit(fmt.printer(response))
|
|
595
802
|
|
|
596
803
|
|
|
597
804
|
# ------------------------------------------------------------- #
|
|
@@ -622,7 +829,9 @@ def filrewa_rule_update(
|
|
|
622
829
|
metavar="PORT[-PORT]",
|
|
623
830
|
help="Port or ports range e.g. 22, 2000-3000",
|
|
624
831
|
),
|
|
625
|
-
proto: Optional[FirewallProto] = typer.Option(
|
|
832
|
+
proto: Optional[FirewallProto] = typer.Option(
|
|
833
|
+
None, case_sensitive=False, help="Protocol."
|
|
834
|
+
),
|
|
626
835
|
):
|
|
627
836
|
"""Change firewall rule."""
|
|
628
837
|
client = create_client(config, profile)
|
|
@@ -643,7 +852,7 @@ def filrewa_rule_update(
|
|
|
643
852
|
"rule_id": rule_id,
|
|
644
853
|
"direction": direction,
|
|
645
854
|
"port": port,
|
|
646
|
-
"
|
|
855
|
+
"protocol": proto,
|
|
647
856
|
"cidr": cidr,
|
|
648
857
|
}
|
|
649
858
|
response = client.update_firewall_rule(**payload)
|
|
@@ -188,17 +188,11 @@ def print_resources(response: Response):
|
|
|
188
188
|
for key in resource_keys:
|
|
189
189
|
if resources[key]:
|
|
190
190
|
for resource in resources[key]:
|
|
191
|
-
try:
|
|
192
|
-
location = resource["location"]
|
|
193
|
-
except KeyError:
|
|
194
|
-
# Balancers, clusters, and databases has no 'location' field.
|
|
195
|
-
# These services is available only in 'ru-1' location.
|
|
196
|
-
location = "ru-1"
|
|
197
191
|
table.row(
|
|
198
192
|
[
|
|
199
193
|
resource["id"],
|
|
200
|
-
resource
|
|
201
|
-
location,
|
|
194
|
+
resource.get("name", resource.get("fqdn")),
|
|
195
|
+
resource.get("location", "ru-1"),
|
|
202
196
|
key[:-1], # this is resource name e.g. 'server'
|
|
203
197
|
]
|
|
204
198
|
)
|
|
@@ -584,9 +584,7 @@ def server_create(
|
|
|
584
584
|
net = IPv4Network(
|
|
585
585
|
client.get_vpc(network).json()["vpc"]["subnet_v4"]
|
|
586
586
|
)
|
|
587
|
-
if IPv4Address(private_ip)
|
|
588
|
-
int(net.network_address) + 4
|
|
589
|
-
):
|
|
587
|
+
if IPv4Address(private_ip) >= net.network_address + 4:
|
|
590
588
|
payload["network"]["ip"] = private_ip
|
|
591
589
|
else:
|
|
592
590
|
# First 3 addresses is reserved for networks OVN based networks
|
|
@@ -18,7 +18,6 @@ from twc import fmt
|
|
|
18
18
|
from twc.typerx import TyperAlias
|
|
19
19
|
from twc.apiwrap import create_client
|
|
20
20
|
from twc.api import TimewebCloud, ServiceRegion, BucketType
|
|
21
|
-
from twc.vars import S3_ENDPOINT
|
|
22
21
|
from .common import (
|
|
23
22
|
verbose_option,
|
|
24
23
|
config_option,
|
|
@@ -501,7 +500,6 @@ def storage_subdomain_remove(
|
|
|
501
500
|
verbose: Optional[bool] = verbose_option,
|
|
502
501
|
config: Optional[Path] = config_option,
|
|
503
502
|
profile: Optional[str] = profile_option,
|
|
504
|
-
output_format: Optional[str] = output_format_option,
|
|
505
503
|
yes: Optional[bool] = yes_option,
|
|
506
504
|
):
|
|
507
505
|
"""Remove subdomains."""
|
|
@@ -510,11 +508,11 @@ def storage_subdomain_remove(
|
|
|
510
508
|
client = create_client(config, profile)
|
|
511
509
|
bucket_id = resolve_bucket_id(client, bucket)
|
|
512
510
|
response = client.delete_bucket_subdomains(bucket_id, subdomains)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
511
|
+
if response.status_code == 204:
|
|
512
|
+
for subdomain in subdomains:
|
|
513
|
+
print(subdomain)
|
|
514
|
+
else:
|
|
515
|
+
sys.exit(fmt.printer(response))
|
|
518
516
|
|
|
519
517
|
|
|
520
518
|
# ------------------------------------------------------------- #
|
|
@@ -533,7 +531,7 @@ def storage_subdomain_gencert(
|
|
|
533
531
|
client = create_client(config, profile)
|
|
534
532
|
for subdomain in subdomains:
|
|
535
533
|
response = client.gen_cert_for_bucket_subdomain(subdomain)
|
|
536
|
-
if response.status_code ==
|
|
534
|
+
if response.status_code == 204:
|
|
537
535
|
print(subdomain)
|
|
538
536
|
else:
|
|
539
537
|
sys.exit(fmt.printer(response))
|
|
@@ -613,10 +611,16 @@ def storage_genconfig(
|
|
|
613
611
|
"rclone": RCLONE_CONFIG_TEMPLATE.strip(),
|
|
614
612
|
}
|
|
615
613
|
|
|
614
|
+
endpoint = "s3.timeweb.cloud"
|
|
615
|
+
if not access_key.isupper():
|
|
616
|
+
# Legacy object storage service have lowercase usernames only.
|
|
617
|
+
# New storage, on the contrary, always has keys in uppercase.
|
|
618
|
+
endpoint = "s3.timeweb.com"
|
|
619
|
+
|
|
616
620
|
file_content = templates[s3_client].format(
|
|
617
621
|
access_key=access_key,
|
|
618
622
|
secret_key=secret_key,
|
|
619
|
-
endpoint=
|
|
623
|
+
endpoint=endpoint,
|
|
620
624
|
)
|
|
621
625
|
|
|
622
626
|
if save_to:
|
|
@@ -67,6 +67,9 @@ class Printer:
|
|
|
67
67
|
|
|
68
68
|
def raw(self):
|
|
69
69
|
"""Print raw API response text (mostly raw JSON)."""
|
|
70
|
+
if isinstance(self._data, dict):
|
|
71
|
+
typer.echo(json.dumps(self._data))
|
|
72
|
+
return
|
|
70
73
|
typer.echo(self._data.text)
|
|
71
74
|
|
|
72
75
|
def colorize(self, data: str, lexer: object = JsonLexer()):
|
|
@@ -77,9 +80,12 @@ class Printer:
|
|
|
77
80
|
"""Print colorized JSON output. Fallback to non-color output if
|
|
78
81
|
Pygments not installed and fallback to raw output on JSONDecodeError.
|
|
79
82
|
"""
|
|
83
|
+
data = self._data
|
|
84
|
+
if not isinstance(self._data, dict):
|
|
85
|
+
data = self._data.json()
|
|
80
86
|
try:
|
|
81
87
|
json_data = json.dumps(
|
|
82
|
-
|
|
88
|
+
data, indent=4, sort_keys=True, ensure_ascii=False
|
|
83
89
|
)
|
|
84
90
|
self.colorize(json_data, lexer=JsonLexer())
|
|
85
91
|
except json.JSONDecodeError:
|
|
@@ -89,10 +95,11 @@ class Printer:
|
|
|
89
95
|
"""Print colorized YAML output. Fallback to non-color output if
|
|
90
96
|
Pygments not installed and fallback to raw output on YAMLError.
|
|
91
97
|
"""
|
|
98
|
+
data = self._data
|
|
99
|
+
if not isinstance(self._data, dict):
|
|
100
|
+
data = self._data.json()
|
|
92
101
|
try:
|
|
93
|
-
yaml_data = yaml.dump(
|
|
94
|
-
self._data.json(), sort_keys=True, allow_unicode=True
|
|
95
|
-
)
|
|
102
|
+
yaml_data = yaml.dump(data, sort_keys=True, allow_unicode=True)
|
|
96
103
|
self.colorize(yaml_data, lexer=YamlLexer())
|
|
97
104
|
except yaml.YAMLError:
|
|
98
105
|
self.raw()
|
|
@@ -6,11 +6,9 @@ expand or other infrastructure or product changes occur.
|
|
|
6
6
|
|
|
7
7
|
# Service URLs
|
|
8
8
|
CONTROL_PANEL_URL = "https://timeweb.cloud/my"
|
|
9
|
-
S3_ENDPOINT_DEPRECATED = "s3.timeweb.com"
|
|
10
|
-
S3_ENDPOINT = "s3.timeweb.cloud"
|
|
11
9
|
|
|
12
10
|
# Location specific parameters. May change later.
|
|
13
11
|
REGIONS_WITH_IPV6 = ["ru-1", "pl-1"]
|
|
14
12
|
REGIONS_WITH_IMAGES = ["ru-1", "ru-3", "kz-1", "pl-1", "nl-1"]
|
|
15
13
|
REGIONS_WITH_LAN = ["ru-1", "ru-3", "nl-1", "pl-1"]
|
|
16
|
-
ZONES_WITH_LAN = ["spb-1", "spb-4", "msk-1", "ams-1", "gdn-1"]
|
|
14
|
+
ZONES_WITH_LAN = ["spb-1", "spb-3", "spb-4", "msk-1", "ams-1", "gdn-1"]
|
twc_cli-2.5.0/README.md
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-

|
|
2
|
-
|
|
3
|
-
Timeweb Cloud Command Line Interface and simple SDK 💫
|
|
4
|
-
|
|
5
|
-
> [Руководство пользователя](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/README.md) 🇷🇺
|
|
6
|
-
> [Command Line Interface (CLI) Reference](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/CLI_REFERENCE.md) 📜
|
|
7
|
-
|
|
8
|
-
# Installation
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
pip install twc-cli
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
# Getting started
|
|
15
|
-
|
|
16
|
-
Get Timeweb Cloud [access token](https://timeweb.cloud/my/api-keys) and
|
|
17
|
-
configure **twc** with command:
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
twc config
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Enter your access token and hit `Enter`.
|
|
24
|
-
|
|
25
|
-
Configuration done! Let's use:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
twc --help
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
# Shell completion
|
|
32
|
-
|
|
33
|
-
To install completion script run:
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
twc --install-completion
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**twc** automatically detect your shell. Supported: Bash, Zsh, Fish, PowerShell.
|
|
40
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|