twc-cli 1.1.0__py3-none-any.whl → 1.3.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.

@@ -0,0 +1,633 @@
1
+ """Database management commands."""
2
+
3
+ import re
4
+ import sys
5
+
6
+ import click
7
+ from click_aliases import ClickAliasedGroup
8
+
9
+ from twc import fmt
10
+ from twc.utils import merge_dicts
11
+ from . import (
12
+ create_client,
13
+ handle_request,
14
+ set_value_from_config,
15
+ options,
16
+ debug,
17
+ GLOBAL_OPTIONS,
18
+ OUTPUT_FORMAT_OPTION,
19
+ )
20
+ from .project import (
21
+ get_default_project_id,
22
+ _project_list,
23
+ _project_resource_move,
24
+ _project_resource_list_databases,
25
+ )
26
+
27
+
28
+ @handle_request
29
+ def _database_list(client, *args, **kwargs):
30
+ return client.get_databases(*args, **kwargs)
31
+
32
+
33
+ @handle_request
34
+ def _database_get(client, *args, **kwargs):
35
+ return client.get_database(*args, **kwargs)
36
+
37
+
38
+ @handle_request
39
+ def _database_create(client, *args, **kwargs):
40
+ return client.create_database(*args, **kwargs)
41
+
42
+
43
+ @handle_request
44
+ def _database_remove(client, *args, **kwargs):
45
+ return client.delete_database(*args, **kwargs)
46
+
47
+
48
+ @handle_request
49
+ def _database_set(client, *args, **kwargs):
50
+ return client.update_database(*args, **kwargs)
51
+
52
+
53
+ @handle_request
54
+ def _database_backup_list(client, *args, **kwargs):
55
+ return client.get_database_backups(*args, **kwargs)
56
+
57
+
58
+ @handle_request
59
+ def _database_backup_create(client, *args, **kwargs):
60
+ return client.create_database_backup(*args, **kwargs)
61
+
62
+
63
+ @handle_request
64
+ def _database_backup_remove(client, *args, **kwargs):
65
+ return client.delete_database_backup(*args, **kwargs)
66
+
67
+
68
+ @handle_request
69
+ def _database_backup_restore(client, *args, **kwargs):
70
+ return client.restore_database_backup(*args, **kwargs)
71
+
72
+
73
+ @handle_request
74
+ def _database_list_presets(client, *args, **kwargs):
75
+ return client.get_database_presets(*args, **kwargs)
76
+
77
+
78
+ # ------------------------------------------------------------- #
79
+ # $ twc database #
80
+ # ------------------------------------------------------------- #
81
+
82
+
83
+ @click.group("database", cls=ClickAliasedGroup)
84
+ @options(GLOBAL_OPTIONS[:2])
85
+ def database():
86
+ """Manage databases."""
87
+
88
+
89
+ # ------------------------------------------------------------- #
90
+ # $ twc database list #
91
+ # ------------------------------------------------------------- #
92
+
93
+
94
+ def print_databases(response: object, filters: str):
95
+ if filters:
96
+ dbs = fmt.filter_list(response.json()["dbs"], filters)
97
+ else:
98
+ dbs = response.json()["dbs"]
99
+
100
+ table = fmt.Table()
101
+ table.header(
102
+ [
103
+ "ID",
104
+ "NAME",
105
+ "STATUS",
106
+ "TYPE",
107
+ "IPV4",
108
+ "INTERNAL IP",
109
+ ]
110
+ )
111
+ for db in dbs:
112
+ table.row(
113
+ [
114
+ db["id"],
115
+ db["name"],
116
+ db["status"],
117
+ db["type"],
118
+ db["ip"],
119
+ db["local_ip"],
120
+ ]
121
+ )
122
+ table.print()
123
+
124
+
125
+ @database.command("list", aliases=["ls"], help="List databases.")
126
+ @options(GLOBAL_OPTIONS)
127
+ @options(OUTPUT_FORMAT_OPTION)
128
+ @click.option(
129
+ "--limit",
130
+ type=int,
131
+ default=500,
132
+ show_default=True,
133
+ help="Items to display.",
134
+ )
135
+ @click.option("--filter", "-f", "filters", default="", help="Filter output.")
136
+ def database_list(config, profile, verbose, output_format, limit, filters):
137
+ client = create_client(config, profile)
138
+ response = _database_list(client, limit=limit)
139
+ fmt.printer(
140
+ response,
141
+ output_format=output_format,
142
+ filters=filters,
143
+ func=print_databases,
144
+ )
145
+
146
+
147
+ # ------------------------------------------------------------- #
148
+ # $ twc database get #
149
+ # ------------------------------------------------------------- #
150
+
151
+
152
+ def print_database(response: object):
153
+ db = response.json()["db"]
154
+ table = fmt.Table()
155
+ table.header(
156
+ [
157
+ "ID",
158
+ "NAME",
159
+ "STATUS",
160
+ "TYPE",
161
+ "IPV4",
162
+ "INTERNAL IP",
163
+ ]
164
+ )
165
+ table.row(
166
+ [
167
+ db["id"],
168
+ db["name"],
169
+ db["status"],
170
+ db["type"],
171
+ db["ip"],
172
+ db["local_ip"],
173
+ ]
174
+ )
175
+ table.print()
176
+
177
+
178
+ @database.command("get", help="Get database.")
179
+ @options(GLOBAL_OPTIONS)
180
+ @options(OUTPUT_FORMAT_OPTION)
181
+ @click.option(
182
+ "--status",
183
+ is_flag=True,
184
+ help="Display status and exit with 0 if status is 'started'.",
185
+ )
186
+ @click.argument("db_id", type=int, required=True)
187
+ def database_get(config, profile, verbose, output_format, status, db_id):
188
+ client = create_client(config, profile)
189
+ response = _database_get(client, db_id)
190
+ if status:
191
+ _status = response.json()["db"]["status"]
192
+ click.echo(_status)
193
+ if _status == "started":
194
+ sys.exit(0)
195
+ else:
196
+ sys.exit(1)
197
+ fmt.printer(
198
+ response,
199
+ output_format=output_format,
200
+ func=print_database,
201
+ )
202
+
203
+
204
+ # ------------------------------------------------------------- #
205
+ # $ twc database list-presets #
206
+ # ------------------------------------------------------------- #
207
+
208
+
209
+ def print_dbs_presets(response: object, filters: str):
210
+ if filters:
211
+ presets = fmt.filter_list(
212
+ response.json()["databases_presets"], filters
213
+ )
214
+ else:
215
+ presets = response.json()["databases_presets"]
216
+
217
+ table = fmt.Table()
218
+ table.header(
219
+ [
220
+ "ID",
221
+ "REGION",
222
+ "PRICE",
223
+ "CPU",
224
+ "RAM",
225
+ "DISK",
226
+ "TYPE",
227
+ ]
228
+ )
229
+ for preset in presets:
230
+ table.row(
231
+ [
232
+ preset["id"],
233
+ preset["location"],
234
+ preset["price"],
235
+ preset["cpu"],
236
+ str(round(preset["ram"] / 1024)) + "G",
237
+ str(round(preset["disk"] / 1024)) + "G",
238
+ preset["type"],
239
+ ]
240
+ )
241
+ table.print()
242
+
243
+
244
+ @database.command(
245
+ "list-presets", aliases=["lp"], help="List database presets."
246
+ )
247
+ @options(GLOBAL_OPTIONS)
248
+ @options(OUTPUT_FORMAT_OPTION)
249
+ @click.option("--filter", "-f", "filters", default="", help="Filter output.")
250
+ @click.option("--region", help="Use region (location).")
251
+ def database_list_presets(
252
+ config, profile, verbose, output_format, filters, region
253
+ ):
254
+ if filters:
255
+ filters = filters.replace("region", "location")
256
+ if region:
257
+ if filters:
258
+ filters = filters + f",location:{region}"
259
+ else:
260
+ filters = f"location:{region}"
261
+
262
+ client = create_client(config, profile)
263
+ response = _database_list_presets(client)
264
+ fmt.printer(
265
+ response,
266
+ output_format=output_format,
267
+ filters=filters,
268
+ func=print_dbs_presets,
269
+ )
270
+
271
+
272
+ # ------------------------------------------------------------- #
273
+ # $ twc database create #
274
+ # ------------------------------------------------------------- #
275
+
276
+
277
+ def set_params(params: tuple) -> dict:
278
+ parameters = {}
279
+ for param in params:
280
+ if re.match(r"^([a-z_]+)=([0-9a-zA-Z]+)$", param):
281
+ parameter, value = param.split("=")
282
+ if value.isdigit():
283
+ value = int(value)
284
+ parameters[parameter] = value
285
+ else:
286
+ raise click.BadParameter(
287
+ f"'{param}': Parameter can contain only digits,"
288
+ " latin letters and underscore."
289
+ )
290
+ return parameters
291
+
292
+
293
+ @database.command("create", help="Create new database.")
294
+ @options(GLOBAL_OPTIONS)
295
+ @options(OUTPUT_FORMAT_OPTION)
296
+ @click.option(
297
+ "--preset-id", type=int, required=True, help="Database preset ID."
298
+ )
299
+ @click.option("--name", required=True, help="Database display name.")
300
+ @click.option(
301
+ "--type",
302
+ "dbms",
303
+ type=click.Choice(["mysql8", "mysql5", "postgres", "redis", "mongodb"]),
304
+ required=True,
305
+ help="DBMS.",
306
+ )
307
+ @click.option(
308
+ "--hash-type",
309
+ type=click.Choice(["caching_sha2", "mysql_native"]),
310
+ default="caching_sha2",
311
+ show_default=True,
312
+ help="Authentication plugin for MySQL.",
313
+ )
314
+ @click.option(
315
+ "--param",
316
+ "params",
317
+ multiple=True,
318
+ help="Database parameters, can be multiple.",
319
+ )
320
+ @click.option(
321
+ "--project-id",
322
+ type=int,
323
+ default=None,
324
+ envvar="TWC_PROJECT",
325
+ callback=set_value_from_config,
326
+ help="Add database to specific project.",
327
+ )
328
+ @click.option("--login", default=None, help="Database user login.")
329
+ @click.option(
330
+ "--password",
331
+ prompt="Set database user password",
332
+ hide_input=True,
333
+ confirmation_prompt=True,
334
+ help="Database user password.",
335
+ )
336
+ def database_create(
337
+ config,
338
+ profile,
339
+ verbose,
340
+ output_format,
341
+ preset_id,
342
+ name,
343
+ dbms,
344
+ hash_type,
345
+ params,
346
+ login,
347
+ password,
348
+ project_id,
349
+ ):
350
+ # pylint: disable=too-many-locals
351
+
352
+ client = create_client(config, profile)
353
+
354
+ if dbms == "mysql8": # alias mysql8 for mysql
355
+ dbms = "mysql"
356
+
357
+ # Check preset_id
358
+ for preset in _database_list_presets(client).json()["databases_presets"]:
359
+ if preset["id"] == preset_id:
360
+ _dbms = re.sub(
361
+ r"[0-9]+", "", dbms
362
+ ) # transform 'mysql5' to 'mysql', etc.
363
+ if not preset["type"].startswith(_dbms):
364
+ sys.exit(
365
+ f"Error: DBMS '{dbms}' doesn't match with preset_id type."
366
+ )
367
+
368
+ payload = {
369
+ "dbms": dbms,
370
+ "preset_id": preset_id,
371
+ "name": name,
372
+ "hash_type": hash_type,
373
+ "login": login,
374
+ "password": password,
375
+ "config_parameters": {},
376
+ }
377
+
378
+ if params:
379
+ payload["config_parameters"] = set_params(params)
380
+
381
+ if project_id:
382
+ debug("Check project_id")
383
+ projects = _project_list(client).json()["projects"]
384
+ if not project_id in [prj["id"] for prj in projects]:
385
+ raise click.BadParameter("Wrong project ID.")
386
+
387
+ response = _database_create(client, **payload)
388
+
389
+ # Add created DB to project if set
390
+ if project_id:
391
+ src_project = get_default_project_id(client)
392
+ # Make useless request to avoid API bug (409 resource_not_found)
393
+ _r = _project_resource_list_databases(client, src_project)
394
+ new_db_id = response.json()["db"]["id"]
395
+ debug(f"Add DB '{new_db_id}' to project '{project_id}'")
396
+ project_resp = _project_resource_move(
397
+ client,
398
+ from_project=src_project,
399
+ to_project=project_id,
400
+ resource_id=new_db_id,
401
+ resource_type="database",
402
+ )
403
+ debug(project_resp.text)
404
+
405
+ fmt.printer(
406
+ response,
407
+ output_format=output_format,
408
+ func=lambda response: click.echo(response.json()["db"]["id"]),
409
+ )
410
+
411
+
412
+ # ------------------------------------------------------------- #
413
+ # $ twc database set #
414
+ # ------------------------------------------------------------- #
415
+
416
+
417
+ @database.command("set", help="Set database properties or parameters.")
418
+ @options(GLOBAL_OPTIONS)
419
+ @options(OUTPUT_FORMAT_OPTION)
420
+ @click.option(
421
+ "--preset-id", type=int, default=None, help="Database preset ID."
422
+ )
423
+ @click.option("--name", default=None, help="Database display name.")
424
+ @click.option(
425
+ "--param",
426
+ "params",
427
+ metavar="PARAMETER=VALUE",
428
+ multiple=True,
429
+ default=None,
430
+ help="Database parameters, can be multiple.",
431
+ )
432
+ @click.option(
433
+ "--password",
434
+ default=None,
435
+ help="Database user password.",
436
+ )
437
+ @click.option(
438
+ "--prompt-password",
439
+ is_flag=True,
440
+ help="Set database user password interactively.",
441
+ )
442
+ @click.option(
443
+ "--external-ip",
444
+ type=bool,
445
+ default=None,
446
+ help="Enable external IPv4 address.",
447
+ )
448
+ @click.argument("db_id", type=int, required=True)
449
+ def database_set(
450
+ config,
451
+ profile,
452
+ verbose,
453
+ output_format,
454
+ preset_id,
455
+ name,
456
+ params,
457
+ password,
458
+ prompt_password,
459
+ external_ip,
460
+ db_id,
461
+ ):
462
+ # pylint: disable=too-many-locals
463
+
464
+ client = create_client(config, profile)
465
+ old_state = _database_get(client, db_id).json()["db"]
466
+ new_params = {}
467
+
468
+ if prompt_password:
469
+ password = click.prompt(
470
+ "Set database user password",
471
+ hide_input=True,
472
+ confirmation_prompt=True,
473
+ )
474
+
475
+ payload = {
476
+ "preset_id": preset_id,
477
+ "name": name,
478
+ "password": password,
479
+ "config_parameters": {},
480
+ "external_ip": external_ip,
481
+ }
482
+
483
+ if params:
484
+ new_params = set_params(params)
485
+
486
+ if new_params:
487
+ payload["config_parameters"] = merge_dicts(
488
+ old_state["config_parameters"], new_params
489
+ )
490
+
491
+ response = _database_set(client, db_id, **payload)
492
+ fmt.printer(
493
+ response,
494
+ output_format=output_format,
495
+ func=lambda response: click.echo(response.json()["db"]["id"]),
496
+ )
497
+
498
+
499
+ # ------------------------------------------------------------- #
500
+ # $ twc database remove #
501
+ # ------------------------------------------------------------- #
502
+
503
+
504
+ @database.command("remove", aliases=["rm"], help="Remove database.")
505
+ @options(GLOBAL_OPTIONS)
506
+ @click.confirmation_option(prompt="This action cannot be undone. Continue?")
507
+ @click.argument("db_ids", nargs=-1, type=int, required=True)
508
+ def database_remove(config, profile, verbose, db_ids):
509
+ client = create_client(config, profile)
510
+ for db_id in db_ids:
511
+ response = _database_remove(client, db_id)
512
+
513
+ if response.status_code == 200:
514
+ del_hash = response.json()["database_delete"]["hash"]
515
+ del_code = click.prompt("Please enter confirmation code", type=int)
516
+ response = _database_remove(
517
+ client, db_id, delete_hash=del_hash, code=del_code
518
+ )
519
+
520
+ if response.status_code == 204:
521
+ click.echo(db_id)
522
+ else:
523
+ fmt.printer(response)
524
+
525
+
526
+ # ------------------------------------------------------------- #
527
+ # $ twc database backup #
528
+ # ------------------------------------------------------------- #
529
+
530
+
531
+ @database.group("backup", cls=ClickAliasedGroup)
532
+ @options(GLOBAL_OPTIONS[:2])
533
+ def db_backup():
534
+ """Manage database backups."""
535
+
536
+
537
+ # ------------------------------------------------------------- #
538
+ # $ twc database backup list #
539
+ # ------------------------------------------------------------- #
540
+
541
+
542
+ def print_db_backups(response: object):
543
+ backups = response.json()["backups"]
544
+ table = fmt.Table()
545
+ table.header(
546
+ [
547
+ "ID",
548
+ "DISK",
549
+ "CREATED",
550
+ "STATUS",
551
+ ]
552
+ )
553
+ for bak in backups:
554
+ table.row(
555
+ [
556
+ bak["id"],
557
+ bak["name"],
558
+ bak["created_at"],
559
+ bak["status"],
560
+ ]
561
+ )
562
+ table.print()
563
+
564
+
565
+ @db_backup.command("list", aliases=["ls"], help="List backups.")
566
+ @options(GLOBAL_OPTIONS)
567
+ @options(OUTPUT_FORMAT_OPTION)
568
+ @click.argument("db_id", type=int, required=True)
569
+ def database_backup_list(config, profile, verbose, output_format, db_id):
570
+ client = create_client(config, profile)
571
+ response = _database_backup_list(client, db_id)
572
+ fmt.printer(
573
+ response,
574
+ output_format=output_format,
575
+ func=print_db_backups,
576
+ )
577
+
578
+
579
+ # ------------------------------------------------------------- #
580
+ # $ twc database backup create #
581
+ # ------------------------------------------------------------- #
582
+
583
+
584
+ @db_backup.command("create", help="Backup database.")
585
+ @options(GLOBAL_OPTIONS)
586
+ @options(OUTPUT_FORMAT_OPTION)
587
+ @click.argument("db_id", type=int, required=True)
588
+ def database_backup_create(config, profile, verbose, output_format, db_id):
589
+ client = create_client(config, profile)
590
+ response = _database_backup_create(client, db_id)
591
+ fmt.printer(
592
+ response,
593
+ output_format=output_format,
594
+ func=lambda response: click.echo(response.json()["backup"]["id"]),
595
+ )
596
+
597
+
598
+ # ------------------------------------------------------------- #
599
+ # $ twc database backup remove #
600
+ # ------------------------------------------------------------- #
601
+
602
+
603
+ @db_backup.command("remove", aliases=["rm"], help="Remove backup.")
604
+ @options(GLOBAL_OPTIONS)
605
+ @click.argument("db_id", type=int, required=True)
606
+ @click.argument("backup_id", type=int, required=True)
607
+ @click.confirmation_option(prompt="This action cannot be undone. Continue?")
608
+ def database_backup_remove(config, profile, verbose, db_id, backup_id):
609
+ client = create_client(config, profile)
610
+ response = _database_backup_remove(client, db_id, backup_id)
611
+ if response.status_code == 204:
612
+ click.echo(backup_id)
613
+ else:
614
+ fmt.printer(response)
615
+
616
+
617
+ # ------------------------------------------------------------- #
618
+ # $ twc database backup restore #
619
+ # ------------------------------------------------------------- #
620
+
621
+
622
+ @db_backup.command("restore", help="Remove backup.")
623
+ @options(GLOBAL_OPTIONS)
624
+ @click.argument("db_id", type=int, required=True)
625
+ @click.argument("backup_id", type=int, required=True)
626
+ @click.confirmation_option(prompt="Data on target disk will lost. Continue?")
627
+ def database_backup_restore(config, profile, verbose, db_id, backup_id):
628
+ client = create_client(config, profile)
629
+ response = _database_backup_restore(client, db_id, backup_id)
630
+ if response.status_code == 204:
631
+ click.echo(backup_id)
632
+ else:
633
+ fmt.printer(response)
twc/commands/image.py CHANGED
@@ -271,7 +271,7 @@ def draw_progressbar(monitor):
271
271
  print("Bytes:", monitor.bytes_read)
