twc-cli 2.5.0__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.5.0 → twc_cli-2.6.0}/CHANGELOG.md +18 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/PKG-INFO +22 -5
- twc_cli-2.6.0/README.md +57 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/pyproject.toml +2 -2
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/__version__.py +1 -1
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/client.py +15 -8
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/types.py +10 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/firewall.py +234 -25
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/storage.py +7 -2
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/vars.py +0 -2
- twc_cli-2.5.0/README.md +0 -40
- {twc_cli-2.5.0 → twc_cli-2.6.0}/COPYING +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/__init__.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/__main__.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/__init__.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/base.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/exceptions.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/apiwrap.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/__init__.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/account.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/balancer.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/common.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/config.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/database.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/domain.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/floating_ip.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/image.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/kubernetes.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/project.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/server.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/ssh_key.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/vpc.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/fmt.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/typerx.py +0 -0
- {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/utils.py +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
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
|
+
|
|
5
23
|
# Версия 2.5.0 (2024.07.24)
|
|
6
24
|
|
|
7
25
|
## Добавлено
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: twc-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.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.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"
|
|
@@ -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"
|
|
@@ -26,6 +26,7 @@ from .types import (
|
|
|
26
26
|
LoadBalancerAlgo,
|
|
27
27
|
FirewallProto,
|
|
28
28
|
FirewallDirection,
|
|
29
|
+
FirewallPolicy,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
|
|
@@ -1549,14 +1550,20 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1549
1550
|
)
|
|
1550
1551
|
|
|
1551
1552
|
def create_firewall_group(
|
|
1552
|
-
self,
|
|
1553
|
+
self,
|
|
1554
|
+
name: str,
|
|
1555
|
+
description: Optional[str] = None,
|
|
1556
|
+
policy: Optional[FirewallPolicy] = FirewallPolicy.DROP,
|
|
1553
1557
|
):
|
|
1554
1558
|
payload = {
|
|
1555
1559
|
"name": name,
|
|
1556
1560
|
**({"description": description} if description else {}),
|
|
1557
1561
|
}
|
|
1558
1562
|
return self._request(
|
|
1559
|
-
"POST",
|
|
1563
|
+
"POST",
|
|
1564
|
+
f"{self.api_url}/firewall/groups",
|
|
1565
|
+
json=payload,
|
|
1566
|
+
params={"policy": policy},
|
|
1560
1567
|
)
|
|
1561
1568
|
|
|
1562
1569
|
def get_firewall_group(self, group_id: UUID):
|
|
@@ -1636,16 +1643,16 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1636
1643
|
self,
|
|
1637
1644
|
group_id: UUID,
|
|
1638
1645
|
direction: FirewallDirection,
|
|
1639
|
-
|
|
1646
|
+
protocol: FirewallProto,
|
|
1640
1647
|
cidr: Union[IPv4Network, IPv6Network],
|
|
1641
1648
|
port: Optional[str] = None,
|
|
1642
1649
|
description: Optional[str] = None,
|
|
1643
1650
|
):
|
|
1644
1651
|
payload = {
|
|
1645
1652
|
**({"description": description} if description else {}),
|
|
1646
|
-
**({} if
|
|
1653
|
+
**({} if protocol == FirewallProto.ICMP.value else {"port": port}),
|
|
1647
1654
|
"direction": direction,
|
|
1648
|
-
"protocol":
|
|
1655
|
+
"protocol": protocol,
|
|
1649
1656
|
"cidr": cidr,
|
|
1650
1657
|
}
|
|
1651
1658
|
return self._request(
|
|
@@ -1670,16 +1677,16 @@ class TimewebCloud(TimewebCloudBase):
|
|
|
1670
1677
|
group_id: UUID,
|
|
1671
1678
|
rule_id: UUID,
|
|
1672
1679
|
direction: FirewallDirection,
|
|
1673
|
-
|
|
1680
|
+
protocol: FirewallProto,
|
|
1674
1681
|
cidr: Union[IPv4Network, IPv6Network],
|
|
1675
1682
|
port: Optional[str] = None,
|
|
1676
1683
|
description: Optional[str] = None,
|
|
1677
1684
|
):
|
|
1678
1685
|
payload = {
|
|
1679
1686
|
**({"description": description} if description else {}),
|
|
1680
|
-
**({} if
|
|
1687
|
+
**({} if protocol == FirewallProto.ICMP.value else {"port": port}),
|
|
1681
1688
|
"direction": direction,
|
|
1682
|
-
"protocol":
|
|
1689
|
+
"protocol": protocol,
|
|
1683
1690
|
"cidr": cidr,
|
|
1684
1691
|
}
|
|
1685
1692
|
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"
|
|
@@ -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)
|
|
@@ -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,
|
|
@@ -613,10 +612,16 @@ def storage_genconfig(
|
|
|
613
612
|
"rclone": RCLONE_CONFIG_TEMPLATE.strip(),
|
|
614
613
|
}
|
|
615
614
|
|
|
615
|
+
endpoint = "s3.timeweb.cloud"
|
|
616
|
+
if not access_key.isupper():
|
|
617
|
+
# Legacy object storage service have lowercase usernames only.
|
|
618
|
+
# New storage, on the contrary, always has keys in uppercase.
|
|
619
|
+
endpoint = "s3.timeweb.com"
|
|
620
|
+
|
|
616
621
|
file_content = templates[s3_client].format(
|
|
617
622
|
access_key=access_key,
|
|
618
623
|
secret_key=secret_key,
|
|
619
|
-
endpoint=
|
|
624
|
+
endpoint=endpoint,
|
|
620
625
|
)
|
|
621
626
|
|
|
622
627
|
if save_to:
|
|
@@ -6,8 +6,6 @@ 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"]
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|