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.

Files changed (35) hide show
  1. {twc_cli-2.5.0 → twc_cli-2.6.0}/CHANGELOG.md +18 -0
  2. {twc_cli-2.5.0 → twc_cli-2.6.0}/PKG-INFO +22 -5
  3. twc_cli-2.6.0/README.md +57 -0
  4. {twc_cli-2.5.0 → twc_cli-2.6.0}/pyproject.toml +2 -2
  5. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/__version__.py +1 -1
  6. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/client.py +15 -8
  7. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/types.py +10 -0
  8. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/firewall.py +234 -25
  9. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/storage.py +7 -2
  10. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/vars.py +0 -2
  11. twc_cli-2.5.0/README.md +0 -40
  12. {twc_cli-2.5.0 → twc_cli-2.6.0}/COPYING +0 -0
  13. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/__init__.py +0 -0
  14. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/__main__.py +0 -0
  15. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/__init__.py +0 -0
  16. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/base.py +0 -0
  17. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/api/exceptions.py +0 -0
  18. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/apiwrap.py +0 -0
  19. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/__init__.py +0 -0
  20. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/account.py +0 -0
  21. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/balancer.py +0 -0
  22. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/common.py +0 -0
  23. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/config.py +0 -0
  24. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/database.py +0 -0
  25. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/domain.py +0 -0
  26. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/floating_ip.py +0 -0
  27. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/image.py +0 -0
  28. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/kubernetes.py +0 -0
  29. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/project.py +0 -0
  30. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/server.py +0 -0
  31. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/ssh_key.py +0 -0
  32. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/commands/vpc.py +0 -0
  33. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/fmt.py +0 -0
  34. {twc_cli-2.5.0 → twc_cli-2.6.0}/twc/typerx.py +0 -0
  35. {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.5.0
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
- ![TWC CLI](https://github.com/timeweb-cloud/twc/blob/master/artwork/light.svg)
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
- > [Руководство пользователя](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/README.md) 🇷🇺
31
- > [Command Line Interface (CLI) Reference](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/CLI_REFERENCE.md) 📜
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
@@ -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.5.0"
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"
@@ -12,5 +12,5 @@
12
12
  import sys
13
13
 
14
14
 
15
- __version__ = "2.5.0"
15
+ __version__ = "2.6.0"
16
16
  __pyversion__ = sys.version.replace("\n", "")
@@ -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, name: str, description: Optional[str] = None
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", f"{self.api_url}/firewall/groups", json=payload
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
- proto: FirewallProto,
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 proto == FirewallProto.ICMP.value else {"port": port}),
1653
+ **({} if protocol == FirewallProto.ICMP.value else {"port": port}),
1647
1654
  "direction": direction,
1648
- "protocol": proto,
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
- proto: FirewallProto,
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 proto == FirewallProto.ICMP.value else {"port": port}),
1687
+ **({} if protocol == FirewallProto.ICMP.value else {"port": port}),
1681
1688
  "direction": direction,
1682
- "protocol": proto,
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+)?/)?(tcp|udp)$)|(^icmp$)", value, re.I):
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
- if re.match(r"^icmp$", value, re.I):
469
- new_values.append((None, "icmp"))
656
+ pair = value.split("/")
657
+ if len(pair) == 1:
658
+ ports, proto = None, pair[0]
470
659
  else:
471
- ports, proto = value.split("/")
472
- new_values.append((ports, proto.lower()))
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
- "0.0.0.0/0",
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(group_name)
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 port in ports:
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[0], # :str port or port range
555
- proto=port[1], # :str protocol name
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
- rule_id: UUID,
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
- group_id = get_group_id_by_rule(client, rule_id)
590
- response = client.delete_firewall_rule(group_id, rule_id)
591
- if response.status_code == 204:
592
- print(rule_id)
593
- else:
594
- sys.exit(fmt.printer(response))
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(None, help="Protocol."),
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
- "proto": proto,
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=S3_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
- ![TWC CLI](https://github.com/timeweb-cloud/twc/blob/master/artwork/light.svg)
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