272
272
 
273
273
 
274
- @image.command("upload", help="Upload image from disk of URL.")
274
+ @image.command("upload", help="Upload image from URL.")
275
275
  @options(GLOBAL_OPTIONS)
276
276
  @options(OUTPUT_FORMAT_OPTION)
277
277
  @click.option(
@@ -302,7 +302,7 @@ def draw_progressbar(monitor):
302
302
  )
303
303
  @click.option(
304
304
  "--location",
305
- type=click.Choice(["ru-1", "ru-2", "pl-1", "kz-1"]),
305
+ type=click.Choice(["ru-1", "ru-2", "pl-1", "kz-1", "nl-1"]),
306
306
  default="ru-1",
307
307
  show_default=True,
308
308
  help="Region to upload image.",
twc/commands/project.py CHANGED
@@ -10,6 +10,7 @@ from . import (
10
10
  create_client,
11
11
  handle_request,
12
12
  options,
13
+ debug,
13
14
  GLOBAL_OPTIONS,
14
15
  OUTPUT_FORMAT_OPTION,
15
16
  )
@@ -91,6 +92,14 @@ def _project_resource_move(client, *args, **kwargs):
91
92
  return client.move_resource_to_project(*args, **kwargs)
92
93
 
93
94
 
95
+ def get_default_project_id(client):
96
+ for prj in _project_list(client).json()["projects"]:
97
+ if prj["is_default"]:
98
+ debug(f"Default project ID is {prj['id']}")
99
+ return prj["id"]
100
+ return None
101
+
102
+
94
103
  # ------------------------------------------------------------- #
95
104
  # $ twc project #
96
105
  # ------------------------------------------------------------- #