twc-cli 2.8.0__py3-none-any.whl → 2.9.1__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 +25 -0
- twc/__version__.py +1 -1
- twc/api/client.py +10 -0
- twc/api/types.py +1 -0
- twc/commands/server.py +81 -29
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.1.dist-info}/METADATA +1 -1
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.1.dist-info}/RECORD +10 -10
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.1.dist-info}/COPYING +0 -0
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.1.dist-info}/WHEEL +0 -0
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.1.dist-info}/entry_points.txt +0 -0
CHANGELOG.md
CHANGED
|
@@ -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
|
## Добавлено
|
twc/__version__.py
CHANGED
twc/api/client.py
CHANGED
|
@@ -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")
|
twc/api/types.py
CHANGED
twc/commands/server.py
CHANGED
|
@@ -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
|
-
|
|
472
|
-
|
|
472
|
+
kind = kind.replace("-", "_")
|
|
473
|
+
configurators_by_type = client.get_server_preset_types().json()[
|
|
474
|
+
"configurators"
|
|
473
475
|
]
|
|
474
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
846
|
+
client,
|
|
847
|
+
region=old_state["location"],
|
|
848
|
+
kind="premium",
|
|
797
849
|
)
|
|
798
850
|
|
|
799
851
|
# Get configurator by configurator_id
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
CHANGELOG.md,sha256=
|
|
1
|
+
CHANGELOG.md,sha256=bNHVOSTbaShheTcY3nR3mF6h4-rIRMLYFG7cf4mE8dE,27220
|
|
2
2
|
COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
|
|
3
3
|
twc/__init__.py,sha256=NwPAMNw3NuHdWGQvWS9_lromVF6VM194oVOipojfJns,113
|
|
4
4
|
twc/__main__.py,sha256=ADHceaQUzgLmvhYHvb5O8urdJWj5IcEHLpTQkSExiD8,2468
|
|
5
|
-
twc/__version__.py,sha256=
|
|
5
|
+
twc/__version__.py,sha256=LA3Qa1Id0E6VF_GJX3GIwgV7Jrh2YzyspaQDWUoXEGM,442
|
|
6
6
|
twc/api/__init__.py,sha256=SXew0Fe51M7nRBNQaaLRH4NjnRHkQUn7J26OCkQsftA,128
|
|
7
7
|
twc/api/base.py,sha256=QRefnIgmlbz8n37GLBKeAK1AtzkcNo1IFjZgHDDECJ4,7912
|
|
8
|
-
twc/api/client.py,sha256=
|
|
8
|
+
twc/api/client.py,sha256=NFS8Jz64b-FHGy2QNwtzxyHGK7tanTquhTSJ6IcH3jg,59447
|
|
9
9
|
twc/api/exceptions.py,sha256=UzK3pKRffcXlhnkPy6MDjP_DygVoV17DuZ_mdNbOzts,2369
|
|
10
|
-
twc/api/types.py,sha256=
|
|
10
|
+
twc/api/types.py,sha256=SJCKJsjdk9dFLgABO2c83oIw6ht-ARwPKmHKVZrbiN8,5419
|
|
11
11
|
twc/apiwrap.py,sha256=hKrg_o6rLfY32SEnWMc1BSXHnSAh7TGar1JQ90YnG5M,2970
|
|
12
12
|
twc/commands/__init__.py,sha256=a-6fHQQwOj--Z7uBZGZL3z1rvJiOGUMQMRET1UknIYo,430
|
|
13
13
|
twc/commands/account.py,sha256=6q9ri02oFbUUZuqNVXO-uHOX45B4ELJlPjyfVaEL5Qw,5960
|
|
@@ -21,7 +21,7 @@ twc/commands/floating_ip.py,sha256=G9nD5BbHCZcuytbzeneDJWQDhd8c8WRtq9pAfwI9m7E,8
|
|
|
21
21
|
twc/commands/image.py,sha256=OviQwegXK55H3TBlroCASVcgj2QUVCTo0ZhF5ug9eT8,8165
|
|
22
22
|
twc/commands/kubernetes.py,sha256=-Cgas1vFVMcrWGinjstuUz3sqX0ZNXv_4mwPwuwKeLE,20870
|
|
23
23
|
twc/commands/project.py,sha256=xnL3kLIumKzrI9EZ6r6m-PGOl3mZ9IhLQua7WZ3Rghg,10499
|
|
24
|
-
twc/commands/server.py,sha256=
|
|
24
|
+
twc/commands/server.py,sha256=1Shwy-rTHeingiEl2EErkXIpm5TDWyBYO3fJl794Jos,71982
|
|
25
25
|
twc/commands/ssh_key.py,sha256=NHgTPhAQpDzt-iPHHVo4XqUJvujNqf019N6N9qYZ9Us,7941
|
|
26
26
|
twc/commands/storage.py,sha256=Pztk5iUBp9RtkdOwsfHaZFCnD8GuH6zOPtluawkRmiI,19404
|
|
27
27
|
twc/commands/vpc.py,sha256=SAht6UD17mU0d_AZY6W34VEYs7CqUsS2iDakPFxAFQU,8876
|
|
@@ -29,8 +29,8 @@ twc/fmt.py,sha256=nbuYZ8nVabYDwCmZqnL3-c6Tmri4B-R_sTCkG6sdfeI,7171
|
|
|
29
29
|
twc/typerx.py,sha256=AZ6BgTQvlrZYfKVYd9YqRNQnAR2XuyqImz4rf6di6f4,6737
|
|
30
30
|
twc/utils.py,sha256=uWizyUC4dHLwtk50q4Sub3zOvnVESfHKBbXYwk5t71w,651
|
|
31
31
|
twc/vars.py,sha256=fva3O2leMGtExb1aWiAk6sOV0O8et9_kEyRpYYIZ7CM,543
|
|
32
|
-
twc_cli-2.
|
|
33
|
-
twc_cli-2.
|
|
34
|
-
twc_cli-2.
|
|
35
|
-
twc_cli-2.
|
|
36
|
-
twc_cli-2.
|
|
32
|
+
twc_cli-2.9.1.dist-info/COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
|
|
33
|
+
twc_cli-2.9.1.dist-info/METADATA,sha256=TIBw2Q5afvk-ZMkmfD1BYaErlpPorV1oXgmFwJ-AWCU,2652
|
|
34
|
+
twc_cli-2.9.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
35
|
+
twc_cli-2.9.1.dist-info/entry_points.txt,sha256=tmTaVRhm8BkNrXC_9XJMum7O9wFVOvkXcBetxmahWvE,40
|
|
36
|
+
twc_cli-2.9.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|