twc-cli 2.8.0__tar.gz → 2.9.1__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 (34) hide show
  1. {twc_cli-2.8.0 → twc_cli-2.9.1}/CHANGELOG.md +25 -0
  2. {twc_cli-2.8.0 → twc_cli-2.9.1}/PKG-INFO +1 -1
  3. {twc_cli-2.8.0 → twc_cli-2.9.1}/pyproject.toml +1 -1
  4. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/__version__.py +1 -1
  5. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/api/client.py +10 -0
  6. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/api/types.py +1 -0
  7. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/server.py +81 -29
  8. {twc_cli-2.8.0 → twc_cli-2.9.1}/COPYING +0 -0
  9. {twc_cli-2.8.0 → twc_cli-2.9.1}/README.md +0 -0
  10. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/__init__.py +0 -0
  11. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/__main__.py +0 -0
  12. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/api/__init__.py +0 -0
  13. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/api/base.py +0 -0
  14. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/api/exceptions.py +0 -0
  15. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/apiwrap.py +0 -0
  16. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/__init__.py +0 -0
  17. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/account.py +0 -0
  18. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/balancer.py +0 -0
  19. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/common.py +0 -0
  20. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/config.py +0 -0
  21. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/database.py +0 -0
  22. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/domain.py +0 -0
  23. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/firewall.py +0 -0
  24. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/floating_ip.py +0 -0
  25. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/image.py +0 -0
  26. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/kubernetes.py +0 -0
  27. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/project.py +0 -0
  28. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/ssh_key.py +0 -0
  29. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/storage.py +0 -0
  30. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/commands/vpc.py +0 -0
  31. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/fmt.py +0 -0
  32. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/typerx.py +0 -0
  33. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/utils.py +0 -0
  34. {twc_cli-2.8.0 → twc_cli-2.9.1}/twc/vars.py +0 -0
@@ -2,6 +2,31 @@
2
2
 
3
3
  В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
4
4
 
5
+ # Версия 2.9.1 (2025.03.07)
6
+
7
+ ## Исправлено
8
+
9
+ - Исправлена ошибка в реализации опции `--gpu` в `twc server create`.
10
+
11
+ # Версия 2.9.0 (2025.03.06)
12
+
13
+ ## Добавлено
14
+
15
+ - Поддержка заказа серверов с GPU
16
+ - Поддержка заказа серверов линейки Dedicated CPU
17
+ - Добавлены новые опции для команды `twc server create`: `--disable-ssh-password-auth`, `--gpus`, `--type`, `--configurator-id`.
18
+
19
+ ## Изменено
20
+
21
+ - Мелкие улучшения в коде.
22
+
23
+ # Версия 2.8.0 (2025.02.13)
24
+
25
+ ## Добавлено
26
+
27
+ - Поддержка SRV-записей для доменов.
28
+ - Теперь можно задавать TTL для DNS-записей.
29
+
5
30
  # Версия 2.7.0 (2024.11.02)
6
31
 
7
32
  ## Добавлено
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: twc-cli
3
- Version: 2.8.0
3
+ Version: 2.9.1
4
4
  Summary: Timeweb Cloud Command Line Interface.
5
5
  Home-page: https://github.com/timeweb-cloud/twc
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "twc-cli"
3
- version = "2.8.0"
3
+ version = "2.9.1"
4
4
  description = "Timeweb Cloud Command Line Interface."
5
5
  authors = ["ge <dev@timeweb.cloud>"]
6
6
  homepage = "https://github.com/timeweb-cloud/twc"
@@ -12,5 +12,5 @@
12
12
  import sys
13
13
 
14
14
 
15
- __version__ = "2.8.0"
15
+ __version__ = "2.9.1"
16
16
  __pyversion__ = sys.version.replace("\n", "")
@@ -76,6 +76,7 @@ class TimewebCloud(TimewebCloudBase):
76
76
  is_ddos_guard: bool = False,
77
77
  network: Optional[dict] = None,
78
78
  availability_zone: Optional[ServiceAvailabilityZone] = None,
79
+ is_root_password_required: Optional[bool] = None,
79
80
  ):
