twc-cli 2.9.1__py3-none-any.whl → 2.10.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 CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
4
4
 
5
+ # Версия 2.10.0 (2025.03.24)
6
+
7
+ ## Добавлено
8
+
9
+ - Добавлена поддержка региона `de-1` (зона `fra-1`) для облачных серверов.
10
+ - Добавлены новые опции к команде `twc database create`:
11
+ - для создания пользователя СУБД: `--user-login`, `--user-password`, `--user-host`, `--user-privileges`, `--user-desc`;
12
+ - для создания первой базы данных: `--db-name`, `--db-desc`;
13
+ - для настроек сети в класетере СУБД: `--network-id`, `--private-ip`, `--public-ip`, `--no-public-ip`;
14
+ - для настройки автоматических бэкапов кластера: `--enable-backups`, `--backup-keep`, `--backup-start-date`, `--backup-interval`, `--backup-day-of-week`.
15
+ - Добавлена новая команда `twc database list-types` для вывода доступных к созданию управляемых баз данных.
16
+ - Добавлена новая команда `twc database backup schedule` позволяющая настроить параметры автоматического резервного копирования кластера.
17
+ - Добавлены новые команды для управления пользователями в кластерах СУБД: `twc database user list`, `twc database user get`, `twc database user create`, `twc database user remove`.
18
+ - Добавлены новые команды для управления базами данных в кластерах СУБД: `twc database instance list`, `twc database instance create`, `twc database instance remove`.
19
+
20
+ ## Изменено
21
+
22
+ - Опция `--network` команды `twc server create` объявлена устаревшей и скрыта, добавлена эквивалентная опция `--network-id`.
23
+ - Опции `--login` и `--password` команды `twc database create` объявлены устаревшими и скрыты, вместо них теперь нужно использовать `--user-login` и `--user-password`.
24
+
25
+ # Версия 2.9.2 (2025.03.10)
26
+
27
+ ## Исправлено
28
+
29
+ - Исправлена ошибка из-за которой на Windows не устанавливались аттрибуты накнфигурационный файл.
30
+
5
31
  # Версия 2.9.1 (2025.03.07)
6
32
 
7
33
  ## Исправлено
twc/__version__.py CHANGED
@@ -12,5 +12,5 @@
12
12
  import sys
13
13
 
14
14
 
15
- __version__ = "2.9.1"
15
+ __version__ = "2.10.0"
16
16
  __pyversion__ = sys.version.replace("\n", "")
twc/api/client.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Timeweb Cloud API client."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from typing import Optional, Union, List
4
6
  from uuid import UUID
5
7
  from pathlib import Path
