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.

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 add #
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+)?/)?(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
 
@@ -482,8 +671,8 @@ def validate_cidr_callback(value):
482
671
  return value
483
672
 
484
673
 
485
- @firewall_rule.command("add")
486
- def firewall_allow(
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
- "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
- """Add new firewall rule."""
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(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_allow(
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_allow(
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)