80
81
  """Create new Cloud Server. Note:
81
82
 
@@ -113,6 +114,11 @@ class TimewebCloud(TimewebCloudBase):
113
114
  if availability_zone
114
115
  else {}
115
116
  ),
117
+ **(
118
+ {"is_root_password_required": is_root_password_required}
119
+ if is_root_password_required is not None
120
+ else {}
121
+ ),
116
122
  }
117
123
 
118
124
  return self._request("POST", f"{self.api_url}/servers", json=payload)
@@ -186,6 +192,10 @@ class TimewebCloud(TimewebCloudBase):
186
192
  "POST", f"{self.api_url}/servers/{server_id}/clone", json={}
187
193
  )
188
194
 
195
+ def get_server_preset_types(self):
196
+ """List all available preset and configurator IDs by their types."""
197
+ return self._request("GET", f"{self.api_url}/presets/types/servers")
198
+
189
199
  def get_server_configurators(self):
190
200
  """List configurators."""
191
201
  return self._request("GET", f"{self.api_url}/configurator/servers")
@@ -124,6 +124,7 @@ class ServerConfiguration(TypedDict):
124
124
  disk: int
125
125
  cpu: int
126
126
  ram: int
127
+ gpu: int
127
128
 
128
129
 
129
130
  class ServerOSType(str, Enum):
@@ -3,6 +3,7 @@
3
3
  import re
4
4
  import sys
5
5
  import webbrowser
6
+ from enum import Enum
6
7
  from logging import debug
7
8
  from typing import Optional, List, Union
8
9
  from pathlib import Path
@@ -466,15 +467,31 @@ def process_ssh_key(client: TimewebCloud, ssh_key: str) -> int:
466
467
  sys.exit(f"Error: SSH-key '{ssh_key}' not found.")
467
468
 
468
469
 
469
- def select_configurator(client: TimewebCloud, region: str) -> int:
470
+ def select_configurator(client: TimewebCloud, kind: str, region: str) -> int:
470
471
  """Find and return configurator_id by location name."""
471
- configurators = client.get_server_configurators().json()[
472
- "server_configurators"
472
+ kind = kind.replace("-", "_")
473
+ configurators_by_type = client.get_server_preset_types().json()[
474
+ "configurators"
473
475
  ]
474
- for configurator in configurators:
476
+ available_configurators = configurators_by_type.get(kind, None)
477
+ if not available_configurators:
478
+ sys.exit(
479
+ f"Error: Unable to select server configurator_id: no configurators with type '{kind}'"
480
+ )
481
+ for configurator in available_configurators:
475
482
  if configurator["location"] == region:
476
483
  return configurator["id"]
477
- sys.exit(f"Unable to select location: '{region}' not found.")
484
+ sys.exit(
485
+ f"Error: Unable to select configurator_id: no configurators for region {region}"
486
+ )
487
+
488
+
489
+ class InstanceKind(str, Enum):
490
+ """Instance types used to select configurator."""
491
+
492
+ STANDARD = "standard"
493
+ PREMIUM = "premium"
494
+ DEDICATED_CPU = "dedicated-cpu"
478
495
 
479
496
 
480
497
  @server.command("create")
@@ -491,7 +508,17 @@ def server_create(
491
508
  None,
492
509
  help="Cloud Server configuration preset ID. "
493
510
  "NOTE: This argument is mutually exclusive with arguments: "
494
- "['--cpu', '--ram', '--disk'].",
511
+ "['--cpu', '--ram', '--disk', '--gpu'].",
512
+ ),
513
+ configurator_id: int = typer.Option(
514
+ None, help="ID of configuration constraints set."
515
+ ),
516
+ instance_kind: Optional[InstanceKind] = typer.Option(
517
+ InstanceKind.PREMIUM,
518
+ "--type",
519
+ help="Cloud Server type. "
520
+ "Servers with GPU always is 'premium'. "
521
+ "This option will be ignored if '--gpu' or '--preset-id' is set.",
495
522
  ),
496
523
  cpu: int = typer.Option(None, help="Number of vCPUs."),
497
524
  ram: str = typer.Option(
@@ -500,6 +527,13 @@ def server_create(
500
527
  disk: str = typer.Option(
501
528
  None, metavar="SIZE", help="System disk size, e.g. 10240M, 10G."
502
529
  ),
530
+ gpus: Optional[int] = typer.Option(None, min=0, max=4, hidden=True),
531
+ gpu: Optional[int] = typer.Option(
532
+ None,
533
+ min=0,
534
+ max=4,
535
+ help="Number of GPUs to attach.",
536
+ ),
503
537
  bandwidth: int = typer.Option(
504
538
  None, callback=bandwidth_callback, help="Network bandwidth."
505
539
  ),
@@ -549,6 +583,11 @@ def server_create(
549
583
  callback=load_from_config_callback,
550
584
  help="Add server to specific project.",
551
585
  ),
586
+ disable_ssh_password_auth: Optional[bool] = typer.Option(
587
+ False,
588
+ "--disable-ssh-password-auth",
589
+ help="Disable sshd password authentication.",
590
+ ),
552
591
  ):
553
592
  """Create Cloud Server."""
554
593
  client = create_client(config, profile)
@@ -560,13 +599,30 @@ def server_create(
560
599
  "is_ddos_guard": ddos_protection,
561
600
  "availability_zone": availability_zone,
562
601
  "network": {},
563
- **(
564
- {"is_local_network": local_network}
565
- if local_network is not None
566
- else {}
567
- ),
568
602
  }
569
603
 
604
+ if local_network is not None:
605
+ print(
606
+ "Option --local-network is deprecated and will be removed soon",
607
+ file=sys.stderr,
608
+ )
609
+ payload["is_local_network"] = local_network
610
+
611
+ if disable_ssh_password_auth:
612
+ if not ssh_keys:
613
+ print(
614
+ "You applied --disable-ssh-password-auth, but no ssh keys is set. "
615
+ "Pass keys to --ssh-key option or setup master ssh key.",
616
+ file=sys.stderr,
617
+ )
618
+ payload["is_root_password_required"] = False
619
+
620
+ instance_kind = instance_kind.value
621
+ if gpus:
622
+ gpu = gpus
623
+ if gpu:
624
+ instance_kind = "gpu"
625
+
570
626
  # Check availability zone
571
627
  usable_zones = ServiceRegion.get_zones(region)
572
628
  if availability_zone is not None and availability_zone not in usable_zones:
@@ -587,7 +643,7 @@ def server_create(
587
643
  if IPv4Address(private_ip) >= net.network_address + 4:
588
644
  payload["network"]["ip"] = private_ip
589
645
  else:
590
- # First 3 addresses is reserved for networks OVN based networks
646
+ # First 3 addresses is reserved by Timeweb Cloud for gateway and future use.
591
647
  sys.exit(
592
648
  f"Error: Private address '{private_ip}' is not allowed. "
593
649
  "IP must be at least the fourth in order in the network."
@@ -600,15 +656,15 @@ def server_create(
600
656
  sys.exit(f"Error: '{public_ip}' is not valid IPv4 address.")
601
657
  else:
602
658
  # New public IPv4 address will be automatically requested with
603
- # correct availability zone. This is official dirty hack.
659
+ # correct availability zone. This is an official dirty hack.
604
660
  if no_public_ip is False:
605
661
  payload["network"]["floating_ip"] = "create_ip"
606
662
 
607
663
  # Set server configuration parameters
608
- if preset_id and (cpu or ram or disk):
664
+ if preset_id and (cpu or ram or disk or gpu):
609
665
  raise UsageError(
610
666
  "'--preset-id' is mutually exclusive with: "
611
- + "['--cpu', '--ram', '--disk']."
667
+ + "['--cpu', '--ram', '--disk', '--gpu']."
612
668
  )
613
669
  if not preset_id and not (cpu or ram or disk):
614
670
  raise UsageError(
@@ -620,7 +676,10 @@ def server_create(
620
676
  if not locals()[param]:
621
677
  raise UsageError(f"Missing parameter: '--{param}'.")
622
678
  # Select configurator_id by region
623
- configurator_id = select_configurator(client, region)
679
+ if not configurator_id:
680
+ configurator_id = select_configurator(
681
+ client, kind=instance_kind, region=region
682
+ )
624
683
  requirements = get_requirements(client, configurator_id)
625
684
  payload["configuration"] = {
626
685
  "configurator_id": configurator_id,
@@ -628,6 +687,8 @@ def server_create(
628
687
  "ram": validate_ram(requirements, size_to_mb(ram)),
629
688
  "disk": validate_disk(requirements, size_to_mb(disk)),
630
689
  }
690
+ if gpu:
691
+ payload["configuration"]["gpu"] = gpu
631
692
  if bandwidth:
632
693
  payload["bandwidth"] = validate_bandwidth(requirements, bandwidth)
633
694
  else:
@@ -655,24 +716,13 @@ def server_create(
655
716
  if user_data:
656
717
  payload["cloud_init"] = user_data.read()
657
718
 
658
- # Check project_id before creating server
659
719
  if project_id:
660
- if not project_id in [
661
- prj["id"] for prj in client.get_projects().json()["projects"]
662
- ]:
663
- sys.exit(f"Wrong project ID: Project '{project_id}' not found.")
720
+ payload["project_id"] = project_id
664
721
 
665
722
  # Create Cloud Server
666
723
  debug("Create Cloud Server...")
667
724
  response = client.create_server(**payload)
668
725
 
669
- if project_id:
670
- debug(f"Add Server to project '{project_id}'...")
671
- client.add_server_to_project(
672
- response.json()["server"]["id"],
673
- project_id,
674
- )
675
-
676
726
  if nat_mode:
677
727
  debug(f"Set NAT mode to '{nat_mode}'")
678
728
  client.set_server_nat_mode(
@@ -793,7 +843,9 @@ def server_resize(
793
843
  debug("Get configurator_id...")
794
844
  if not configurator_id:
795
845
  configurator_id = select_configurator(
796
- client, old_state["location"]
846
+ client,
847
+ region=old_state["location"],
848
+ kind="premium",
797
849
  )
798
850
 
799
851
  # Get configurator by configurator_id
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