@@ -20,7 +22,6 @@ from .types import (
20
22
  ServiceRegion,
21
23
  ServiceAvailabilityZone,
22
24
  ResourceType,
23
- DBMS,
24
25
  MySQLAuthPlugin,
25
26
  LoadBalancerProto,
26
27
  LoadBalancerAlgo,
@@ -727,19 +728,27 @@ class TimewebCloud(TimewebCloudBase):
727
728
  """Get database presets list."""
728
729
  return self._request("GET", f"{self.api_url}/presets/dbs")
729
730
 
731
+ def get_database_types(self):
732
+ """Get database types."""
733
+ return self._request("GET", f"{self.api_url}/database-types")
734
+
735
+ def get_database_configrators(self):
736
+ """Get database_configurators."""
737
+ return self._request(
738
+ "GET", f"{self.api_url_v1}/configurator/databases"
739
+ )
740
+
730
741
  def create_database(
731
742
  self,
732
743
  name: str,
733
- dbms: DBMS,
744
+ dbms: str,
734
745
  preset_id: int,
735
746
  password: str,
736
747
  login: Optional[str] = None,
737
748
  hash_type: Optional[MySQLAuthPlugin] = None,
738
749
  config_parameters: Optional[dict] = None,
739
750
  ):
740
- """Create database."""
741
- if dbms == "mysql8":
742
- dbms = "mysql"
751
+ """Create database. DEPRECATED."""
743
752
  payload = {
744
753
  "name": name,
745
754
  "type": dbms,
@@ -751,6 +760,42 @@ class TimewebCloud(TimewebCloudBase):
751
760
  }
752
761
  return self._request("POST", f"{self.api_url}/dbs", json=payload)
753
762
 
763
+ def create_database2(
764
+ self,
765
+ name: str,
766
+ dbms: str,
767
+ network: Optional[dict[str, str]] = None,
768
+ preset_id: Optional[int] = None,
769
+ configurator_id: Optional[int] = None,
770
+ admin: Optional[dict[str, str]] = None,
771
+ instance: Optional[dict[str, str]] = None,
772
+ auto_backups: Optional[dict[str, str]] = None,
773
+ hash_type: Optional[MySQLAuthPlugin] = None,
774
+ config_parameters: Optional[dict] = None,
775
+ project_id: Optional[int] = None,
776
+ ):
777
+ """Create database cluster."""
778
+ payload = {
779
+ "name": name,
780
+ "type": dbms,
781
+ "hash_type": hash_type,
782
+ **({"network": network} if network else {}),
783
+ **(
784
+ {"config_parameters": config_parameters}
785
+ if config_parameters
786
+ else {}
787
+ ),
788
+ **({"preset_id": preset_id} if preset_id else {}),
789
+ **(
790
+ {"configurator_id": configurator_id} if configurator_id else {}
791
+ ),
792
+ **({"admin": admin} if admin else {}),
793
+ **({"auto_backups": auto_backups} if auto_backups else {}),
794
+ **({"instance": instance} if instance else {}),
795
+ **({"project_id": project_id} if project_id else {}),
796
+ }
797
+ return self._request("POST", f"{self.api_url}/databases", json=payload)
798
+
754
799
  def update_database(
755
800
  self,
756
801
  db_id: int,
@@ -837,6 +882,110 @@ class TimewebCloud(TimewebCloudBase):
837
882
  f"{self.api_url}/dbs/{db_id}/backups/{backup_id}",
838
883
  )
839
884
 
885
+ def get_database_autobackup_settings(self, db_id: int):
886
+ """Get database autobackup settings."""
887
+ return self._request(
888
+ "GET",
889
+ f"{self.api_url}/dbs/{db_id}/auto-backups",
890
+ )
891
+
892
+ def update_database_autobackup_settings(
893
+ self,
894
+ db_id: int,
895
+ is_enabled: Optional[bool] = None,
896
+ copy_count: Optional[int] = None,
897
+ creation_start_at: Optional[int] = None,
898
+ interval: Optional[BackupInterval] = None,
899
+ day_of_week: Optional[int] = None,
900
+ ):
901
+ """Set database autobackup settings."""
902
+ payload = {
903
+ **({"is_enabled": is_enabled} if is_enabled is not None else {}),
904
+ **({"copy_count": copy_count} if copy_count else {}),
905
+ **(
906
+ {"creation_start_at": creation_start_at}
907
+ if creation_start_at
908
+ else {}
909
+ ),
910
+ **({"interval": interval} if interval else {}),
911
+ **({"day_of_week": day_of_week} if day_of_week else {}),
912
+ }
913
+ return self._request(
914
+ "PATCH",
915
+ f"{self.api_url}/dbs/{db_id}/auto-backups",
916
+ json=payload,
917
+ )
918
+
919
+ def create_database_user(
920
+ self,
921
+ db_id: int,
922
+ login: str,
923
+ password: str,
924
+ privileges: List[str],
925
+ host: Optional[str] = None,
926
+ instance_id: Optional[int] = None,
927
+ description: Optional[str] = None,
928
+ ):
929
+ payload = {
930
+ "login": login,
931
+ "password": password,
932
+ "privileges": privileges,
933
+ }
934
+ if host:
935
+ payload["host"] = host
936
+ if instance_id:
937
+ payload["instance_id"] = instance_id
938
+ if description:
939
+ payload["description"] = description
940
+ return self._request(
941
+ "POST", f"{self.api_url}/databases/{db_id}/admins", json=payload
942
+ )
943
+
944
+ def get_database_users(self, db_id: int):
945
+ """..."""
946
+ return self._request(
947
+ "GET", f"{self.api_url_v1}/databases/{db_id}/admins"
948
+ )
949
+
950
+ def get_database_user(self, db_id: int, user_id: int):
951
+ """..."""
952
+ return self._request(
953
+ "GET", f"{self.api_url_v1}/databases/{db_id}/admins/{user_id}"
954
+ )
955
+
956
+ def delete_database_user(self, db_id: int, user_id: int):
957
+ """..."""
958
+ return self._request(
959
+ "DELETE", f"{self.api_url_v1}/databases/{db_id}/admins/{user_id}"
960
+ )
961
+
962
+ def create_database_instance(
963
+ self,
964
+ db_id: int,
965
+ name: str,
966
+ description: Optional[str] = None,
967
+ ):
968
+ """..."""
969
+ payload = {"name": name}
970
+ if description:
971
+ payload["description"] = description
972
+ return self._request(
973
+ "POST", f"{self.api_url}/databases/{db_id}/instances"
974
+ )
975
+
976
+ def get_database_instances(self, db_id: int):
977
+ """..."""
978
+ return self._request(
979
+ "GET", f"{self.api_url}/databases/{db_id}/instances"
980
+ )
981
+
982
+ def delete_database_instance(self, db_id: int, instance_id: int):
983
+ """..."""
984
+ return self._request(
985
+ "DELETE",
986
+ f"{self.api_url}/databases/{db_id}/instances/{instance_id}",
987
+ )
988
+
840
989
  # -----------------------------------------------------------------------
841
990
  # Object Storage
842
991
 
twc/api/types.py CHANGED
@@ -13,6 +13,7 @@ class ServiceRegion(str, Enum):
13
13
  KZ_1 = "kz-1"
14
14
  PL_1 = "pl-1"
15
15
  NL_1 = "nl-1"
16
+ DE_1 = "de-1"
16
17
 
17
18
  @classmethod
18
19
  def get_zones(cls, region: str) -> List[str]:
@@ -29,6 +30,8 @@ class ServiceRegion(str, Enum):
29
30
  return ["gdn-1"]
30
31
  if region == cls.NL_1:
31
32
  return ["ams-1"]
33
+ if region == cls.DE_1:
34
+ return ["fra-1"]
32
35
  return []
33
36
 
34
37
 
@@ -44,6 +47,7 @@ class ServiceAvailabilityZone(str, Enum):
44
47
  ALA_1 = "ala-1"
45
48
  GDN_1 = "gdn-1"
46
49
  AMS_1 = "ams-1"
50
+ FRA_1 = "fra-1"
47
51
 
48
52
  @classmethod
49
53
  def get_region(cls, zone: str) -> Optional[str]:
@@ -60,6 +64,8 @@ class ServiceAvailabilityZone(str, Enum):
60
64
  return ServiceRegion.PL_1
61
65
  if zone == cls.AMS_1:
62
66
  return ServiceRegion.NL_1
67
+ if zone == cls.FRA_1:
68
+ return ServiceRegion.DE_1
63
69
  return None
64
70
 
65
71
 
@@ -170,16 +176,6 @@ class ResourceType(str, Enum):
170
176
  DEDICATED_SERVER = "dedicated"
171
177
 
172
178
 
173
- class DBMS(str, Enum):
174
- """Available DBMS in Timeweb Cloud managed databases service."""
175
-
176
- MYSQL_5 = "mysql5"
177
- MYSQL_8 = "mysql8"
178
- POSTGRES = "postgres"
179
- REDIS = "redis"
180
- MONGODB = "mongodb"
181
-
182
-
183
179
  class MySQLAuthPlugin(str, Enum):
184
180
  """MySQL auth plugin options in Timeweb Cloud managed databases
185
181
  service.
twc/commands/config.py CHANGED
@@ -38,7 +38,7 @@ def write_to_file(data: dict, filepath: Path) -> None:
38
38
  if os.name == "nt":
39
39
  hidden_file_attr = 0x02
40
40
  ctypes.windll.kernel32.SetFileAttributesW(
41
- filepath, hidden_file_attr
41
+ str(filepath), hidden_file_attr
42
42
  )
43
43
  print(f"Done! Configuration is saved in {filepath}")
44
44
  sys.exit(0)
twc/commands/database.py CHANGED
@@ -2,15 +2,18 @@
2
2
 
3
3
  import re
4
4
  import sys
5
+ import textwrap
6
+ from datetime import date, datetime
5
7
  from typing import Optional, List
6
8
  from pathlib import Path
9
+ from ipaddress import IPv4Address, IPv4Network
7
10
 
8
11
  import typer
9
12
  from requests import Response
10
13
 
11
14
  from twc import fmt
12
15
  from twc.typerx import TyperAlias
13
- from twc.api import ServiceRegion, DBMS, MySQLAuthPlugin
16
+ from twc.api import ServiceRegion, MySQLAuthPlugin, BackupInterval
14
17
  from twc.apiwrap import create_client
15
18
  from twc.utils import merge_dicts
16
19
  from .common import (
@@ -27,6 +30,12 @@ from .common import (
27
30
  database = TyperAlias(help=__doc__)
28
31
  database_backup = TyperAlias(help="Manage database backups.")
29
32
  database.add_typer(database_backup, name="backup")
33
+ database_user = TyperAlias(help="Manage database users.")
34
+ database.add_typer(database_user, name="user")
35
+ database_instance = TyperAlias(
36
+ help="Manage instances in cluster (databases/topics/etc)."
37
+ )
38
+ database.add_typer(database_instance, name="instance", aliases=["db"])
30
39
 
31
40
 
32
41
  # ------------------------------------------------------------- #
@@ -206,6 +215,34 @@ def database_list_presets(
206
215
  )
207
216
 
208
217
 
218
+ # ------------------------------------------------------------- #
219
+ # $ twc database list-types #
220
+ # ------------------------------------------------------------- #
221
+
222
+
223
+ @database.command("list-types", "lt")
224
+ def database_list_types(
225
+ verbose: Optional[bool] = verbose_option,
226
+ config: Optional[Path] = config_option,
227
+ profile: Optional[str] = profile_option,
228
+ output_format: Optional[str] = output_format_option,
229
+ ):
230
+ """List database configuration presets."""
231
+ client = create_client(config, profile)
232
+ response = client.get_database_types().json()
233
+ table = fmt.Table()
234
+ table.header(["TYPE", "DATABASE", "VERSION"])
235
+ for service in response["types"]:
236
+ table.row(
237
+ [
238
+ service["type"],
239
+ service["name"],
240
+ service["version"],
241
+ ]
242
+ )
243
+ table.print()
244
+
245
+
209
246
  # ------------------------------------------------------------- #
210
247
  # $ twc database create #
211
248
  # ------------------------------------------------------------- #
@@ -228,6 +265,12 @@ def set_params(params: list) -> dict:
228
265
  return parameters
229
266
 
230
267
 
268
+ def dbms_parameters_callback(value: str) -> List[str]:
269
+ if value:
270
+ return value.split(",")
271
+ return []
272
+
273
+
231
274
  @database.command("create")
232
275
  def database_create(
233
276
  verbose: Optional[bool] = verbose_option,
@@ -235,80 +278,185 @@ def database_create(
235
278
  profile: Optional[str] = profile_option,
236
279
  output_format: Optional[str] = output_format_option,
237
280
  preset_id: int = typer.Option(..., help="Database configuration preset."),
238
- dbms: DBMS = typer.Option(
281
+ dbms: str = typer.Option(
239
282
  ...,
240
283
  "--type",
241
- case_sensitive=False,
242
- help="Database management system.",
284
+ help="Database management system. See TYPE in `twc database list-types`.",
243
285
  ),
244
286
  hash_type: Optional[MySQLAuthPlugin] = typer.Option(
245
287
  MySQLAuthPlugin.CACHING_SHA2.value,
246
288
  case_sensitive=False,
247
289
  help="Authentication plugin for MySQL.",
248
290
  ),
249
- name: str = typer.Option(..., help="Database instance display name."),
291
+ name: str = typer.Option(..., help="Database cluster display name."),
250
292
  params: Optional[List[str]] = typer.Option(
251
293
  None,
252
294
  "--param",
253
295
  metavar="PARAM=VALUE",
254
- help="Database parameters, can be multiple.",
296
+ help="Database config parameters, can be multiple.",
255
297
  ),
256
- login: Optional[str] = typer.Option(None, help="Database user login."),
257
- password: str = typer.Option(
258
- ...,
259
- prompt="Database user password",
260
- confirmation_prompt=True,
261
- hide_input=True,
298
+ login: Optional[str] = typer.Option(
299
+ # DEPRECATED
300
+ None,
301
+ hidden=True,
302
+ help="Database user login.",
303
+ ),
304
+ password: Optional[str] = typer.Option(
305
+ # DEPRECATED
306
+ None,
307
+ hidden=True,
308
+ ),
309
+ user_login: Optional[str] = typer.Option(None, help="User login."),
310
+ user_password: Optional[str] = typer.Option(None, help="User password."),
311
+ user_host: Optional[str] = typer.Option(
312
+ "%", help="User host for MySQL, Postgres"
313
+ ),
314
+ user_privileges: Optional[str] = typer.Option(
315
+ [],
316
+ help="Comma-separated list of user privileges.",
317
+ callback=dbms_parameters_callback,
318
+ ),
319
+ user_desc: Optional[str] = typer.Option(None, help="Comment for user."),
320
+ db_name: Optional[str] = typer.Option(None, help="Database name."),
321
+ db_desc: Optional[str] = typer.Option(None, help="Database comment."),
322
+ network_id: Optional[str] = typer.Option(None, help="Private network ID."),
323
+ private_ip: Optional[str] = typer.Option(
324
+ None, help="Private IPv4 address."
325
+ ),
326
+ public_ip: Optional[str] = typer.Option(
327
+ None, help="Public IPv4 address. New address by default."
328
+ ),
329
+ no_public_ip: Optional[bool] = typer.Option(
330
+ False, "--no-public-ip", help="Do not add public IPv4 address."
262
331
  ),
263
332
  project_id: Optional[int] = typer.Option(
264
333
  None,
265
334
  envvar="TWC_PROJECT",
266
335
  show_envvar=False,
267
336
  callback=load_from_config_callback,
268
- help="Add database to specific project.",
337
+ help="Add database cluster to specific project.",
338
+ ),
339
+ enable_backups: Optional[bool] = typer.Option(
340
+ False,
341
+ "--enable-backups",
342
+ help="Enable atomatic backups of database cluster.",
343
+ ),
344
+ backup_keep: Optional[int] = typer.Option(
345
+ 1,
346
+ show_default=True,
347
+ help="Number of backups to keep.",
348
+ ),
349
+ backup_start_date: Optional[datetime] = typer.Option(
350
+ date.today().strftime("%Y-%m-%d"),
351
+ formats=["%Y-%m-%d"],
352
+ show_default=False,
353
+ help="Start date of the first backup creation [default: today].",
354
+ ),
355
+ backup_interval: Optional[BackupInterval] = typer.Option(
356
+ BackupInterval.DAY.value,
357
+ "--backup-interval",
358
+ help="Backup interval.",
359
+ ),
360
+ backup_day_of_week: Optional[int] = typer.Option(
361
+ 1,
362
+ min=1,
363
+ max=7,
364
+ help="The day of the week on which backups will be created."
365
+ " NOTE: This option works only with interval 'week'."
366
+ " First day of week is monday.",
269
367
  ),
270
368
  ):
271
- """Create managed database instance."""
369
+ """Create managed database cluster."""
272
370
  client = create_client(config, profile)
273
371
 
274
- # Check preset_id
275
- for preset in client.get_database_presets().json()["databases_presets"]:
276
- if preset["id"] == preset_id:
277
- _dbms = re.sub(
278
- r"[0-9]+", "", dbms
279
- ) # transform 'mysql5' to 'mysql', etc.
280
- if not preset["type"].startswith(_dbms):
281
- sys.exit(
282
- f"Error: DBMS '{dbms}' doesn't match with preset_id type."
283
- )
284
-
285
372
  payload = {
286
373
  "dbms": dbms,
287
374
  "preset_id": preset_id,
288
375
  "name": name,
289
376
  "hash_type": hash_type,
290
- "login": login,
291
- "password": password,
292
377
  "config_parameters": {},
378
+ "network": {},
293
379
  }
294
380
 
381
+ if enable_backups:
382
+ backup_start_date = backup_start_date.strftime("%Y-%m-%dT%H:%M:%SZ")
383
+ payload["auto_backups"] = {
384
+ "copy_count": backup_keep,
385
+ "creation_started_at": backup_start_date,
386
+ "interval": backup_interval,
387
+ "day_of_week": backup_day_of_week,
388
+ }
389
+
390
+ if network_id:
391
+ payload["network"]["id"] = network_id
392
+ if private_ip:
393
+ net = IPv4Network(
394
+ client.get_vpc(network_id).json()["vpc"]["subnet_v4"]
395
+ )
396
+ if IPv4Address(private_ip) >= net.network_address + 4:
397
+ payload["network"]["ip"] = private_ip
398
+ else:
399
+ # First 3 addresses is reserved by Timeweb Cloud for gateway and future use.
400
+ sys.exit(
401
+ f"Error: Private address '{private_ip}' is not allowed. "
402
+ "IP must be at least the fourth in order in the network."
403
+ )
404
+ if public_ip:
405
+ try:
406
+ _ = IPv4Address(public_ip)
407
+ payload["network"]["floating_ip"] = public_ip
408
+ except ValueError:
409
+ sys.exit(f"Error: '{public_ip}' is not valid IPv4 address.")
410
+ else:
411
+ # New public IPv4 address will be automatically requested with
412
+ # correct availability zone. This is an official dirty hack.
413
+ if no_public_ip is False:
414
+ payload["network"]["floating_ip"] = "create_ip"
415
+
416
+ if login:
417
+ print(
418
+ "--login is deprecated use --user-login instead", file=sys.stderr
419
+ )
420
+ user_login = login
421
+
422
+ if password:
423
+ print(
424
+ "--password is deprecated use --user-password instead",
425
+ file=sys.stderr,
426
+ )
427
+ user_password = password
428
+
429
+ if user_password and not user_login:
430
+ sys.exit("Error: --user-login required if --user-password is set.")
431
+
432
+ if user_login:
433
+ if not user_password:
434
+ user_password = typer.prompt(
435
+ "Database user password",
436
+ hide_input=True,
437
+ confirmation_prompt=True,
438
+ )
439
+ payload["admin"] = {
440
+ "login": user_login,
441
+ "password": user_password,
442
+ "host": user_host,
443
+ "privileges": user_privileges,
444
+ "description": user_desc or "",
445
+ }
446
+
447
+ if db_name:
448
+ payload["instance"] = {
449
+ "name": db_name,
450
+ "description": db_desc or "",
451
+ }
452
+
295
453
  if params:
296
454
  payload["config_parameters"] = set_params(params)
297
455
 
298
456
  if project_id:
299
- if not project_id in [
300
- prj["id"] for prj in client.get_projects().json()["projects"]
301
- ]:
302
- sys.exit(f"Wrong project ID: Project '{project_id}' not found.")
457
+ payload["project_id"] = project_id
303
458
 
304
- response = client.create_database(**payload)
305
-
306
- # Add created DB to project if set
307
- if project_id:
308
- client.add_database_to_project(
309
- response.json()["db"]["id"],
310
- project_id,
311
- )
459
+ response = client.create_database2(**payload)
312
460
 
313
461
  fmt.printer(
314
462
  response,
@@ -537,4 +685,344 @@ def database_backup_restore(
537
685
  # $ twc database backup schedule #
538
686
  # ------------------------------------------------------------- #
539
687
 
540
- # FUTURE: Waiting for API endpoint release.
688
+
689
+ def print_autobackup_settings(response: Response):
690
+ """Print backup settings info."""
691
+ table = fmt.Table()
692
+ settings = response.json()["auto_backups_settings"]
693
+ translated_keys = {
694
+ "copy_count": "Keep copies",
695
+ "creation_start_at": "Backup start date",
696
+ "is_enabled": "Enabled",
697
+ "interval": "Interval",
698
+ "day_of_week": "Day of week",
699
+ }
700
+ for key in settings.keys():
701
+ table.row([translated_keys[key], ":", settings[key]])
702
+ table.print()
703
+
704
+
705
+ @database_backup.command("schedule")
706
+ def database_backup_schedule(
707
+ db_id: int,
708
+ verbose: Optional[bool] = verbose_option,
709
+ config: Optional[Path] = config_option,
710
+ profile: Optional[str] = profile_option,
711
+ output_format: Optional[str] = output_format_option,
712
+ status: bool = typer.Option(
713
+ False,
714
+ "--status",
715
+ help="Display automatic backups status.",
716
+ ),
717
+ enable: Optional[bool] = typer.Option(
718
+ None,
719
+ "--enable/--disable",
720
+ show_default=False,
721
+ help="Enable or disable automatic backups.",
722
+ ),
723
+ keep: int = typer.Option(
724
+ 1,
725
+ show_default=True,
726
+ help="Number of backups to keep.",
727
+ ),
728
+ start_date: datetime = typer.Option(
729
+ date.today().strftime("%Y-%m-%d"),
730
+ formats=["%Y-%m-%d"],
731
+ show_default=False,
732
+ help="Start date of the first backup creation [default: today].",
733
+ ),
734
+ interval: BackupInterval = typer.Option(
735
+ BackupInterval.DAY.value,
736
+ "--interval",
737
+ help="Backup interval.",
738
+ ),
739
+ day_of_week: Optional[int] = typer.Option(
740
+ 1,
741
+ min=1,
742
+ max=7,
743
+ help="The day of the week on which backups will be created."
744
+ " NOTE: This option works only with interval 'week'."
745
+ " First day of week is monday.",
746
+ ),
747
+ ):
748
+ """Manage database cluster automatic backup settings."""
749
+ client = create_client(config, profile)
750
+
751
+ if status:
752
+ response = client.get_database_autobackup_settings(db_id)
753
+ fmt.printer(
754
+ response,
755
+ output_format=output_format,
756
+ func=print_autobackup_settings,
757
+ )
758
+ if response.json()["auto_backups_settings"]["is_enabled"]:
759
+ sys.exit(0)
760
+ else:
761
+ sys.exit(1)
762
+
763
+ start_date = start_date.strftime("%Y-%m-%dT%H:%M:%SZ")
764
+
765
+ response = client.update_database_autobackup_settings(
766
+ db_id,
767
+ is_enabled=enable,
768
+ copy_count=keep,
769
+ creation_start_at=start_date,
770
+ interval=interval,
771
+ day_of_week=day_of_week,
772
+ )
773
+ fmt.printer(
774
+ response,
775
+ output_format=output_format,
776
+ func=print_autobackup_settings,
777
+ )
778
+
779
+
780
+ # ------------------------------------------------------------- #
781
+ # $ twc database user list #
782
+ # ------------------------------------------------------------- #
783
+
784
+
785
+ def _print_database_users(response: Response):
786
+ users = response.json()["admins"]
787
+ table = fmt.Table()
788
+ table.header(["ID", "LOGIN", "HOST", "CREATED"])
789
+ for user in users:
790
+ table.row(
791
+ [
792
+ user["id"],
793
+ user["login"],
794
+ user["host"],
795
+ user["created_at"],
796
+ ]
797
+ )
798
+ table.print()
799
+
800
+
801
+ @database_user.command("list", "ls")
802
+ def database_user_list(
803
+ db_id: int,
804
+ verbose: Optional[bool] = verbose_option,
805
+ config: Optional[Path] = config_option,
806
+ profile: Optional[str] = profile_option,
807
+ output_format: Optional[str] = output_format_option,
808
+ ):
809
+ """List database users."""
810
+ client = create_client(config, profile)
811
+ response = client.get_database_users(db_id)
812
+ fmt.printer(
813
+ response,
814
+ output_format=output_format,
815
+ func=_print_database_users,
816
+ )
817
+
818
+
819
+ # ------------------------------------------------------------- #
820
+ # $ twc database user get #
821
+ # ------------------------------------------------------------- #
822
+
823
+
824
+ def _print_database_user(response: Response):
825
+ user = response.json()["admin"]
826
+ out = textwrap.dedent(
827
+ f"""
828
+ Login: {user['login']}
829
+ Host: {user['host']}
830
+ Created: {user['created_at']}
831
+ Description: {user['description']}
832
+ """
833
+ ).strip()
834
+ print(out)
835
+ print()
836
+ table = fmt.Table()
837
+ table.header(["INSTANCE_ID", "PRIVILEGES"])
838
+ for instance in user["instances"]:
839
+ table.row(
840
+ [
841
+ instance["instance_id"],
842
+ ", ".join(instance["privileges"]),
843
+ ]
844
+ )
845
+ table.print()
846
+
847
+
848
+ @database_user.command("get")
849
+ def database_user_get(
850
+ db_id: int,
851
+ user_id: int,
852
+ verbose: Optional[bool] = verbose_option,
853
+ config: Optional[Path] = config_option,
854
+ profile: Optional[str] = profile_option,
855
+ output_format: Optional[str] = output_format_option,
856
+ ):
857
+ """Get database user."""
858
+ client = create_client(config, profile)
859
+ response = client.get_database_user(db_id, user_id)
860
+ fmt.printer(
861
+ response,
862
+ output_format=output_format,
863
+ func=_print_database_user,
864
+ )
865
+
866
+
867
+ # ------------------------------------------------------------- #
868
+ # $ twc database user create #
869
+ # ------------------------------------------------------------- #
870
+
871
+
872
+ @database_user.command("create")
873
+ def database_user_create(
874
+ db_id: int,
875
+ verbose: Optional[bool] = verbose_option,
876
+ config: Optional[Path] = config_option,
877
+ profile: Optional[str] = profile_option,
878
+ output_format: Optional[str] = output_format_option,
879
+ login: str = typer.Option(..., help="User login."),
880
+ password: str = typer.Option(
881
+ ...,
882
+ prompt=True,
883
+ hide_input=True,
884
+ confirmation_prompt=True,
885
+ help="User password.",
886
+ ),
887
+ host: Optional[str] = typer.Option(
888
+ "%", help="User host for MySQL, Postgres"
889
+ ),
890
+ instance_id: Optional[int] = typer.Option(
891
+ None,
892
+ help="The specific instance ID to which the privileges will be "
893
+ "applied. If not specified, the privileges will be applied to "
894
+ "all available instances.",
895
+ ),
896
+ privileges: Optional[str] = typer.Option(
897
+ [],
898
+ help="Comma-separated list of user privileges.",
899
+ callback=dbms_parameters_callback,
900
+ ),
901
+ desc: Optional[str] = typer.Option(None, help="Comment for user."),
902
+ ):
903
+ """Create database users."""
904
+ client = create_client(config, profile)
905
+ response = client.create_database_user(
906
+ db_id=db_id,
907
+ login=login,
908
+ password=password,
909
+ privileges=privileges,
910
+ host=host,
911
+ instance_id=instance_id,
912
+ description=desc,
913
+ )
914
+ fmt.printer(
915
+ response,
916
+ output_format=output_format,
917
+ func=lambda response: print(response.json()["admin"]["id"]),
918
+ )
919
+
920
+
921
+ # ------------------------------------------------------------- #
922
+ # $ twc database user remove #
923
+ # ------------------------------------------------------------- #
924
+
925
+
926
+ @database_user.command("remove", "rm")
927
+ def database_user_remove(
928
+ db_id: int,
929
+ user_id: int,
930
+ verbose: Optional[bool] = verbose_option,
931
+ config: Optional[Path] = config_option,
932
+ profile: Optional[str] = profile_option,
933
+ ):
934
+ """Delete database user."""
935
+ client = create_client(config, profile)
936
+ response = client.get_database_user(db_id, user_id)
937
+ if response.status_code == 204:
938
+ print(user_id)
939
+ else:
940
+ sys.exit(fmt.printer(response))
941
+
942
+
943
+ # ------------------------------------------------------------- #
944
+ # $ twc database instance list #
945
+ # ------------------------------------------------------------- #
946
+
947
+
948
+ def _print_database_instances(response: Response):
949
+ instances = response.json()["instances"]
950
+ table = fmt.Table()
951
+ table.header(["ID", "NAME", "CREATED", "DESCRIPTION"])
952
+ for i in instances:
953
+ table.row(
954
+ [
955
+ i["id"],
956
+ i["name"],
957
+ i["created_at"],
958
+ i["description"],
959
+ ]
960
+ )
961
+ table.print()
962
+
963
+
964
+ @database_instance.command("list", "ls")
965
+ def database_instance_list(
966
+ db_id: int,
967
+ verbose: Optional[bool] = verbose_option,
968
+ config: Optional[Path] = config_option,
969
+ profile: Optional[str] = profile_option,
970
+ output_format: Optional[str] = output_format_option,
971
+ ):
972
+ """List databases in database cluster."""
973
+ client = create_client(config, profile)
974
+ response = client.get_database_instances(db_id)
975
+ fmt.printer(
976
+ response,
977
+ output_format=output_format,
978
+ func=_print_database_instances,
979
+ )
980
+
981
+
982
+ # ------------------------------------------------------------- #
983
+ # $ twc database instance create #
984
+ # ------------------------------------------------------------- #
985
+
986
+
987
+ @database_instance.command("create")
988
+ def database_instance_create(
989
+ db_id: int,
990
+ name: str,
991
+ verbose: Optional[bool] = verbose_option,
992
+ config: Optional[Path] = config_option,
993
+ profile: Optional[str] = profile_option,
994
+ output_format: Optional[str] = output_format_option,
995
+ desc: Optional[str] = typer.Option(None, help="Comment for database."),
996
+ ):
997
+ """Create database in database cluster."""
998
+ client = create_client(config, profile)
999
+ response = client.create_database_instance(
1000
+ db_id, name=name, description=desc
1001
+ )
1002
+ fmt.printer(
1003
+ response,
1004
+ output_format=output_format,
1005
+ func=lambda response: print(response.json()["instance"]["id"]),
1006
+ )
1007
+
1008
+
1009
+ # ------------------------------------------------------------- #
1010
+ # $ twc database instance remove #
1011
+ # ------------------------------------------------------------- #
1012
+
1013
+
1014
+ @database_instance.command("remove", "rm")
1015
+ def database_instance_remove(
1016
+ db_id: int,
1017
+ instance_id: int,
1018
+ verbose: Optional[bool] = verbose_option,
1019
+ config: Optional[Path] = config_option,
1020
+ profile: Optional[str] = profile_option,
1021
+ ):
1022
+ """Delete database from cluster."""
1023
+ client = create_client(config, profile)
1024
+ response = client.get_database_user(db_id, instance_id)
1025
+ if response.status_code == 204:
1026
+ print(instance_id)
1027
+ else:
1028
+ sys.exit(fmt.printer(response))
twc/commands/server.py CHANGED
@@ -558,7 +558,10 @@ def server_create(
558
558
  help="Enable LAN.",
559
559
  hidden=True,
560
560
  ),
561
- network: Optional[str] = typer.Option(None, help="Private network ID."),
561
+ network: Optional[str] = typer.Option(
562
+ None, hidden=True, help="Private network ID."
563
+ ),
564
+ network_id: Optional[str] = typer.Option(None, help="Private network ID."),
562
565
  private_ip: Optional[str] = typer.Option(
563
566
  None, help="Private IPv4 address."
564
567
  ),
@@ -631,14 +634,21 @@ def server_create(
631
634
  )
632
635
 
633
636
  # Set network parameters
634
- if nat_mode or private_ip:
635
- if not network:
636
- sys.exit("Error: Pass '--network' option first.")
637
637
  if network:
638
- payload["network"]["id"] = network
638
+ print(
639
+ "Option --network is deprecated and will be removed soon, "
640
+ "use --network-id instead",
641
+ file=sys.stderr,
642
+ )
643
+ network_id = network
644
+ if nat_mode or private_ip:
645
+ if not network_id:
646
+ sys.exit("Error: Pass '--network-id' option first.")
647
+ if network_id:
648
+ payload["network"]["id"] = network_id
639
649
  if private_ip:
640
650
  net = IPv4Network(
641
- client.get_vpc(network).json()["vpc"]["subnet_v4"]
651
+ client.get_vpc(network_id).json()["vpc"]["subnet_v4"]
642
652
  )
643
653
  if IPv4Address(private_ip) >= net.network_address + 4:
644
654
  payload["network"]["ip"] = private_ip
twc/vars.py CHANGED
@@ -8,7 +8,15 @@ expand or other infrastructure or product changes occur.
8
8
  CONTROL_PANEL_URL = "https://timeweb.cloud/my"
9
9
 
10
10
  # Location specific parameters. May change later.
11
- REGIONS_WITH_IPV6 = ["ru-1", "pl-1"]
11
+ REGIONS_WITH_IPV6 = ["ru-1", "ru-3", "pl-1", "nl-1"]
12
12
  REGIONS_WITH_IMAGES = ["ru-1", "ru-3", "kz-1", "pl-1", "nl-1"]
13
- REGIONS_WITH_LAN = ["ru-1", "ru-3", "nl-1", "pl-1"]
14
- ZONES_WITH_LAN = ["spb-1", "spb-3", "spb-4", "msk-1", "ams-1", "gdn-1"]
13
+ REGIONS_WITH_LAN = ["ru-1", "ru-3", "nl-1", "pl-1", "de-1"]
14
+ ZONES_WITH_LAN = [
15
+ "spb-1",
16
+ "spb-3",
17
+ "spb-4",
18
+ "msk-1",
19
+ "ams-1",
20
+ "gdn-1",
21
+ "fra-1",
22
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: twc-cli
3
- Version: 2.9.1
3
+ Version: 2.10.0
4
4
  Summary: Timeweb Cloud Command Line Interface.
5
5
  Home-page: https://github.com/timeweb-cloud/twc
6
6
  License: MIT
@@ -1,36 +1,36 @@
1
- CHANGELOG.md,sha256=bNHVOSTbaShheTcY3nR3mF6h4-rIRMLYFG7cf4mE8dE,27220
1
+ CHANGELOG.md,sha256=9i0MuhpW4DSXw848ZGwm7AdA-XA9rYyOigBRT2T76cU,29632
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=LA3Qa1Id0E6VF_GJX3GIwgV7Jrh2YzyspaQDWUoXEGM,442
5
+ twc/__version__.py,sha256=hlCsp4lDiHyiPjydCpKPl4urI6KoC8-yx7qcaoAret8,443
6
6
  twc/api/__init__.py,sha256=SXew0Fe51M7nRBNQaaLRH4NjnRHkQUn7J26OCkQsftA,128
7
7
  twc/api/base.py,sha256=QRefnIgmlbz8n37GLBKeAK1AtzkcNo1IFjZgHDDECJ4,7912
8
- twc/api/client.py,sha256=NFS8Jz64b-FHGy2QNwtzxyHGK7tanTquhTSJ6IcH3jg,59447
8
+ twc/api/client.py,sha256=T74KLeKHyAxzKHkpdNF-VOkfiAwnu7us61xzPosV5_o,64366
9
9
  twc/api/exceptions.py,sha256=UzK3pKRffcXlhnkPy6MDjP_DygVoV17DuZ_mdNbOzts,2369
10
- twc/api/types.py,sha256=SJCKJsjdk9dFLgABO2c83oIw6ht-ARwPKmHKVZrbiN8,5419
10
+ twc/api/types.py,sha256=uagnD3TPpoJFYUFK6HfHQPlEXs2GLxuJdjhNIbraXwM,5374
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
14
14
  twc/commands/balancer.py,sha256=QAouc74ZT5go11gB1vjjfYtd1luTmWrfpACPwokZ5sU,20278
15
15
  twc/commands/common.py,sha256=Wph8cVogUNNvc456SQrASb7mv7G88I8ETwHgISVjLQQ,8282
16
- twc/commands/config.py,sha256=hoRtxn2VRxIsuy9vgO6yd0Cu15Rbl-uYMZeU0Ix7dG0,8797
17
- twc/commands/database.py,sha256=2NZ-TyRBkFgfYJyUdZUcfdqSaX7QVdWDU4k_yQNtUvo,16052
16
+ twc/commands/config.py,sha256=xHNEZVmM60c9dApLfNsj78sXZk6VsFwPdVIHO9r8xks,8802
17
+ twc/commands/database.py,sha256=dX0z8LLpVTfCM7QNe0tU1DH7IWIwaJZBdSqHs6hWzNQ,31446
18
18
  twc/commands/domain.py,sha256=BIg5k0TDQ-iWnhjuAHaWlZBB0bfaZgqZ2EWZGk3BICA,17154
19
19
  twc/commands/firewall.py,sha256=KNolqbi2rsppOZwbs_j3yoZQt-0wKbj1JPGiZdfGxDE,27439
20
20
  twc/commands/floating_ip.py,sha256=G9nD5BbHCZcuytbzeneDJWQDhd8c8WRtq9pAfwI9m7E,8747
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=1Shwy-rTHeingiEl2EErkXIpm5TDWyBYO3fJl794Jos,71982
24
+ twc/commands/server.py,sha256=5yb_pdB5BOoj_UAWdMxiCtuGdRBgcllkStMqyRSlx9k,72315
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
28
28
  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
- twc/vars.py,sha256=fva3O2leMGtExb1aWiAk6sOV0O8et9_kEyRpYYIZ7CM,543
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,,
31
+ twc/vars.py,sha256=XRtd77WWy2Ym2Yaodx84CKqKu2272O-Bj_fMFCjzuwg,607
32
+ twc_cli-2.10.0.dist-info/COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
33
+ twc_cli-2.10.0.dist-info/METADATA,sha256=ZTVW8woDo4IdmAsKC-Okd4tHJ6CKKNgBQ7pnKw5Yc98,2653
34
+ twc_cli-2.10.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
35
+ twc_cli-2.10.0.dist-info/entry_points.txt,sha256=tmTaVRhm8BkNrXC_9XJMum7O9wFVOvkXcBetxmahWvE,40
36
+ twc_cli-2.10.0.dist-info/RECORD,,