twc-cli 1.0.0rc0__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.

twc/commands/server.py ADDED
@@ -0,0 +1,2148 @@
1
+ """Cloud Server management commands."""
2
+
3
+ import re
4
+ import os
5
+ import sys
6
+ import datetime
7
+
8
+ import click
9
+ from click_aliases import ClickAliasedGroup
10
+
11
+ from twc import fmt
12
+ from . import (
13
+ create_client,
14
+ handle_request,
15
+ options,
16
+ log,
17
+ confirm_action,
18
+ MutuallyExclusiveOption,
19
+ GLOBAL_OPTIONS,
20
+ OUTPUT_FORMAT_OPTION,
21
+ DEFAULT_CONFIGURATOR_ID,
22
+ REGIONS_WITH_CONFIGURATOR,
23
+ REGIONS_WITH_IPV6,
24
+ )
25
+ from .ssh_key import (
26
+ _ssh_key_list,
27
+ _ssh_key_new,
28
+ )
29
+
30
+
31
+ @handle_request
32
+ def _server_list(client, **kwargs):
33
+ return client.get_servers(**kwargs)
34
+
35
+
36
+ @handle_request
37
+ def _server_get(client, *args, **kwargs):
38
+ return client.get_server(*args, **kwargs)
39
+
40
+
41
+ @handle_request
42
+ def _server_create(client, *args, **kwargs):
43
+ return client.create_server(*args, **kwargs)
44
+
45
+
46
+ @handle_request
47
+ def _server_update(client, *args, **kwargs):
48
+ return client.update_server(*args, **kwargs)
49
+
50
+
51
+ @handle_request
52
+ def _server_action(client, *args, **kwargs):
53
+ return client.do_action_with_server(*args, **kwargs)
54
+
55
+
56
+ @handle_request
57
+ def _server_remove(client, *args):
58
+ return client.delete_server(*args)
59
+
60
+
61
+ @handle_request
62
+ def _get_server_configurators(client):
63
+ return client.get_server_configurators()
64
+
65
+
66
+ @handle_request
67
+ def _server_presets(client):
68
+ return client.get_server_presets()
69
+
70
+
71
+ @handle_request
72
+ def _server_software(client):
73
+ return client.get_server_software()
74
+
75
+
76
+ @handle_request
77
+ def _server_os_images(client):
78
+ return client.get_server_os_images()
79
+
80
+
81
+ @handle_request
82
+ def _server_logs(client, *args, **kwargs):
83
+ return client.get_server_logs(*args, **kwargs)
84
+
85
+
86
+ @handle_request
87
+ def _server_set_boot_mode(client, *args, **kwargs):
88
+ return client.set_server_boot_mode(*args, **kwargs)
89
+
90
+
91
+ @handle_request
92
+ def _server_set_nat_mode(client, *args, **kwargs):
93
+ return client.set_server_nat_mode(*args, **kwargs)
94
+
95
+
96
+ @handle_request
97
+ def _server_ip_list(client, *args):
98
+ return client.get_ips_by_server_id(*args)
99
+
100
+
101
+ @handle_request
102
+ def _server_ip_add(client, *args, **kwargs):
103
+ return client.add_ip_addr(*args, **kwargs)
104
+
105
+
106
+ @handle_request
107
+ def _server_ip_remove(client, *args, **kwargs):
108
+ return client.delete_ip_addr(*args, **kwargs)
109
+
110
+
111
+ @handle_request
112
+ def _server_ip_set_ptr(client, *args, **kwargs):
113
+ return client.update_ip_addr(*args, **kwargs)
114
+
115
+
116
+ @handle_request
117
+ def _server_disk_list(client, *args):
118
+ return client.get_disks_by_server_id(*args)
119
+
120
+
121
+ @handle_request
122
+ def _server_disk_get(client, *args):
123
+ return client.get_disk(*args)
124
+
125
+
126
+ @handle_request
127
+ def _server_disk_add(client, *args, **kwargs):
128
+ return client.add_disk(*args, **kwargs)
129
+
130
+
131
+ @handle_request
132
+ def _server_disk_remove(client, *args, **kwargs):
133
+ return client.delete_disk(*args, **kwargs)
134
+
135
+
136
+ @handle_request
137
+ def _server_disk_resize(client, *args, **kwargs):
138
+ return client.update_disk(*args, **kwargs)
139
+
140
+
141
+ @handle_request
142
+ def _server_disk_autobackup_status(client, *args, **kwargs):
143
+ return client.get_disk_autobackup_settings(*args, **kwargs)
144
+
145
+
146
+ @handle_request
147
+ def _server_disk_autobackup_update(client, *args, **kwargs):
148
+ return client.update_disk_autobackup_settings(*args, **kwargs)
149
+
150
+
151
+ @handle_request
152
+ def _server_backup_list(client, *args, **kwargs):
153
+ return client.get_disk_backups(*args, **kwargs)
154
+
155
+
156
+ @handle_request
157
+ def _server_backup_get(client, *args, **kwargs):
158
+ return client.get_disk_backup(*args, **kwargs)
159
+
160
+
161
+ @handle_request
162
+ def _server_backup_create(client, *args, **kwargs):
163
+ return client.create_disk_backup(*args, **kwargs)
164
+
165
+
166
+ @handle_request
167
+ def _server_backup_set_property(client, *args, **kwargs):
168
+ return client.update_disk_backup(*args, **kwargs)
169
+
170
+
171
+ @handle_request
172
+ def _server_backup_remove(client, *args, **kwargs):
173
+ return client.delete_disk_backup(*args, **kwargs)
174
+
175
+
176
+ @handle_request
177
+ def _server_backup_do_action(client, *args, **kwargs):
178
+ return client.do_action_with_disk_backup(*args, **kwargs)
179
+
180
+
181
+ # ------------------------------------------------------------- #
182
+ # $ twc server #
183
+ # ------------------------------------------------------------- #
184
+
185
+
186
+ @click.group("server", cls=ClickAliasedGroup)
187
+ @options(GLOBAL_OPTIONS[:2])
188
+ def server():
189
+ """Manage Cloud Servers."""
190
+
191
+
192
+ # ------------------------------------------------------------- #
193
+ # $ twc server list #
194
+ # ------------------------------------------------------------- #
195
+
196
+
197
+ def print_servers(response: object, filters: str, ids: bool):
198
+ if filters:
199
+ servers = fmt.filter_list(response.json()["servers"], filters)
200
+ else:
201
+ servers = response.json()["servers"]
202
+
203
+ if ids:
204
+ for _server in servers:
205
+ click.echo(_server["id"])
206
+ return
207
+
208
+ table = fmt.Table()
209
+ table.header(
210
+ [
211
+ "ID",
212
+ "NAME",
213
+ "REGION",
214
+ "STATUS",
215
+ "IPV4",
216
+ ]
217
+ )
218
+ for _server in servers:
219
+ for network in _server["networks"]:
220
+ if network["type"] == "public":
221
+ for ip_addr in network["ips"]:
222
+ if (
223
+ ip_addr["type"] == "ipv4"
224
+ and ip_addr["is_main"] is True
225
+ ):
226
+ main_ipv4 = ip_addr["ip"]
227
+ table.row(
228
+ [
229
+ _server["id"],
230
+ _server["name"],
231
+ _server["location"],
232
+ _server["status"],
233
+ main_ipv4,
234
+ ]
235
+ )
236
+ table.print()
237
+
238
+
239
+ @server.command("list", aliases=["ls"], help="List Cloud Servers.")
240
+ @options(GLOBAL_OPTIONS)
241
+ @options(OUTPUT_FORMAT_OPTION)
242
+ @click.option("--filter", "-f", "filters", default="", help="Filter output.")
243
+ @click.option("--region", help="Use region (location).")
244
+ @click.option("--limit", default=100, help="Limit [default: 100].")
245
+ @click.option("--ids", is_flag=True, help="Print only server IDs.")
246
+ def server_list(
247
+ config, profile, verbose, region, output_format, filters, limit, ids
248
+ ):
249
+ if filters:
250
+ filters = filters.replace("region", "location")
251
+ if region:
252
+ if filters:
253
+ filters = filters + f",location:{region}"
254
+ else:
255
+ filters = f"location:{region}"
256
+
257
+ client = create_client(config, profile)
258
+ response = _server_list(client, limit=limit)
259
+ fmt.printer(
260
+ response,
261
+ output_format=output_format,
262
+ filters=filters,
263
+ func=print_servers,
264
+ ids=ids,
265
+ )
266
+
267
+
268
+ # ------------------------------------------------------------- #
269
+ # $ twc server get #
270
+ # ------------------------------------------------------------- #
271
+
272
+
273
+ def print_server(response: object):
274
+ _server = response.json()["server"]
275
+
276
+ for network in _server["networks"]:
277
+ if network["type"] == "public":
278
+ for ip_addr in network["ips"]:
279
+ if ip_addr["type"] == "ipv4" and ip_addr["is_main"] is True:
280
+ main_ipv4 = ip_addr["ip"]
281
+
282
+ table = fmt.Table()
283
+ table.header(
284
+ [
285
+ "ID",
286
+ "NAME",
287
+ "REGION",
288
+ "STATUS",
289
+ "IPV4",
290
+ ]
291
+ )
292
+ table.row(
293
+ [
294
+ _server["id"],
295
+ _server["name"],
296
+ _server["location"],
297
+ _server["status"],
298
+ main_ipv4,
299
+ ]
300
+ )
301
+ table.print()
302
+
303
+
304
+ def print_server_networks(response: object):
305
+ networks = response.json()["server"]["networks"]
306
+ for network in networks:
307
+ if network["type"] == "public":
308
+ table = fmt.Table()
309
+ table.header(
310
+ [
311
+ "NETWORK",
312
+ "ADDRESS",
313
+ "VERSION",
314
+ "PTR",
315
+ "PRIMARY",
316
+ ]
317
+ )
318
+ for ip_addr in network["ips"]:
319
+ table.row(
320
+ [
321
+ network["type"],
322
+ ip_addr["ip"],
323
+ ip_addr["type"],
324
+ ip_addr["ptr"],
325
+ ip_addr["is_main"],
326
+ ]
327
+ )
328
+ table.print()
329
+ else:
330
+ table = fmt.Table()
331
+ for ip_addr in network["ips"]:
332
+ table.row([network["type"], ip_addr["ip"], ip_addr["type"]])
333
+ table.print()
334
+
335
+
336
+ def print_server_disks(response: object):
337
+ disks = response.json()["server"]["disks"]
338
+ table = fmt.Table()
339
+ table.header(
340
+ [
341
+ "ID",
342
+ "NAME",
343
+ "MOUNTED",
344
+ "SYSTEM",
345
+ "TYPE",
346
+ "STATUS",
347
+ "SIZE",
348
+ "USED",
349
+ ]
350
+ )
351
+ for disk in disks:
352
+ table.row(
353
+ [
354
+ disk["id"],
355
+ disk["system_name"],
356
+ disk["is_mounted"],
357
+ disk["is_system"],
358
+ disk["type"],
359
+ disk["status"],
360
+ str(round(disk["size"] / 1024)) + "G",
361
+ str(round(disk["used"] / 1024, 1)) + "G",
362
+ ]
363
+ )
364
+ table.print()
365
+
366
+
367
+ @server.command("get", help="Get Cloud Server.")
368
+ @options(GLOBAL_OPTIONS)
369
+ @options(OUTPUT_FORMAT_OPTION)
370
+ @click.option(
371
+ "--status",
372
+ is_flag=True,
373
+ help="Display status and exit with 0 if status is 'on'.",
374
+ )
375
+ @click.option("--networks", is_flag=True, help="Display networks.")
376
+ @click.option("--disks", is_flag=True, help="Display disks.")
377
+ @click.argument("server_id", type=int, required=True)
378
+ def server_get(
379
+ config, profile, verbose, output_format, status, networks, disks, server_id
380
+ ):
381
+ client = create_client(config, profile)
382
+ response = _server_get(client, server_id)
383
+ if status:
384
+ _status = response.json()["server"]["status"]
385
+ if _status == "on":
386
+ click.echo(_status)
387
+ sys.exit(0)
388
+ else:
389
+ sys.exit(_status)
390
+ if networks:
391
+ print_server_networks(response)
392
+ sys.exit(0)
393
+ if disks:
394
+ print_server_disks(response)
395
+ sys.exit(0)
396
+ fmt.printer(response, output_format=output_format, func=print_server)
397
+
398
+
399
+ # ------------------------------------------------------------- #
400
+ # $ twc server create #
401
+ # ------------------------------------------------------------- #
402
+
403
+
404
+ def get_os_name_by_id(os_images: list, os_id: int) -> str:
405
+ """Return human readable operating system name by OS ID::
406
+
407
+ 79 --> ubuntu-22.04
408
+ 65 --> windows-2012-standard
409
+ """
410
+ for os in os_images:
411
+ if os["id"] == os_id:
412
+ if os["family"] == "linux":
413
+ return f"{os['name']}-{os['version']}"
414
+ return f"{os['name']}-{os['version']}-{os['version_codename']}"
415
+ return None
416
+
417
+
418
+ def get_os_id_by_name(os_images: list, os_name: str) -> int:
419
+ """Return OS image ID by name. For example::
420
+
421
+ ubuntu-22.04 --> 79
422
+ windows-2012-standard --> 65
423
+ """
424
+ os_id = None
425
+
426
+ if os_name.startswith("windows-"):
427
+ name, version, codename = os_name.split("-")
428
+ for os in os_images:
429
+ if (
430
+ os["name"] == name
431
+ and os["version"] == version
432
+ and os["version_codename"] == codename
433
+ ):
434
+ os_id = os["id"]
435
+ else:
436
+ name, version = os_name.split("-")
437
+ for os in os_images:
438
+ if os["name"] == name and os["version"] == version:
439
+ os_id = os["id"]
440
+ return os_id
441
+
442
+
443
+ def size_to_mb(size: str) -> int:
444
+ """Transform string like '5G' into integer in megabytes.
445
+ Case insensitive. For example::
446
+
447
+ 1T --> 1048576
448
+ 5G --> 5120
449
+ 1024M --> 1024
450
+ 2048 --> 2048
451
+
452
+ NOTE! This function does not support floats e.g. 1.5T x--> 1572864
453
+ """
454
+ match = re.match(r"^([0-9]+)([mgt]?)$", size, re.I)
455
+ if match:
456
+ try:
457
+ val, unit = list(match.groups())
458
+ if unit.lower() == "g":
459
+ return int(val) * 1024
460
+ if unit.lower() == "t":
461
+ return int(val) * 1048576
462
+ return int(val)
463
+ except TypeError:
464
+ return None
465
+ else:
466
+ return None
467
+
468
+
469
+ def check_value(
470
+ value: int, minv: int = 0, maxv: int = 0, step: int = 0
471
+ ) -> bool:
472
+ """Check integer `value` is suitable with required limitations.
473
+ Return True if success. This function is for value testing by
474
+ `server_configurators` requirements.
475
+ """
476
+ try:
477
+ # pylint: disable=chained-comparison
478
+ return value <= maxv and value >= minv and (value / step).is_integer()
479
+ except TypeError:
480
+ return None
481
+
482
+
483
+ def validate_bandwidth(ctx, param, value):
484
+ """Return valid bandwidth value or exit. See "Callback for Validation"
485
+ at https://click.palletsprojects.com/en/8.1.x/options
486
+ """
487
+ if not value:
488
+ return None
489
+
490
+ if check_value(value, minv=100, maxv=1000, step=100):
491
+ return value
492
+
493
+ raise click.BadParameter("Value must be in range 100-1000 with step 100.")
494
+
495
+
496
+ def validate_image(client, image: str) -> int:
497
+ """Return valid os_id or exit."""
498
+ log("Get list of OS images...")
499
+ os_images = _server_os_images(client).json()["servers_os"]
500
+
501
+ if re.match(r"^[a-z]+-[0-9.]+$", image, re.I):
502
+ return get_os_id_by_name(os_images, image)
503
+
504
+ try:
505
+ if int(image) in [int(os["id"]) for os in os_images]:
506
+ return int(image)
507
+ return None
508
+ except (TypeError, ValueError):
509
+ return None
510
+
511
+
512
+ def validate_cpu(configurator: dict, value: int) -> int:
513
+ """Return valid cpu value or exit."""
514
+ if check_value(
515
+ value,
516
+ minv=configurator["requirements"]["cpu_min"],
517
+ maxv=configurator["requirements"]["cpu_max"],
518
+ step=configurator["requirements"]["cpu_step"],
519
+ ):
520
+ return value
521
+
522
+ raise click.BadParameter("Too many or too few CPUs.")
523
+
524
+
525
+ def validate_ram(configurator: dict, value: str) -> int:
526
+ """Return valid RAM value in megabytes or exit."""
527
+ if check_value(
528
+ size_to_mb(value),
529
+ minv=configurator["requirements"]["ram_min"],
530
+ maxv=configurator["requirements"]["ram_max"],
531
+ step=configurator["requirements"]["ram_step"],
532
+ ):
533
+ return size_to_mb(value)
534
+
535
+ raise click.BadParameter("Too large or too small size of RAM.")
536
+
537
+
538
+ def validate_disk(configurator: dict, value: str) -> int:
539
+ """Return valid disk value in megabytes or exit."""
540
+ if check_value(
541
+ size_to_mb(value),
542
+ minv=configurator["requirements"]["disk_min"],
543
+ maxv=configurator["requirements"]["disk_max"],
544
+ step=configurator["requirements"]["disk_step"],
545
+ ):
546
+ return size_to_mb(value)
547
+
548
+ raise click.BadParameter("Too large or too small disk size.")
549
+
550
+
551
+ def get_configuration(
552
+ client, configurator_id: int, cpu: int, ram: str, disk: str
553
+ ) -> dict:
554
+ """Return `configuration` if CPU, RAM and Disk values is valid or exit.
555
+ This function is used into server_create().
556
+ """
557
+ configurators = _get_server_configurators(client).json()
558
+
559
+ for item in configurators["server_configurators"]:
560
+ if item["id"] == configurator_id:
561
+ configurator = item # <-- current configurator
562
+
563
+ return {
564
+ "configurator_id": configurator_id,
565
+ "cpu": validate_cpu(configurator, cpu),
566
+ "ram": validate_ram(configurator, ram),
567
+ "disk": validate_disk(configurator, disk),
568
+ }
569
+
570
+
571
+ def add_ssh_key_from_file(
572
+ client, ssh_key_file: str, existing_ssh_keys: list
573
+ ) -> int:
574
+ """Return integer SSH-key ID. Add new SSH-key if not exist."""
575
+ ssh_key_name = os.path.basename(ssh_key_file)
576
+ try:
577
+ with open(ssh_key_file, "r", encoding="utf-8") as pubkey:
578
+ ssh_key_body = pubkey.read().strip()
579
+ except (OSError, IOError, FileNotFoundError) as error:
580
+ sys.exit(f"Error: Cannot read SSH-key: {error}")
581
+
582
+ # I don't want to add the same key over and over
583
+ for exist_key in existing_ssh_keys:
584
+ if ssh_key_body == exist_key["body"]:
585
+ log(
586
+ f"SSH-Key '{ssh_key_name}' already exists,"
587
+ f" ID {exist_key['id']} is used."
588
+ )
589
+ return exist_key["id"]
590
+
591
+ log(f"Add new SSH-key '{ssh_key_name}'...")
592
+ added_key = _ssh_key_new(
593
+ client,
594
+ name=ssh_key_name,
595
+ body=ssh_key_body,
596
+ is_default=False,
597
+ )
598
+ ssh_key_id = added_key.json()["ssh_key"]["id"]
599
+ log(f"New SSH-key '{ssh_key_name} ID is '{ssh_key_id}'")
600
+ return ssh_key_id
601
+
602
+
603
+ def add_ssh_key(client, existing_ssh_keys: list, pubkey: str) -> int:
604
+ """Retrun SSH-key ID. from file, by SSH-key ID or by SSH-key name."""
605
+ # From filesystem
606
+ if os.path.exists(pubkey):
607
+ log(f"SSH-key to add: file: {pubkey}")
608
+ return add_ssh_key_from_file(client, pubkey, existing_ssh_keys)
609
+
610
+ # Add by ID
611
+ if pubkey.isdigit():
612
+ if int(pubkey) in [key["id"] for key in existing_ssh_keys]:
613
+ log(f"SSH-key to add: ID: {pubkey}")
614
+ return int(pubkey)
615
+ sys.exit(f"Error: SSH-key with ID {pubkey} not found.")
616
+
617
+ # Add by name
618
+ for ssh_key in existing_ssh_keys:
619
+ if pubkey == ssh_key["name"]:
620
+ log(f"SSH-key to add: name: {pubkey} ID: {ssh_key['id']}")
621
+ return ssh_key["id"]
622
+
623
+ sys.exit(f"Error: SSH-key '{pubkey}' not found.")
624
+
625
+
626
+ @server.command("create", help="Create Cloud Server.")
627
+ @options(GLOBAL_OPTIONS)
628
+ @options(OUTPUT_FORMAT_OPTION)
629
+ @click.option("--name", required=True, help="Cloud Server display name.")
630
+ @click.option("--comment", help="Comment.")
631
+ @click.option("--avatar-id", default=None, help="Avatar ID.")
632
+ @click.option("--image", required=True, help="OS image to install.")
633
+ @click.option(
634
+ "--preset-id",
635
+ type=int,
636
+ cls=MutuallyExclusiveOption,
637
+ mutually_exclusive=["cpu", "ram", "disk"],
638
+ help="Configuration preset ID.",
639
+ )
640
+ @click.option("--cpu", type=int, help="Number of CPUs.")
641
+ @click.option("--ram", help="RAM size, e.g. 1024M, 1G.")
642
+ @click.option("--disk", help="Primary disk size e.g. 15360M, 15G.")
643
+ @click.option(
644
+ "--bandwidth",
645
+ type=int,
646
+ callback=validate_bandwidth,
647
+ help="Network bandwidth.",
648
+ )
649
+ @click.option(
650
+ "--software-id", type=int, default=None, help="Software ID to install."
651
+ )
652
+ @click.option(
653
+ "--ssh-key",
654
+ metavar="FILE|ID|NAME",
655
+ default=None,
656
+ multiple=True,
657
+ help="SSH-key, can be multiple.",
658
+ )
659
+ @click.option(
660
+ "--ddos-protection",
661
+ type=bool,
662
+ default=False,
663
+ show_default=True,
664
+ help="Enable DDoS-Guard.",
665
+ )
666
+ @click.option(
667
+ "--local-network",
668
+ type=bool,
669
+ default=False,
670
+ show_default=True,
671
+ help="Enable local network.",
672
+ )
673
+ def server_create(
674
+ config,
675
+ profile,
676
+ verbose,
677
+ output_format,
678
+ name,
679
+ comment,
680
+ avatar_id,
681
+ image,
682
+ preset_id,
683
+ cpu,
684
+ ram,
685
+ disk,
686
+ bandwidth,
687
+ software_id,
688
+ ssh_key,
689
+ ddos_protection,
690
+ local_network,
691
+ ):
692
+ """Create Cloud Server."""
693
+ # pylint: disable=too-many-locals
694
+
695
+ client = create_client(config, profile)
696
+
697
+ # Get os_id or exit
698
+ log("Looking for os_id...")
699
+ os_id = validate_image(client, image)
700
+ log(f"os_id is {os_id}")
701
+ if not os_id:
702
+ raise click.BadParameter("Wrong image name or ID.")
703
+
704
+ # Fallback bandwidth to minimum
705
+ if not bandwidth and not preset_id:
706
+ bandwidth = 100
707
+
708
+ # SSH-keys
709
+ ssh_keys_ids = []
710
+ log("Get SSH-keys...")
711
+ existing_ssh_keys = _ssh_key_list(client).json()["ssh_keys"]
712
+
713
+ for pubkey in ssh_key:
714
+ ssh_keys_ids.append(add_ssh_key(client, existing_ssh_keys, pubkey))
715
+
716
+ # Create Cloud Server from configurator or preset
717
+ if cpu or ram or disk:
718
+ log("Get configurator...")
719
+ configuration = get_configuration(
720
+ client,
721
+ DEFAULT_CONFIGURATOR_ID,
722
+ cpu,
723
+ ram,
724
+ disk,
725
+ )
726
+
727
+ # Do request
728
+ log("Create Cloud Server with configurator...")
729
+ response = _server_create(
730
+ client,
731
+ configuration=configuration,
732
+ os_id=os_id,
733
+ bandwidth=bandwidth,
734
+ name=name,
735
+ is_ddos_guard=ddos_protection,
736
+ is_local_network=local_network,
737
+ comment=comment,
738
+ software_id=software_id,
739
+ avatar_id=avatar_id,
740
+ ssh_keys_ids=ssh_keys_ids,
741
+ )
742
+ elif preset_id:
743
+ # Set bandwidth value from preset if option is not set
744
+ if not bandwidth:
745
+ log("Check preset_id...")
746
+ presets = _server_presets(client).json()["server_presets"]
747
+ for preset in presets:
748
+ if preset["id"] == preset_id:
749
+ log(f"Set bandwidth from preset: {preset['bandwidth']}")
750
+ bandwidth = preset["bandwidth"]
751
+
752
+ # Do request
753
+ log(f"Create Cloud Server with preset_id {preset_id}...")
754
+ response = _server_create(
755
+ client,
756
+ preset_id=preset_id,
757
+ os_id=os_id,
758
+ bandwidth=bandwidth,
759
+ name=name,
760
+ is_ddos_guard=ddos_protection,
761
+ is_local_network=local_network,
762
+ comment=comment,
763
+ software_id=software_id,
764
+ avatar_id=avatar_id,
765
+ ssh_keys_ids=ssh_keys_ids,
766
+ )
767
+ else:
768
+ raise click.UsageError(
769
+ "Configuration or preset is required. "
770
+ "Set '--cpu', '--ram' and '--disk' or '--preset-id'"
771
+ )
772
+
773
+ fmt.printer(
774
+ response,
775
+ output_format=output_format,
776
+ func=lambda response: click.echo(response.json()["server"]["id"]),
777
+ )
778
+
779
+
780
+ # ------------------------------------------------------------- #
781
+ # $ twc server set-property #
782
+ # ------------------------------------------------------------- #
783
+
784
+
785
+ @server.command("set-property", help="Update Cloud Server properties.")
786
+ @options(GLOBAL_OPTIONS)
787
+ @options(OUTPUT_FORMAT_OPTION)
788
+ @click.option("--name", help="Cloud server display name.")
789
+ @click.option("--comment", help="Comment.")
790
+ @click.option("--avatar-id", default=None, help="Avatar ID.")
791
+ @click.argument("server_id", type=int, required=True)
792
+ def server_set_property(
793
+ config,
794
+ profile,
795
+ verbose,
796
+ output_format,
797
+ name,
798
+ comment,
799
+ avatar_id,
800
+ server_id,
801
+ ):
802
+ client = create_client(config, profile)
803
+ payload = {}
804
+
805
+ if name:
806
+ payload.update({"name": name})
807
+ if comment:
808
+ payload.update({"comment": comment})
809
+ if avatar_id:
810
+ payload.update({"avatar_id": avatar_id})
811
+
812
+ if not payload:
813
+ raise click.UsageError(
814
+ "Nothing to do. Set one of "
815
+ "['--name', '--comment', '--avatar-id']"
816
+ )
817
+
818
+ response = _server_update(client, server_id, payload)
819
+ fmt.printer(
820
+ response,
821
+ output_format=output_format,
822
+ func=lambda response: click.echo(response.json()["server"]["id"]),
823
+ )
824
+
825
+
826
+ # ------------------------------------------------------------- #
827
+ # $ twc server resize #
828
+ # ------------------------------------------------------------- #
829
+
830
+
831
+ @server.command("resize", help="Change CPU, RAM, disk and bandwidth.")
832
+ @options(GLOBAL_OPTIONS)
833
+ @options(OUTPUT_FORMAT_OPTION)
834
+ @click.option(
835
+ "--preset-id",
836
+ type=int,
837
+ cls=MutuallyExclusiveOption,
838
+ mutually_exclusive=["cpu", "ram", "disk"],
839
+ help="Configuration preset ID.",
840
+ )
841
+ @click.option("--cpu", type=int, help="Number of vCPUs.")
842
+ @click.option("--ram", help="RAM size, e.g. 1024M, 1G.")
843
+ @click.option("--disk", help="Primary disk size e.g. 15360M, 15G.")
844
+ @click.option(
845
+ "--bandwidth",
846
+ type=int,
847
+ callback=validate_bandwidth,
848
+ help="Network bandwidth.",
849
+ )
850
+ @click.option(
851
+ "--yes",
852
+ "confirmed",
853
+ is_flag=True,
854
+ help="Confirm the action without prompting.",
855
+ )
856
+ @click.argument("server_id", type=int, required=True)
857
+ def server_resize(
858
+ config,
859
+ profile,
860
+ verbose,
861
+ output_format,
862
+ preset_id,
863
+ cpu,
864
+ ram,
865
+ disk,
866
+ bandwidth,
867
+ confirmed,
868
+ server_id,
869
+ ):
870
+ """Resize Cloud Server CPU, RAM and primary disk size."""
871
+ # pylint: disable=too-many-locals
872
+ # pylint: disable=too-many-branches
873
+ # pylint: disable=too-many-statements
874
+
875
+ client = create_client(config, profile)
876
+ payload = {}
877
+
878
+ # Save original server state
879
+ log("Get server original state...")
880
+ old_state = _server_get(client, server_id).json()["server"]
881
+
882
+ # Get original server preset tags
883
+ old_preset_tags = []
884
+ if old_state["preset_id"]:
885
+ log(f"Get preset tags by preset_id {old_state['preset_id']}...")
886
+ presets = _server_presets(client).json()["server_presets"]
887
+ for preset in presets:
888
+ if preset["id"] == old_state["preset_id"]:
889
+ old_preset_tags = preset["tags"]
890
+ log(f"Preset tags is {old_preset_tags}")
891
+
892
+ # Return error if user tries to change dedicated server
893
+ if "vds_dedic" in old_preset_tags:
894
+ sys.exit(
895
+ "Error: Cannot change dedicated server."
896
+ + "Please contact techsupport."
897
+ )
898
+
899
+ # Handle case: user tries to change configurator or switch from
900
+ # preset to configurator.
901
+
902
+ # Return error if user tries to switch from preset to configurator in
903
+ # location where configurator is unavailable.
904
+ if cpu or ram or disk:
905
+ if old_state["location"] not in REGIONS_WITH_CONFIGURATOR:
906
+ sys.exit(
907
+ "Error: Can not change configuration in location "
908
+ + f"'{old_state['location']}'. Change preset_id instead."
909
+ )
910
+
911
+ # Get original server configurator_id
912
+ configurator_id = old_state["configurator_id"]
913
+ configurator = None
914
+
915
+ # Get configurator_id if user tries to switch from preset to
916
+ # configurator. Don't ask what is this.
917
+ log("Get configurator_id...")
918
+ if not configurator_id:
919
+ if (
920
+ "ssd_2022" in old_preset_tags
921
+ or "discount35" in old_preset_tags
922
+ ):
923
+ configurator_id = 11 # discount configurator
924
+ else:
925
+ configurator_id = 9 # old full price configurator
926
+
927
+ # Get configurator by configurator_id
928
+ log(f"configurator_id is {configurator_id}, get confugurator...")
929
+ configurators = _get_server_configurators(client).json()
930
+ for item in configurators["server_configurators"]:
931
+ if item["id"] == configurator_id:
932
+ configurator = item # <-- this!
933
+
934
+ # Check configurator and return error if configurator is unavailable
935
+ log(f"Configurator: '{configurator}'")
936
+ if configurator_id and not configurator:
937
+ sys.exit(
938
+ "Error: Configurator is not available for your server. "
939
+ + "Try to create new server."
940
+ )
941
+
942
+ # Add configurator key to payload
943
+ payload.update({"configurator": {}})
944
+
945
+ # Get original size of primary disk
946
+ for old_disk in old_state["disks"]:
947
+ if old_disk["is_system"]: # is True
948
+ primary_disk_size = old_disk["size"]
949
+
950
+ # Fill payload with original server specs
951
+ payload["configurator"].update(
952
+ {
953
+ "configurator_id": configurator_id,
954
+ "cpu": old_state["cpu"],
955
+ "ram": old_state["ram"],
956
+ "disk": primary_disk_size,
957
+ }
958
+ )
959
+
960
+ # Refill payload with parameters from command line
961
+ if cpu:
962
+ payload["configurator"].update(
963
+ {"cpu": validate_cpu(configurator, cpu)}
964
+ )
965
+
966
+ if ram:
967
+ payload["configurator"].update(
968
+ {"ram": validate_ram(configurator, ram)}
969
+ )
970
+
971
+ if disk:
972
+ payload["configurator"].update(
973
+ {"disk": validate_disk(configurator, disk)}
974
+ )
975
+
976
+ if bandwidth:
977
+ payload.update({"bandwidth": bandwidth})
978
+
979
+ # Handle case: user tries to change preset to another preset.
980
+ # Check passed preset_id and exit on fail
981
+ if preset_id:
982
+ # I cannot want to change preset to itself
983
+ if preset_id == old_state["preset_id"]:
984
+ sys.exit(
985
+ "Error: Cannot change preset to itself. "
986
+ f"Server already have preset_id {old_state['preset_id']}."
987
+ )
988
+
989
+ presets = _server_presets(client).json()["server_presets"]
990
+ for preset in presets:
991
+ if preset["id"] == preset_id:
992
+ payload.update({"preset_id": preset_id})
993
+ try:
994
+ payload["preset_id"]
995
+ except KeyError:
996
+ sys.exit(f"Error: Invalid preset_id {preset_id}")
997
+
998
+ # Prompt if no option --yes passed
999
+ if not confirmed:
1000
+ if not confirm_action("Server will restart, continue?"):
1001
+ sys.exit("Aborted!")
1002
+
1003
+ # Make request
1004
+ response = _server_update(client, server_id, payload)
1005
+ fmt.printer(
1006
+ response,
1007
+ output_format=output_format,
1008
+ func=lambda response: click.echo(response.json()["server"]["id"]),
1009
+ )
1010
+
1011
+
1012
+ # ------------------------------------------------------------- #
1013
+ # $ twc server reinstall #
1014
+ # ------------------------------------------------------------- #
1015
+
1016
+
1017
+ @server.command("reinstall", help="Reinstall OS or software.")
1018
+ @options(GLOBAL_OPTIONS)
1019
+ @options(OUTPUT_FORMAT_OPTION)
1020
+ @click.option("--image", default=None, help="OS image to install.")
1021
+ @click.option(
1022
+ "--software-id", type=int, default=None, help="Software ID to install."
1023
+ )
1024
+ @click.confirmation_option(
1025
+ prompt="All data on Cloud Server will be lost.\n"
1026
+ "This action cannot be undone. Are you sure?"
1027
+ )
1028
+ @click.argument("server_id", type=int, required=True)
1029
+ def server_reinstall(
1030
+ config,
1031
+ profile,
1032
+ verbose,
1033
+ output_format,
1034
+ image,
1035
+ software_id,
1036
+ server_id,
1037
+ ):
1038
+ client = create_client(config, profile)
1039
+ payload = {}
1040
+
1041
+ if image:
1042
+ os_id = validate_image(client, image)
1043
+ if not os_id:
1044
+ raise click.BadParameter("Wrong image name or ID.")
1045
+ payload.update({"os_id": os_id})
1046
+
1047
+ if software_id:
1048
+ old_state = _server_get(client, server_id).json()["server"]
1049
+ payload.update(
1050
+ {
1051
+ "os_id": old_state["os"]["id"],
1052
+ "software_id": software_id,
1053
+ }
1054
+ )
1055
+
1056
+ if not payload:
1057
+ raise click.UsageError(
1058
+ "Nothing to do. Set one of ['--image', '--software-id']"
1059
+ )
1060
+
1061
+ response = _server_update(client, server_id, payload)
1062
+ fmt.printer(
1063
+ response,
1064
+ output_format=output_format,
1065
+ func=lambda response: click.echo(response.json()["server"]["id"]),
1066
+ )
1067
+
1068
+
1069
+ # ------------------------------------------------------------- #
1070
+ # $ twc server <action> #
1071
+ # remove, boot, reboot, shutdown, clone, reset-root-password #
1072
+ # ------------------------------------------------------------- #
1073
+
1074
+ # ------------------------------------------------------------- #
1075
+ # $ twc server remove #
1076
+ # ------------------------------------------------------------- #
1077
+
1078
+
1079
+ @server.command("remove", aliases=["rm"], help="Remove Cloud Server.")
1080
+ @options(GLOBAL_OPTIONS)
1081
+ @click.argument("server_ids", nargs=-1, type=int, required=True)
1082
+ @click.confirmation_option(
1083
+ prompt="This action cannot be undone. Are you sure?"
1084
+ )
1085
+ def server_remove(config, profile, verbose, server_ids):
1086
+ client = create_client(config, profile)
1087
+
1088
+ for server_id in server_ids:
1089
+ response = _server_remove(client, server_id)
1090
+ if response.status_code == 204:
1091
+ click.echo(server_id)
1092
+ else:
1093
+ fmt.printer(response)
1094
+
1095
+
1096
+ # ------------------------------------------------------------- #
1097
+ # $ twc server boot #
1098
+ # ------------------------------------------------------------- #
1099
+
1100
+
1101
+ @server.command("boot", aliases=["start"], help="Boot Cloud Server.")
1102
+ @options(GLOBAL_OPTIONS)
1103
+ @click.argument("server_ids", nargs=-1, type=int, required=True)
1104
+ def server_start(config, profile, verbose, server_ids):
1105
+ client = create_client(config, profile)
1106
+
1107
+ for server_id in server_ids:
1108
+ response = _server_action(client, server_id, action="start")
1109
+ if response.status_code == 204:
1110
+ click.echo(server_id)
1111
+ else:
1112
+ fmt.printer(response)
1113
+
1114
+
1115
+ # ------------------------------------------------------------- #
1116
+ # $ twc server reboot #
1117
+ # ------------------------------------------------------------- #
1118
+
1119
+
1120
+ @server.command("reboot", aliases=["restart"], help="Reboot Cloud Server.")
1121
+ @options(GLOBAL_OPTIONS)
1122
+ @click.option("--hard", "hard_reboot", is_flag=True, help="Do hard reboot.")
1123
+ @click.argument("server_ids", nargs=-1, type=int, required=True)
1124
+ def server_reboot(config, profile, verbose, server_ids, hard_reboot):
1125
+ if hard_reboot:
1126
+ action = "hard_reboot"
1127
+ else:
1128
+ action = "boot"
1129
+ client = create_client(config, profile)
1130
+
1131
+ for server_id in server_ids:
1132
+ response = _server_action(client, server_id, action=action)
1133
+ if response.status_code == 204:
1134
+ click.echo(server_id)
1135
+ else:
1136
+ fmt.printer(response)
1137
+
1138
+
1139
+ # ------------------------------------------------------------- #
1140
+ # $ twc server shutdown #
1141
+ # ------------------------------------------------------------- #
1142
+
1143
+
1144
+ @server.command("shutdown", aliases=["stop"], help="Shutdown Cloud Server.")
1145
+ @options(GLOBAL_OPTIONS)
1146
+ @click.option(
1147
+ "--hard", "hard_shutdown", is_flag=True, help="Do hard shutdown."
1148
+ )
1149
+ @click.argument("server_ids", nargs=-1, type=int, required=True)
1150
+ def server_shutdown(config, profile, verbose, server_ids, hard_shutdown):
1151
+ if hard_shutdown:
1152
+ action = "hard_shutdown"
1153
+ else:
1154
+ action = "shutdown"
1155
+ client = create_client(config, profile)
1156
+
1157
+ for server_id in server_ids:
1158
+ response = _server_action(client, server_id, action=action)
1159
+ if response.status_code == 204:
1160
+ click.echo(server_id)
1161
+ else:
1162
+ fmt.printer(response)
1163
+
1164
+
1165
+ # ------------------------------------------------------------- #
1166
+ # $ twc server clone #
1167
+ # ------------------------------------------------------------- #
1168
+
1169
+
1170
+ @server.command("clone", help="Clone Cloud Server.")
1171
+ @options(GLOBAL_OPTIONS)
1172
+ @click.argument("server_id", type=int, required=True)
1173
+ def server_clone(config, profile, verbose, server_id):
1174
+ client = create_client(config, profile)
1175
+ response = _server_action(client, server_id, action="clone")
1176
+ if response.status_code == 204:
1177
+ click.echo(server_id)
1178
+ else:
1179
+ fmt.printer(response)
1180
+
1181
+
1182
+ # ------------------------------------------------------------- #
1183
+ # $ twc server reset-root-password #
1184
+ # ------------------------------------------------------------- #
1185
+
1186
+
1187
+ @server.command("reset-root-password", help="Reset root user password.")
1188
+ @options(GLOBAL_OPTIONS)
1189
+ @click.confirmation_option(
1190
+ prompt="New password will sent to contact email. Continue?"
1191
+ )
1192
+ @click.argument("server_id", type=int, required=True)
1193
+ def server_reset_root(config, profile, verbose, server_id):
1194
+ client = create_client(config, profile)
1195
+ response = _server_action(client, server_id, action="reset_password")
1196
+ if response.status_code == 204:
1197
+ click.echo(server_id)
1198
+ else:
1199
+ fmt.printer(response)
1200
+
1201
+
1202
+ # ------------------------------------------------------------- #
1203
+ # $ twc server list-presets #
1204
+ # ------------------------------------------------------------- #
1205
+
1206
+
1207
+ def print_presets(response: object, filters: str):
1208
+ if filters:
1209
+ presets = fmt.filter_list(response.json()["server_presets"], filters)
1210
+ else:
1211
+ presets = response.json()["server_presets"]
1212
+ table = fmt.Table()
1213
+ table.header(
1214
+ [
1215
+ "ID",
1216
+ "REGION",
1217
+ "PRICE",
1218
+ "CPU",
1219
+ "CPU FREQ",
1220
+ "RAM",
1221
+ "DISK",
1222
+ "DISK TYPE",
1223
+ "BANDWIDTH",
1224
+ "DESCRIPTION",
1225
+ "LOCAL NETWORK",
1226
+ ]
1227
+ )
1228
+ for preset in presets:
1229
+ table.row(
1230
+ [
1231
+ preset["id"],
1232
+ preset["location"],
1233
+ preset["price"],
1234
+ preset["cpu"],
1235
+ preset["cpu_frequency"],
1236
+ preset["ram"],
1237
+ preset["disk"],
1238
+ preset["disk_type"],
1239
+ preset["bandwidth"],
1240
+ preset["description_short"],
1241
+ preset["is_allowed_local_network"],
1242
+ ]
1243
+ )
1244
+ table.print()
1245
+
1246
+
1247
+ @server.command("list-presets", help="List configuration presets.")
1248
+ @options(GLOBAL_OPTIONS)
1249
+ @options(OUTPUT_FORMAT_OPTION)
1250
+ @click.option("--region", "-r", help="Use region (location).")
1251
+ @click.option("--filter", "-f", "filters", default="", help="Filter output.")
1252
+ def server_presets(config, profile, verbose, output_format, filters, region):
1253
+ if filters:
1254
+ filters = filters.replace("region", "location")
1255
+ if region:
1256
+ if filters:
1257
+ filters = filters + f",location:{region}"
1258
+ else:
1259
+ filters = f"location:{region}"
1260
+
1261
+ client = create_client(config, profile)
1262
+ response = _server_presets(client)
1263
+ fmt.printer(
1264
+ response,
1265
+ output_format=output_format,
1266
+ filters=filters,
1267
+ func=print_presets,
1268
+ )
1269
+
1270
+
1271
+ # ------------------------------------------------------------- #
1272
+ # $ twc server list-os-images #
1273
+ # ------------------------------------------------------------- #
1274
+
1275
+
1276
+ def print_os_images(response: object, filters: str):
1277
+ if filters:
1278
+ os_list = fmt.filter_list(response.json()["servers_os"], filters)
1279
+ else:
1280
+ os_list = response.json()["servers_os"]
1281
+ table = fmt.Table()
1282
+ table.header(
1283
+ ["ID", "FAMILY", "NAME", "VERSION", "CODENAME", "REQUIREMENTS"]
1284
+ )
1285
+ for os in os_list:
1286
+ try:
1287
+ value = os["requirements"]["disk_min"]
1288
+ requirements = f"disk_min: {value}G"
1289
+ except KeyError:
1290
+ requirements = ""
1291
+ table.row(
1292
+ [
1293
+ os["id"],
1294
+ os["family"],
1295
+ os["name"],
1296
+ os["version"],
1297
+ os["version_codename"],
1298
+ requirements,
1299
+ ]
1300
+ )
1301
+ table.print()
1302
+
1303
+
1304
+ @server.command(
1305
+ "list-os-images", help="List prebuilt operating system images."
1306
+ )
1307
+ @options(GLOBAL_OPTIONS)
1308
+ @options(OUTPUT_FORMAT_OPTION)
1309
+ @click.option("--filter", "-f", "filters", default="", help="Filter output.")
1310
+ def server_os_images(config, profile, verbose, output_format, filters):
1311
+ client = create_client(config, profile)
1312
+ response = _server_os_images(client)
1313
+ fmt.printer(
1314
+ response,
1315
+ output_format=output_format,
1316
+ filters=filters,
1317
+ func=print_os_images,
1318
+ )
1319
+
1320
+
1321
+ # ------------------------------------------------------------- #
1322
+ # $ twc server list-software #
1323
+ # ------------------------------------------------------------- #
1324
+
1325
+
1326
+ def print_software(response: object):
1327
+ table = fmt.Table()
1328
+ table.header(["ID", "NAME", "OS"])
1329
+ for soft in response.json()["servers_software"]:
1330
+ table.row(
1331
+ [
1332
+ soft["id"],
1333
+ soft["name"],
1334
+ ", ".join([str(k) for k in soft["os_ids"]]),
1335
+ ]
1336
+ )
1337
+ table.print()
1338
+
1339
+
1340
+ @server.command("list-software", help="List software.")
1341
+ @options(GLOBAL_OPTIONS)
1342
+ @options(OUTPUT_FORMAT_OPTION)
1343
+ def server_software(config, profile, verbose, output_format):
1344
+ client = create_client(config, profile)
1345
+ response = _server_software(client)
1346
+ fmt.printer(response, output_format=output_format, func=print_software)
1347
+
1348
+
1349
+ # ------------------------------------------------------------- #
1350
+ # $ twc server logs #
1351
+ # ------------------------------------------------------------- #
1352
+
1353
+
1354
+ def print_logs(response: object):
1355
+ event_log = response.json()["server_logs"]
1356
+ for line in event_log:
1357
+ click.echo(
1358
+ f"{line['logged_at']} id={line['id']} event={line['event']}"
1359
+ )
1360
+
1361
+
1362
+ @server.command("logs", help="View Cloud Server events log.")
1363
+ @options(GLOBAL_OPTIONS)
1364
+ @options(OUTPUT_FORMAT_OPTION)
1365
+ @click.option("--limit", default=100, show_default=True, help="Limit.")
1366
+ @click.option(
1367
+ "--order",
1368
+ default="asc",
1369
+ show_default=True,
1370
+ type=click.Choice(["asc", "desc"]),
1371
+ help="Sort logs by datetime.",
1372
+ )
1373
+ @click.argument("server_id", type=int, required=True)
1374
+ def server_logs(
1375
+ config, profile, verbose, output_format, limit, order, server_id
1376
+ ):
1377
+ client = create_client(config, profile)
1378
+ response = _server_logs(
1379
+ client, server_id=server_id, limit=limit, order=order
1380
+ )
1381
+ fmt.printer(response, output_format=output_format, func=print_logs)
1382
+
1383
+
1384
+ # ------------------------------------------------------------- #
1385
+ # $ twc server set-boot-mode #
1386
+ # ------------------------------------------------------------- #
1387
+
1388
+
1389
+ @server.command("set-boot-mode", help="Set Cloud Server boot mode.")
1390
+ @options(GLOBAL_OPTIONS)
1391
+ @click.option(
1392
+ "--server-id",
1393
+ "server_ids",
1394
+ type=int,
1395
+ multiple=True,
1396
+ required=True,
1397
+ help="Cloud Server ID, can be multiple.",
1398
+ )
1399
+ @click.confirmation_option(prompt="Server will reboot, continue?")
1400
+ @click.argument(
1401
+ "boot_mode",
1402
+ type=click.Choice(["default", "single", "recovery_disk"]),
1403
+ required=True,
1404
+ )
1405
+ def server_set_boot_mode(config, profile, verbose, boot_mode, server_ids):
1406
+ client = create_client(config, profile)
1407
+
1408
+ for server_id in server_ids:
1409
+ response = _server_set_boot_mode(
1410
+ client, server_id, boot_mode=boot_mode
1411
+ )
1412
+ if response.status_code == 204:
1413
+ click.echo(server_id)
1414
+ else:
1415
+ fmt.printer(response)
1416
+
1417
+
1418
+ # ------------------------------------------------------------- #
1419
+ # $ twc server set-nat-mode #
1420
+ # ------------------------------------------------------------- #
1421
+
1422
+
1423
+ @server.command("set-nat-mode", help="Set Cloud Server NAT mode.")
1424
+ @options(GLOBAL_OPTIONS)
1425
+ @click.option(
1426
+ "--server-id",
1427
+ "server_ids",
1428
+ type=int,
1429
+ multiple=True,
1430
+ required=True,
1431
+ help="Cloud Server ID, can be multiple.",
1432
+ )
1433
+ @click.argument(
1434
+ "nat_mode",
1435
+ type=click.Choice(["dnat_and_snat", "snat", "no_nat"]),
1436
+ required=True,
1437
+ )
1438
+ def server_set_nat_mode(config, profile, verbose, nat_mode, server_ids):
1439
+ client = create_client(config, profile)
1440
+
1441
+ for server_id in server_ids:
1442
+ response = _server_set_nat_mode(client, server_id, nat_mode=nat_mode)
1443
+ if response.status_code == 204:
1444
+ click.echo(server_id)
1445
+ else:
1446
+ fmt.printer(response)
1447
+
1448
+
1449
+ # ------------------------------------------------------------- #
1450
+ # $ twc server ip #
1451
+ # ------------------------------------------------------------- #
1452
+
1453
+
1454
+ @server.group("ip", cls=ClickAliasedGroup)
1455
+ @options(GLOBAL_OPTIONS[:2])
1456
+ def ip_addr():
1457
+ """Manage public IPs."""
1458
+
1459
+
1460
+ # ------------------------------------------------------------- #
1461
+ # $ twc server ip list #
1462
+ # ------------------------------------------------------------- #
1463
+
1464
+
1465
+ def print_ips(response: object):
1466
+ ips = response.json()["server_ips"]
1467
+ table = fmt.Table()
1468
+ table.header(["ADDRESS", "VERSION", "PTR", "PRIMARY"])
1469
+ for ip_addr in ips:
1470
+ table.row(
1471
+ [
1472
+ ip_addr["ip"],
1473
+ ip_addr["type"],
1474
+ ip_addr["ptr"],
1475
+ ip_addr["is_main"],
1476
+ ]
1477
+ )
1478
+ table.print()
1479
+
1480
+
1481
+ @ip_addr.command(
1482
+ "list", aliases=["ls"], help="List public IPs attached to Cloud Server."
1483
+ )
1484
+ @options(GLOBAL_OPTIONS)
1485
+ @options(OUTPUT_FORMAT_OPTION)
1486
+ @click.argument("server_id", type=int, required=True)
1487
+ def server_ip_list(config, profile, verbose, output_format, server_id):
1488
+ client = create_client(config, profile)
1489
+ response = _server_ip_list(client, server_id)
1490
+ fmt.printer(response, output_format=output_format, func=print_ips)
1491
+
1492
+
1493
+ # ------------------------------------------------------------- #
1494
+ # $ twc server ip add #
1495
+ # ------------------------------------------------------------- #
1496
+
1497
+
1498
+ @ip_addr.command("add", help="Attach new IP to Cloud Server.")
1499
+ @options(GLOBAL_OPTIONS)
1500
+ @options(OUTPUT_FORMAT_OPTION)
1501
+ @click.option(
1502
+ "--ipv4/--ipv6", default=True, show_default=True, help="IP version."
1503
+ )
1504
+ @click.option("--ptr", help="IP address pointer (RDNS).")
1505
+ @click.option(
1506
+ "--to-server",
1507
+ "server_id",
1508
+ type=int,
1509
+ required=True,
1510
+ help="Cloud Server ID.",
1511
+ )
1512
+ def server_ip_add(
1513
+ config, profile, verbose, output_format, ipv4, ptr, server_id
1514
+ ):
1515
+ client = create_client(config, profile)
1516
+ if not ipv4:
1517
+ log("Get Cloud Server location...")
1518
+ location = _server_get(client, server_id).json()["server"]["location"]
1519
+ if location not in REGIONS_WITH_IPV6:
1520
+ sys.exit(f"Error: IPv6 is not available in location '{location}'.")
1521
+ ip_version = "ipv6"
1522
+ else:
1523
+ ip_version = "ipv4"
1524
+
1525
+ response = _server_ip_add(client, server_id, version=ip_version, ptr=ptr)
1526
+ fmt.printer(
1527
+ response,
1528
+ output_format=output_format,
1529
+ func=lambda response: click.echo(response.json()["server_ip"]["ip"]),
1530
+ )
1531
+
1532
+
1533
+ # ------------------------------------------------------------- #
1534
+ # $ twc server ip remove #
1535
+ # ------------------------------------------------------------- #
1536
+
1537
+
1538
+ def get_server_id_by_ip(client, ip_address):
1539
+ """Return server_id if IP address found or return None."""
1540
+ servers = _server_list(client, limit=10000).json()["servers"]
1541
+ for server in servers:
1542
+ for network in server["networks"]:
1543
+ for ip_addr in network["ips"]:
1544
+ if ip_address == ip_addr["ip"]:
1545
+ return server["id"]
1546
+ return None
1547
+
1548
+
1549
+ @ip_addr.command("remove", aliases=["rm"], help="Remove IP address.")
1550
+ @options(GLOBAL_OPTIONS)
1551
+ @click.confirmation_option(
1552
+ prompt="This action cannot be undone. Are you sure?"
1553
+ )
1554
+ @click.argument("ip_address", required=True)
1555
+ def server_ip_remove(config, profile, verbose, ip_address):
1556
+ client = create_client(config, profile)
1557
+
1558
+ log("Looking for IP address...")
1559
+ server_id = get_server_id_by_ip(client, ip_address)
1560
+ if not server_id:
1561
+ sys.exit(f"IP address '{ip_address}' not found.")
1562
+
1563
+ log("Check IP...")
1564
+ ips = _server_ip_list(client, server_id).json()["server_ips"]
1565
+ for ip_addr in ips:
1566
+ if ip_addr["ip"] == ip_address and ip_addr["is_main"]:
1567
+ sys.exit("Error: Cannot remove Cloud Server primaty IP address.")
1568
+
1569
+ response = _server_ip_remove(client, server_id, ip_address)
1570
+
1571
+ if response.status_code == 204:
1572
+ click.echo(ip_address)
1573
+ else:
1574
+ fmt.printer(response)
1575
+
1576
+
1577
+ # ------------------------------------------------------------- #
1578
+ # $ twc server ip set-ptr #
1579
+ # ------------------------------------------------------------- #
1580
+
1581
+
1582
+ @ip_addr.command("set-ptr", help="Set IP pointer (RDNS).")
1583
+ @options(GLOBAL_OPTIONS)
1584
+ @options(OUTPUT_FORMAT_OPTION)
1585
+ @click.option("--address", "ip_address", required=True, help="IP address.")
1586
+ @click.argument("ptr", required=True)
1587
+ def server_ip_set_ptr(
1588
+ config, profile, verbose, output_format, ip_address, ptr
1589
+ ):
1590
+ client = create_client(config, profile)
1591
+
1592
+ log("Looking for IP address...")
1593
+ server_id = get_server_id_by_ip(client, ip_address)
1594
+ if not server_id:
1595
+ sys.exit(f"IP address '{ip_address}' not found.")
1596
+
1597
+ response = _server_ip_set_ptr(
1598
+ client, server_id, ip_addr=ip_address, ptr=ptr
1599
+ )
1600
+ fmt.printer(
1601
+ response,
1602
+ output_format=output_format,
1603
+ func=lambda response: click.echo(response.json()["server_ip"]["ip"]),
1604
+ )
1605
+
1606
+
1607
+ # ------------------------------------------------------------- #
1608
+ # $ twc server disk #
1609
+ # ------------------------------------------------------------- #
1610
+
1611
+
1612
+ @server.group("disk", cls=ClickAliasedGroup)
1613
+ @options(GLOBAL_OPTIONS[:2])
1614
+ def disk():
1615
+ """Manage Cloud Server disks."""
1616
+
1617
+
1618
+ # ------------------------------------------------------------- #
1619
+ # $ twc server disk list #
1620
+ # ------------------------------------------------------------- #
1621
+
1622
+
1623
+ def print_disks(response: object):
1624
+ disks = response.json()["server_disks"]
1625
+ table = fmt.Table()
1626
+ table.header(
1627
+ [
1628
+ "ID",
1629
+ "NAME",
1630
+ "MOUNTED",
1631
+ "SYSTEM",
1632
+ "TYPE",
1633
+ "STATUS",
1634
+ "SIZE",
1635
+ "USED",
1636
+ ]
1637
+ )
1638
+ for disk in disks:
1639
+ table.row(
1640
+ [
1641
+ disk["id"],
1642
+ disk["system_name"],
1643
+ disk["is_mounted"],
1644
+ disk["is_system"],
1645
+ disk["type"],
1646
+ disk["status"],
1647
+ str(round(disk["size"] / 1024)) + "G",
1648
+ str(round(disk["used"] / 1024, 1)) + "G",
1649
+ ]
1650
+ )
1651
+ table.print()
1652
+
1653
+
1654
+ @disk.command("list", aliases=["ls"], help="List Cloud Server disks.")
1655
+ @options(GLOBAL_OPTIONS)
1656
+ @options(OUTPUT_FORMAT_OPTION)
1657
+ @click.argument("server_id", type=int, required=True)
1658
+ def server_disk_list(config, profile, verbose, output_format, server_id):
1659
+ client = create_client(config, profile)
1660
+ response = _server_disk_list(client, server_id)
1661
+ fmt.printer(response, output_format=output_format, func=print_disks)
1662
+
1663
+
1664
+ # ------------------------------------------------------------- #
1665
+ # $ twc server disk get #
1666
+ # ------------------------------------------------------------- #
1667
+
1668
+
1669
+ def print_disk(response: object):
1670
+ disk = response.json()["server_disk"]
1671
+ table = fmt.Table()
1672
+ table.header(
1673
+ [
1674
+ "ID",
1675
+ "NAME",
1676
+ "MOUNTED",
1677
+ "SYSTEM",
1678
+ "TYPE",
1679
+ "STATUS",
1680
+ "SIZE",
1681
+ "USED",
1682
+ ]
1683
+ )
1684
+ table.row(
1685
+ [
1686
+ disk["id"],
1687
+ disk["system_name"],
1688
+ disk["is_mounted"],
1689
+ disk["is_system"],
1690
+ disk["type"],
1691
+ disk["status"],
1692
+ str(round(disk["size"] / 1024)) + "G",
1693
+ str(round(disk["used"] / 1024, 1)) + "G",
1694
+ ]
1695
+ )
1696
+ table.print()
1697
+
1698
+
1699
+ def get_server_id_by_disk_id(client, disk_id: int) -> int:
1700
+ log("Looking for server_id by disk_id...")
1701
+ server_id = None
1702
+ servers = _server_list(client, limit=10000).json()["servers"]
1703
+ for server in servers:
1704
+ for disk in server["disks"]:
1705
+ if int(disk_id) == disk["id"]:
1706
+ server_id = server["id"]
1707
+ return server_id
1708
+
1709
+
1710
+ @disk.command("get", help="Get Cloud Server disk.")
1711
+ @options(GLOBAL_OPTIONS)
1712
+ @options(OUTPUT_FORMAT_OPTION)
1713
+ @click.argument("disk_id", type=int, required=True)
1714
+ def server_disk_get(config, profile, verbose, output_format, disk_id):
1715
+ client = create_client(config, profile)
1716
+ server_id = get_server_id_by_disk_id(client, disk_id)
1717
+ if not server_id:
1718
+ sys.exit(f"Error: Disk {disk_id} not found.")
1719
+ response = _server_disk_get(client, server_id, disk_id)
1720
+ fmt.printer(response, output_format=output_format, func=print_disk)
1721
+
1722
+
1723
+ # ------------------------------------------------------------- #
1724
+ # $ twc server disk add #
1725
+ # ------------------------------------------------------------- #
1726
+
1727
+
1728
+ @disk.command("add", help="Add disk to Cloud Server.")
1729
+ @options(GLOBAL_OPTIONS)
1730
+ @options(OUTPUT_FORMAT_OPTION)
1731
+ @click.option("--size", required=True, help="Disk size e.g. 50G.")
1732
+ @click.option(
1733
+ "--to-server",
1734
+ "server_id",
1735
+ type=int,
1736
+ metavar="SERVER_ID",
1737
+ required=True,
1738
+ help="Cloud Server ID.",
1739
+ )
1740
+ def server_disk_add(config, profile, verbose, output_format, size, server_id):
1741
+ client = create_client(config, profile)
1742
+ if check_value(size_to_mb(size), minv=5120, maxv=512000, step=5120):
1743
+ response = _server_disk_add(client, server_id, size=size_to_mb(size))
1744
+ else:
1745
+ raise click.BadParameter(
1746
+ "Value must be in range 5120-512000M with step 5120."
1747
+ )
1748
+ fmt.printer(
1749
+ response,
1750
+ output_format=output_format,
1751
+ func=lambda response: click.echo(response.json()["server_disk"]["id"]),
1752
+ )
1753
+
1754
+
1755
+ # ------------------------------------------------------------- #
1756
+ # $ twc server disk remove #
1757
+ # ------------------------------------------------------------- #
1758
+
1759
+
1760
+ @disk.command("remove", aliases=["rm"], help="Remove disks.")
1761
+ @options(GLOBAL_OPTIONS)
1762
+ @options(OUTPUT_FORMAT_OPTION)
1763
+ @click.confirmation_option(prompt="This action cannot be undone, continue?")
1764
+ @click.argument("disk_ids", nargs=-1, type=int, required=True)
1765
+ def server_disk_remove(config, profile, verbose, output_format, disk_ids):
1766
+ client = create_client(config, profile)
1767
+ for disk_id in disk_ids:
1768
+ server_id = get_server_id_by_disk_id(client, disk_id)
1769
+ response = _server_disk_remove(client, server_id, disk_id)
1770
+ if response.status_code == 204:
1771
+ click.echo(disk_id)
1772
+ else:
1773
+ fmt.printer(response)
1774
+
1775
+
1776
+ # ------------------------------------------------------------- #
1777
+ # $ twc server disk resize #
1778
+ # ------------------------------------------------------------- #
1779
+
1780
+
1781
+ @disk.command("resize", help="Change disk size (only increase).")
1782
+ @options(GLOBAL_OPTIONS)
1783
+ @options(OUTPUT_FORMAT_OPTION)
1784
+ @click.option("--size", required=True, help="Disk size e.g. 50G.")
1785
+ @click.argument("disk_id", type=int, required=True)
1786
+ def server_disk_resize(config, profile, verbose, output_format, size, disk_id):
1787
+ client = create_client(config, profile)
1788
+ server_id = get_server_id_by_disk_id(client, disk_id)
1789
+ if check_value(size_to_mb(size), minv=5120, maxv=512000, step=5120):
1790
+ response = _server_disk_resize(
1791
+ client, server_id, disk_id, size=size_to_mb(size)
1792
+ )
1793
+ else:
1794
+ raise click.BadParameter(
1795
+ "Value must be in range 5120-512000M with step 5120."
1796
+ )
1797
+ fmt.printer(
1798
+ response,
1799
+ output_format=output_format,
1800
+ func=lambda response: click.echo(response.json()["server_disk"]["id"]),
1801
+ )
1802
+
1803
+
1804
+ # ------------------------------------------------------------- #
1805
+ # $ twc server disk auto-backup #
1806
+ # ------------------------------------------------------------- #
1807
+
1808
+
1809
+ def print_autobackup_settings(response: object):
1810
+ table = fmt.Table()
1811
+ settings = response.json()["auto_backups_settings"]
1812
+ translated_keys = {
1813
+ "copy_count": "Keep copies",
1814
+ "creation_start_at": "Backup start date",
1815
+ "is_enabled": "Enabled",
1816
+ "interval": "Interval",
1817
+ "day_of_week": "Day of week",
1818
+ }
1819
+ for key in settings.keys():
1820
+ table.row([translated_keys[key], ":", settings[key]])
1821
+ table.print()
1822
+
1823
+
1824
+ @disk.command("auto-backup", help="Manage disk automatic backups settings.")
1825
+ @options(GLOBAL_OPTIONS)
1826
+ @options(OUTPUT_FORMAT_OPTION)
1827
+ @click.option(
1828
+ "--status", is_flag=True, help="Display automatic backups status."
1829
+ )
1830
+ @click.option(
1831
+ "--enable/--disable",
1832
+ default=False,
1833
+ show_default=True,
1834
+ help="Enable backups.",
1835
+ )
1836
+ @click.option(
1837
+ "--keep",
1838
+ type=int,
1839
+ default=1,
1840
+ show_default=True,
1841
+ help="Number of copies to keep.",
1842
+ )
1843
+ @click.option(
1844
+ "--start-date",
1845
+ type=click.DateTime(formats=["%Y-%m-%d"]),
1846
+ default=datetime.date.today().strftime("%Y-%m-%d"),
1847
+ help="Start date of the first backup creation. [default: today]",
1848
+ )
1849
+ @click.option(
1850
+ "--interval",
1851
+ type=click.Choice(["day", "week", "month"]),
1852
+ default="day",
1853
+ show_default=True,
1854
+ help="Backup interval.",
1855
+ )
1856
+ @click.option(
1857
+ "--day-of-week",
1858
+ type=click.IntRange(min=1, max=7),
1859
+ help="The day of the week on which backups will be created."
1860
+ " Works only with '--interval week'.",
1861
+ )
1862
+ @click.argument("disk_id", type=int, required=True)
1863
+ def server_disk_autobackup(
1864
+ config,
1865
+ profile,
1866
+ verbose,
1867
+ output_format,
1868
+ status,
1869
+ enable,
1870
+ keep,
1871
+ start_date,
1872
+ interval,
1873
+ day_of_week,
1874
+ disk_id,
1875
+ ):
1876
+ client = create_client(config, profile)
1877
+ server_id = get_server_id_by_disk_id(client, disk_id)
1878
+
1879
+ if status:
1880
+ response = _server_disk_autobackup_status(client, server_id, disk_id)
1881
+ fmt.printer(
1882
+ response,
1883
+ output_format=output_format,
1884
+ func=print_autobackup_settings,
1885
+ )
1886
+ if response.json()["auto_backups_settings"]["is_enabled"]:
1887
+ sys.exit(0)
1888
+ else:
1889
+ sys.exit(1)
1890
+
1891
+ start_date = start_date.strftime("%Y-%m-%dT%H:%M:%SZ")
1892
+
1893
+ response = _server_disk_autobackup_update(
1894
+ client,
1895
+ server_id,
1896
+ disk_id,
1897
+ is_enabled=enable,
1898
+ copy_count=keep,
1899
+ creation_start_at=start_date,
1900
+ interval=interval,
1901
+ day_of_week=day_of_week,
1902
+ )
1903
+ fmt.printer(
1904
+ response,
1905
+ output_format=output_format,
1906
+ func=print_autobackup_settings,
1907
+ )
1908
+
1909
+
1910
+ # ------------------------------------------------------------- #
1911
+ # $ twc server backup #
1912
+ # ------------------------------------------------------------- #
1913
+
1914
+
1915
+ @server.group("backup", cls=ClickAliasedGroup)
1916
+ @options(GLOBAL_OPTIONS[:2])
1917
+ def backup():
1918
+ """Manage Cloud Server disk backups."""
1919
+
1920
+
1921
+ # ------------------------------------------------------------- #
1922
+ # $ twc server backup list #
1923
+ # ------------------------------------------------------------- #
1924
+
1925
+
1926
+ def print_backups(response: object):
1927
+ backups = response.json()["backups"]
1928
+ table = fmt.Table()
1929
+ table.header(
1930
+ [
1931
+ "ID",
1932
+ "DISK",
1933
+ "STATUS",
1934
+ "CREATED",
1935
+ "SIZE",
1936
+ "TYPE",
1937
+ "COMMENT",
1938
+ ]
1939
+ )
1940
+ for backup in backups:
1941
+ table.row(
1942
+ [
1943
+ backup["id"],
1944
+ backup["name"],
1945
+ backup["status"],
1946
+ backup["created_at"],
1947
+ str(round(backup["size"] / 1024)) + "G",
1948
+ backup["type"],
1949
+ backup["comment"],
1950
+ ]
1951
+ )
1952
+ table.print()
1953
+
1954
+
1955
+ @backup.command("list", aliases=["ls"], help="List backups by disk_id.")
1956
+ @options(GLOBAL_OPTIONS)
1957
+ @options(OUTPUT_FORMAT_OPTION)
1958
+ @click.argument("disk_id", type=int, required=True)
1959
+ def server_backup_list(config, profile, verbose, output_format, disk_id):
1960
+ client = create_client(config, profile)
1961
+ server_id = get_server_id_by_disk_id(client, disk_id)
1962
+ response = _server_backup_list(client, server_id, disk_id)
1963
+ fmt.printer(response, output_format=output_format, func=print_backups)
1964
+
1965
+
1966
+ # ------------------------------------------------------------- #
1967
+ # $ twc server backup get #
1968
+ # ------------------------------------------------------------- #
1969
+
1970
+
1971
+ def print_backup(response: object):
1972
+ backup = response.json()["backup"]
1973
+ table = fmt.Table()
1974
+ table.header(
1975
+ [
1976
+ "ID",
1977
+ "DISK",
1978
+ "STATUS",
1979
+ "CREATED",
1980
+ "SIZE",
1981
+ "TYPE",
1982
+ "COMMENT",
1983
+ ]
1984
+ )
1985
+ table.row(
1986
+ [
1987
+ backup["id"],
1988
+ backup["name"],
1989
+ backup["status"],
1990
+ backup["created_at"],
1991
+ str(round(backup["size"] / 1024)) + "G",
1992
+ backup["type"],
1993
+ backup["comment"],
1994
+ ]
1995
+ )
1996
+ table.print()
1997
+
1998
+
1999
+ @backup.command("get", help="Get backup.")
2000
+ @options(GLOBAL_OPTIONS)
2001
+ @options(OUTPUT_FORMAT_OPTION)
2002
+ @click.argument("disk_id", type=int, required=True)
2003
+ @click.argument("backup_id", type=int, required=True)
2004
+ def server_backup_get(
2005
+ config, profile, verbose, output_format, disk_id, backup_id
2006
+ ):
2007
+ client = create_client(config, profile)
2008
+ server_id = get_server_id_by_disk_id(client, disk_id)
2009
+ response = _server_backup_get(client, server_id, disk_id, backup_id)
2010
+ fmt.printer(response, output_format=output_format, func=print_backup)
2011
+
2012
+
2013
+ # ------------------------------------------------------------- #
2014
+ # $ twc server backup create #
2015
+ # ------------------------------------------------------------- #
2016
+
2017
+
2018
+ @backup.command("create", help="Create disk backup.")
2019
+ @options(GLOBAL_OPTIONS)
2020
+ @options(OUTPUT_FORMAT_OPTION)
2021
+ @click.option("--comment", type=str, default=None, help="Comment.")
2022
+ @click.argument("disk_id", type=int, required=True)
2023
+ def server_backup_create(
2024
+ config, profile, verbose, output_format, comment, disk_id
2025
+ ):
2026
+ client = create_client(config, profile)
2027
+ server_id = get_server_id_by_disk_id(client, disk_id)
2028
+ response = _server_backup_create(
2029
+ client, server_id, disk_id, comment=comment
2030
+ )
2031
+ fmt.printer(
2032
+ response,
2033
+ output_format=output_format,
2034
+ func=lambda response: click.echo(response.json()["backup"]["id"]),
2035
+ )
2036
+
2037
+
2038
+ # ------------------------------------------------------------- #
2039
+ # $ twc server backup set-property #
2040
+ # ------------------------------------------------------------- #
2041
+
2042
+
2043
+ @backup.command("set-property", help="Change backup properties.")
2044
+ @options(GLOBAL_OPTIONS)
2045
+ @options(OUTPUT_FORMAT_OPTION)
2046
+ @click.option("--comment", type=str, default=None, help="Comment.")
2047
+ @click.argument("disk_id", type=int, required=True)
2048
+ @click.argument("backup_id", type=int, required=True)
2049
+ def server_backup_set_property(
2050
+ config, profile, verbose, output_format, comment, disk_id, backup_id
2051
+ ):
2052
+ client = create_client(config, profile)
2053
+ server_id = get_server_id_by_disk_id(client, disk_id)
2054
+ response = _server_backup_set_property(
2055
+ client, server_id, disk_id, backup_id, comment=comment
2056
+ )
2057
+ fmt.printer(
2058
+ response,
2059
+ output_format=output_format,
2060
+ func=lambda response: click.echo(response.json()["backup"]["id"]),
2061
+ )
2062
+
2063
+
2064
+ # ------------------------------------------------------------- #
2065
+ # $ twc server backup remove #
2066
+ # ------------------------------------------------------------- #
2067
+
2068
+
2069
+ @backup.command("remove", aliases=["rm"], help="Remove backup.")
2070
+ @options(GLOBAL_OPTIONS)
2071
+ @click.argument("disk_id", type=int, required=True)
2072
+ @click.argument("backup_id", nargs=-1, type=int, required=True)
2073
+ @click.confirmation_option(
2074
+ prompt="This action cannot be undone. Are you sure?"
2075
+ )
2076
+ def server_backup_remove(config, profile, verbose, disk_id, backup_id):
2077
+ client = create_client(config, profile)
2078
+ server_id = get_server_id_by_disk_id(client, disk_id)
2079
+ for backup in backup_id:
2080
+ response = _server_backup_remove(client, server_id, disk_id, backup)
2081
+ if response.status_code == 204:
2082
+ click.echo(server_id)
2083
+ else:
2084
+ fmt.printer(response)
2085
+
2086
+
2087
+ # ------------------------------------------------------------- #
2088
+ # $ twc server backup restore #
2089
+ # ------------------------------------------------------------- #
2090
+
2091
+
2092
+ @backup.command("restore", help="Restore backup.")
2093
+ @options(GLOBAL_OPTIONS)
2094
+ @click.argument("disk_id", type=int, required=True)
2095
+ @click.argument("backup_id", type=int, required=True)
2096
+ @click.confirmation_option(prompt="Data on target disk will lost. Continue?")
2097
+ def server_backup_restore(config, profile, verbose, disk_id, backup_id):
2098
+ client = create_client(config, profile)
2099
+ server_id = get_server_id_by_disk_id(client, disk_id)
2100
+ response = _server_backup_do_action(
2101
+ client, server_id, disk_id, backup_id, action="restore"
2102
+ )
2103
+ if response.status_code == 204:
2104
+ click.echo(server_id)
2105
+ else:
2106
+ fmt.printer(response)
2107
+
2108
+
2109
+ # ------------------------------------------------------------- #
2110
+ # $ twc server backup mount #
2111
+ # ------------------------------------------------------------- #
2112
+
2113
+
2114
+ @backup.command("mount", help="Attach backup as external drive.")
2115
+ @options(GLOBAL_OPTIONS)
2116
+ @click.argument("disk_id", type=int, required=True)
2117
+ @click.argument("backup_id", type=int, required=True)
2118
+ def server_backup_mount(config, profile, verbose, disk_id, backup_id):
2119
+ client = create_client(config, profile)
2120
+ server_id = get_server_id_by_disk_id(client, disk_id)
2121
+ response = _server_backup_do_action(
2122
+ client, server_id, disk_id, backup_id, action="mount"
2123
+ )
2124
+ if response.status_code == 204:
2125
+ click.echo(server_id)
2126
+ else:
2127
+ fmt.printer(response)
2128
+
2129
+
2130
+ # ------------------------------------------------------------- #
2131
+ # $ twc server backup unmount #
2132
+ # ------------------------------------------------------------- #
2133
+
2134
+
2135
+ @backup.command("unmount", help="Detach backup from Cloud Server.")
2136
+ @options(GLOBAL_OPTIONS)
2137
+ @click.argument("disk_id", type=int, required=True)
2138
+ @click.argument("backup_id", type=int, required=True)
2139
+ def server_backup_unmount(config, profile, verbose, disk_id, backup_id):
2140
+ client = create_client(config, profile)
2141
+ server_id = get_server_id_by_disk_id(client, disk_id)
2142
+ response = _server_backup_do_action(
2143
+ client, server_id, disk_id, backup_id, action="unmount"
2144
+ )
2145
+ if response.status_code == 204:
2146
+ click.echo(server_id)
2147
+ else:
2148
+ fmt.printer(response)