twc-cli 2.8.0__py3-none-any.whl → 2.9.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 +19 -0
- twc/__version__.py +1 -1
- twc/api/client.py +10 -0
- twc/commands/server.py +78 -29
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.0.dist-info}/METADATA +1 -1
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.0.dist-info}/RECORD +9 -9
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.0.dist-info}/COPYING +0 -0
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.0.dist-info}/WHEEL +0 -0
- {twc_cli-2.8.0.dist-info → twc_cli-2.9.0.dist-info}/entry_points.txt +0 -0
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
|
|
4
4
|
|
|
5
|
+
# Версия 2.9.0 (2025.03.06)
|
|
6
|
+
|
|
7
|
+
## Добавлено
|
|
8
|
+
|
|
9
|
+
- Поддержка заказа серверов с GPU
|
|
10
|
+
- Поддержка заказа серверов линейки Dedicated CPU
|
|
11
|
+
- Добавлены новые опции для команды `twc server create`: `--disable-ssh-password-auth`, `--gpus`, `--type`, `--configurator-id`.
|
|
12
|
+
|
|
13
|
+
## Изменено
|
|
14
|
+
|
|
15
|
+
- Мелкие улучшения в коде.
|
|
16
|
+
|
|
17
|
+
# Версия 2.8.0 (2025.02.13)
|
|
18
|
+
|
|
19
|
+
## Добавлено
|
|
20
|
+
|
|
21
|
+
- Поддержка SRV-записей для доменов.
|
|
22
|
+
- Теперь можно задавать TTL для DNS-записей.
|
|
23
|
+
|
|
5
24
|
# Версия 2.7.0 (2024.11.02)
|
|
6
25
|
|
|
7
26
|
## Добавлено
|
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/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', '--gpus'].",
|
|
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 '--gpus' 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,12 @@ 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(
|
|
531
|
+
None,
|
|
532
|
+
min=0,
|
|
533
|
+
max=4,
|
|
534
|
+
help="Number of GPUs to attach.",
|
|
535
|
+
),
|
|
503
536
|
bandwidth: int = typer.Option(
|
|
504
537
|
None, callback=bandwidth_callback, help="Network bandwidth."
|
|
505
538
|
),
|
|
@@ -549,6 +582,11 @@ def server_create(
|
|
|
549
582
|
callback=load_from_config_callback,
|
|
550
583
|
help="Add server to specific project.",
|
|
551
584
|
),
|
|
585
|
+
disable_ssh_password_auth: Optional[bool] = typer.Option(
|
|
586
|
+
False,
|
|
587
|
+
"--disable-ssh-password-auth",
|
|
588
|
+
help="Disable sshd password authentication.",
|
|
589
|
+
),
|
|
552
590
|
):
|
|
553
591
|
"""Create Cloud Server."""
|
|
554
592
|
client = create_client(config, profile)
|
|
@@ -560,13 +598,28 @@ def server_create(
|
|
|
560
598
|
"is_ddos_guard": ddos_protection,
|
|
561
599
|
"availability_zone": availability_zone,
|
|
562
600
|
"network": {},
|
|
563
|
-
**(
|
|
564
|
-
{"is_local_network": local_network}
|
|
565
|
-
if local_network is not None
|
|
566
|
-
else {}
|
|
567
|
-
),
|
|
568
601
|
}
|
|
569
602
|
|
|
603
|
+
if local_network is not None:
|
|
604
|
+
print(
|
|
605
|
+
"Option --local-network is deprecated and will be removed soon",
|
|
606
|
+
file=sys.stderr,
|
|
607
|
+
)
|
|
608
|
+
payload["is_local_network"] = local_network
|
|
609
|
+
|
|
610
|
+
if disable_ssh_password_auth:
|
|
611
|
+
if not ssh_keys:
|
|
612
|
+
print(
|
|
613
|
+
"You applied --disable-ssh-password-auth, but no ssh keys is set. "
|
|
614
|
+
"Pass keys to --ssh-key option or setup master ssh key.",
|
|
615
|
+
file=sys.stderr,
|
|
616
|
+
)
|
|
617
|
+
payload["is_root_password_required"] = False
|
|
618
|
+
|
|
619
|
+
instance_kind = instance_kind.value
|
|
620
|
+
if gpus:
|
|
621
|
+
instance_kind = "gpu"
|
|
622
|
+
|
|
570
623
|
# Check availability zone
|
|
571
624
|
usable_zones = ServiceRegion.get_zones(region)
|
|
572
625
|
if availability_zone is not None and availability_zone not in usable_zones:
|
|
@@ -587,7 +640,7 @@ def server_create(
|
|
|
587
640
|
if IPv4Address(private_ip) >= net.network_address + 4:
|
|
588
641
|
payload["network"]["ip"] = private_ip
|
|
589
642
|
else:
|
|
590
|
-
# First 3 addresses is reserved for
|
|
643
|
+
# First 3 addresses is reserved by Timeweb Cloud for gateway and future use.
|
|
591
644
|
sys.exit(
|
|
592
645
|
f"Error: Private address '{private_ip}' is not allowed. "
|
|
593
646
|
"IP must be at least the fourth in order in the network."
|
|
@@ -600,15 +653,15 @@ def server_create(
|
|
|
600
653
|
sys.exit(f"Error: '{public_ip}' is not valid IPv4 address.")
|
|
601
654
|
else:
|
|
602
655
|
# New public IPv4 address will be automatically requested with
|
|
603
|
-
# correct availability zone. This is official dirty hack.
|
|
656
|
+
# correct availability zone. This is an official dirty hack.
|
|
604
657
|
if no_public_ip is False:
|
|
605
658
|
payload["network"]["floating_ip"] = "create_ip"
|
|
606
659
|
|
|
607
660
|
# Set server configuration parameters
|
|
608
|
-
if preset_id and (cpu or ram or disk):
|
|
661
|
+
if preset_id and (cpu or ram or disk or gpus):
|
|
609
662
|
raise UsageError(
|
|
610
663
|
"'--preset-id' is mutually exclusive with: "
|
|
611
|
-
+ "['--cpu', '--ram', '--disk']."
|
|
664
|
+
+ "['--cpu', '--ram', '--disk', '--gpus']."
|
|
612
665
|
)
|
|
613
666
|
if not preset_id and not (cpu or ram or disk):
|
|
614
667
|
raise UsageError(
|
|
@@ -620,7 +673,10 @@ def server_create(
|
|
|
620
673
|
if not locals()[param]:
|
|
621
674
|
raise UsageError(f"Missing parameter: '--{param}'.")
|
|
622
675
|
# Select configurator_id by region
|
|
623
|
-
|
|
676
|
+
if not configurator_id:
|
|
677
|
+
configurator_id = select_configurator(
|
|
678
|
+
client, kind=instance_kind, region=region
|
|
679
|
+
)
|
|
624
680
|
requirements = get_requirements(client, configurator_id)
|
|
625
681
|
payload["configuration"] = {
|
|
626
682
|
"configurator_id": configurator_id,
|
|
@@ -628,6 +684,8 @@ def server_create(
|
|
|
628
684
|
"ram": validate_ram(requirements, size_to_mb(ram)),
|
|
629
685
|
"disk": validate_disk(requirements, size_to_mb(disk)),
|
|
630
686
|
}
|
|
687
|
+
if gpus:
|
|
688
|
+
payload["configuration"]["gpus"] = gpus
|
|
631
689
|
if bandwidth:
|
|
632
690
|
payload["bandwidth"] = validate_bandwidth(requirements, bandwidth)
|
|
633
691
|
else:
|
|
@@ -655,24 +713,13 @@ def server_create(
|
|
|
655
713
|
if user_data:
|
|
656
714
|
payload["cloud_init"] = user_data.read()
|
|
657
715
|
|
|
658
|
-
# Check project_id before creating server
|
|
659
716
|
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.")
|
|
717
|
+
payload["project_id"] = project_id
|
|
664
718
|
|
|
665
719
|
# Create Cloud Server
|
|
666
720
|
debug("Create Cloud Server...")
|
|
667
721
|
response = client.create_server(**payload)
|
|
668
722
|
|
|
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
723
|
if nat_mode:
|
|
677
724
|
debug(f"Set NAT mode to '{nat_mode}'")
|
|
678
725
|
client.set_server_nat_mode(
|
|
@@ -793,7 +840,9 @@ def server_resize(
|
|
|
793
840
|
debug("Get configurator_id...")
|
|
794
841
|
if not configurator_id:
|
|
795
842
|
configurator_id = select_configurator(
|
|
796
|
-
client,
|
|
843
|
+
client,
|
|
844
|
+
region=old_state["location"],
|
|
845
|
+
kind="premium",
|
|
797
846
|
)
|
|
798
847
|
|
|
799
848
|
# Get configurator by configurator_id
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
CHANGELOG.md,sha256=
|
|
1
|
+
CHANGELOG.md,sha256=21X0xzAaI_iHbjPxLPuWpMoghzW3LmGchhXRTjB_eIU,27056
|
|
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=DbYVn6TuN9FcaK_hzU8lsJQfEXuMuhVIoohBrsqV2Eo,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
10
|
twc/api/types.py,sha256=HCxdTi-o8nVq4ShPthd2fUvlYufEoXafx_6qrNHFH04,5406
|
|
11
11
|
twc/apiwrap.py,sha256=hKrg_o6rLfY32SEnWMc1BSXHnSAh7TGar1JQ90YnG5M,2970
|
|
@@ -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=bPeSht7VqCZUeHfUUhDa-Si2um2nPR4riUnBwGUR0u4,71886
|
|
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.0.dist-info/COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
|
|
33
|
+
twc_cli-2.9.0.dist-info/METADATA,sha256=jcSe64PkZXrOOqCrDrqyi2yrrqlZbmX5opo5hW0oHl0,2652
|
|
34
|
+
twc_cli-2.9.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
35
|
+
twc_cli-2.9.0.dist-info/entry_points.txt,sha256=tmTaVRhm8BkNrXC_9XJMum7O9wFVOvkXcBetxmahWvE,40
|
|
36
|
+
twc_cli-2.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|