twc-cli 2.4.1__py3-none-any.whl → 2.6.0__py3-none-any.whl
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.
- CHANGELOG.md +46 -0
- twc/__main__.py +2 -0
- twc/__version__.py +1 -1
- twc/api/base.py +5 -8
- twc/api/client.py +95 -11
- twc/api/types.py +64 -7
- twc/apiwrap.py +2 -2
- twc/commands/__init__.py +1 -0
- twc/commands/common.py +18 -2
- twc/commands/firewall.py +238 -29
- twc/commands/floating_ip.py +296 -0
- twc/commands/image.py +10 -7
- twc/commands/project.py +1 -2
- twc/commands/server.py +75 -21
- twc/commands/storage.py +7 -3
- twc/commands/vpc.py +23 -9
- twc/fmt.py +1 -1
- twc/vars.py +3 -5
- twc_cli-2.6.0.dist-info/METADATA +83 -0
- twc_cli-2.6.0.dist-info/RECORD +36 -0
- {twc_cli-2.4.1.dist-info → twc_cli-2.6.0.dist-info}/WHEEL +1 -1
- twc_cli-2.4.1.dist-info/METADATA +0 -66
- twc_cli-2.4.1.dist-info/RECORD +0 -35
- {twc_cli-2.4.1.dist-info → twc_cli-2.6.0.dist-info}/COPYING +0 -0
- {twc_cli-2.4.1.dist-info → twc_cli-2.6.0.dist-info}/entry_points.txt +0 -0
twc/commands/firewall.py
CHANGED
|
@@ -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
|
# ------------------------------------------------------------- #
|
|
@@ -453,23 +641,24 @@ def filrewall_rule_list(
|
|
|
453
641
|
|
|
454
642
|
|
|
455
643
|
# ------------------------------------------------------------- #
|
|
456
|
-
# $ twc firewall rule
|
|
644
|
+
# $ twc firewall rule create #
|
|
457
645
|
# ------------------------------------------------------------- #
|
|
458
646
|
|
|
459
647
|
|
|
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
|
|
|
@@ -482,8 +671,8 @@ def validate_cidr_callback(value):
|
|
|
482
671
|
return value
|
|
483
672
|
|
|
484
673
|
|
|
485
|
-
@firewall_rule.command("add")
|
|
486
|
-
def
|
|
674
|
+
@firewall_rule.command("create", "add")
|
|
675
|
+
def firewall_rule_create(
|
|
487
676
|
ports: List[str] = typer.Argument(
|
|
488
677
|
...,
|
|
489
678
|
metavar="[PORT[-PORT]/]PROTO...",
|
|
@@ -510,17 +699,22 @@ def firewall_allow(
|
|
|
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."""
|
|
524
718
|
client = create_client(config, profile)
|
|
525
719
|
if make_group is not None and group is not None:
|
|
526
720
|
raise UsageError(
|
|
@@ -535,7 +729,9 @@ def firewall_allow(
|
|
|
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_allow(
|
|
|
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_allow(
|
|
|
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)
|