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.
- CHANGELOG.md +48 -15
- twc/__main__.py +72 -1
- twc/__version__.py +1 -1
- twc/api/client.py +319 -2
- twc/click_ext.py +34 -0
- twc/commands/__init__.py +10 -43
- twc/commands/config.py +141 -22
- twc/commands/database.py +633 -0
- twc/commands/image.py +2 -2
- twc/commands/project.py +9 -0
- twc/commands/server.py +89 -47
- twc/commands/storage.py +818 -0
- twc/fmt.py +14 -1
- twc/utils.py +20 -0
- twc/vars.py +20 -0
- {twc_cli-1.1.0.dist-info → twc_cli-1.3.0.dist-info}/METADATA +2 -1
- twc_cli-1.3.0.dist-info/RECORD +26 -0
- {twc_cli-1.1.0.dist-info → twc_cli-1.3.0.dist-info}/WHEEL +1 -1
- twc_cli-1.1.0.dist-info/RECORD +0 -21
- {twc_cli-1.1.0.dist-info → twc_cli-1.3.0.dist-info}/COPYING +0 -0
- {twc_cli-1.1.0.dist-info → twc_cli-1.3.0.dist-info}/entry_points.txt +0 -0
twc/commands/storage.py
ADDED
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
"""Object Storage management commands."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click_aliases import ClickAliasedGroup
|
|
7
|
+
|
|
8
|
+
from twc import fmt
|
|
9
|
+
from twc.vars import TWC_S3_ENDPOINT
|
|
10
|
+
from . import (
|
|
11
|
+
create_client,
|
|
12
|
+
handle_request,
|
|
13
|
+
set_value_from_config,
|
|
14
|
+
options,
|
|
15
|
+
debug,
|
|
16
|
+
GLOBAL_OPTIONS,
|
|
17
|
+
OUTPUT_FORMAT_OPTION,
|
|
18
|
+
)
|
|
19
|
+
from .project import (
|
|
20
|
+
get_default_project_id,
|
|
21
|
+
_project_list,
|
|
22
|
+
_project_resource_move,
|
|
23
|
+
_project_resource_list_buckets,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@handle_request
|
|
28
|
+
def _storage_list(client, *args, **kwargs):
|
|
29
|
+
return client.get_buckets(*args, **kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@handle_request
|
|
33
|
+
def _storage_mb(client, *args, **kwargs):
|
|
34
|
+
return client.create_bucket(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@handle_request
|
|
38
|
+
def _storage_rb(client, *args, **kwargs):
|
|
39
|
+
return client.delete_bucket(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@handle_request
|
|
43
|
+
def _storage_set(client, *args, **kwargs):
|
|
44
|
+
return client.update_bucket(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@handle_request
|
|
48
|
+
def _storage_user_list(client, *args, **kwargs):
|
|
49
|
+
return client.get_storage_users(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@handle_request
|
|
53
|
+
def _storage_user_passwd(client, *args, **kwargs):
|
|
54
|
+
return client.update_storage_user_secret(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@handle_request
|
|
58
|
+
def _storage_transfer_new(client, *args, **kwargs):
|
|
59
|
+
return client.start_storage_transfer(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@handle_request
|
|
63
|
+
def _storage_transfer_status(client, *args, **kwargs):
|
|
64
|
+
return client.get_storage_transfer_status(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@handle_request
|
|
68
|
+
def _storage_subdomain_list(client, *args, **kwargs):
|
|
69
|
+
return client.get_bucket_subdomains(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@handle_request
|
|
73
|
+
def _storage_subdomain_add(client, *args, **kwargs):
|
|
74
|
+
return client.add_bucket_subdomains(*args, **kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@handle_request
|
|
78
|
+
def _storage_subdomain_remove(client, *args, **kwargs):
|
|
79
|
+
return client.delete_bucket_subdomains(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@handle_request
|
|
83
|
+
def _storage_subdomain_gencert(client, *args, **kwargs):
|
|
84
|
+
return client.gen_cert_for_bucket_subdomain(*args, **kwargs)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@handle_request
|
|
88
|
+
def _storage_list_presets(client, *args, **kwargs):
|
|
89
|
+
return client.get_storage_presets(*args, **kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------- #
|
|
93
|
+
# $ twc storage #
|
|
94
|
+
# ------------------------------------------------------------- #
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@click.group(
|
|
98
|
+
"storage",
|
|
99
|
+
cls=ClickAliasedGroup,
|
|
100
|
+
short_help="Manage Object Storage buckets.",
|
|
101
|
+
)
|
|
102
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
103
|
+
def storage():
|
|
104
|
+
"""Manage Object Storage buckets.
|
|
105
|
+
|
|
106
|
+
NOTE: TWC CLI does not implement S3-compatible API client, it uses
|
|
107
|
+
Timeweb Cloud specific API methods instead. Use third party S3 clients
|
|
108
|
+
to manage objects e.g. s3cmd, rclone, etc.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------- #
|
|
113
|
+
# $ twc storage list #
|
|
114
|
+
# ------------------------------------------------------------- #
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def print_buckets(response: object, filters: str):
|
|
118
|
+
if filters:
|
|
119
|
+
buckets = fmt.filter_list(response.json()["buckets"], filters)
|
|
120
|
+
else:
|
|
121
|
+
buckets = response.json()["buckets"]
|
|
122
|
+
|
|
123
|
+
table = fmt.Table()
|
|
124
|
+
table.header(
|
|
125
|
+
[
|
|
126
|
+
"ID",
|
|
127
|
+
"NAME",
|
|
128
|
+
"REGION",
|
|
129
|
+
"STATUS",
|
|
130
|
+
"TYPE",
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
for bucket in buckets:
|
|
134
|
+
table.row(
|
|
135
|
+
[
|
|
136
|
+
bucket["id"],
|
|
137
|
+
bucket["name"],
|
|
138
|
+
bucket["location"],
|
|
139
|
+
bucket["status"],
|
|
140
|
+
bucket["type"],
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
table.print()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@storage.command("list", aliases=["ls"], help="List buckets.")
|
|
147
|
+
@options(GLOBAL_OPTIONS)
|
|
148
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
149
|
+
@click.option("--filter", "-f", "filters", default="", help="Filter output.")
|
|
150
|
+
def storage_list(config, profile, verbose, output_format, filters):
|
|
151
|
+
client = create_client(config, profile)
|
|
152
|
+
response = _storage_list(client)
|
|
153
|
+
fmt.printer(
|
|
154
|
+
response,
|
|
155
|
+
output_format=output_format,
|
|
156
|
+
filters=filters,
|
|
157
|
+
func=print_buckets,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------- #
|
|
162
|
+
# $ twc storage list-presets #
|
|
163
|
+
# ------------------------------------------------------------- #
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def print_storage_presets(response: object, filters: str):
|
|
167
|
+
if filters:
|
|
168
|
+
presets = fmt.filter_list(response.json()["storages_presets"], filters)
|
|
169
|
+
else:
|
|
170
|
+
presets = response.json()["storages_presets"]
|
|
171
|
+
|
|
172
|
+
table = fmt.Table()
|
|
173
|
+
table.header(
|
|
174
|
+
[
|
|
175
|
+
"ID",
|
|
176
|
+
"REGION",
|
|
177
|
+
"PRICE",
|
|
178
|
+
"DISK",
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
for preset in presets:
|
|
182
|
+
table.row(
|
|
183
|
+
[
|
|
184
|
+
preset["id"],
|
|
185
|
+
preset["location"],
|
|
186
|
+
preset["price"],
|
|
187
|
+
str(round(preset["disk"] / 1024)) + "G",
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
table.print()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@storage.command(
|
|
194
|
+
"list-presets", aliases=["lp"], help="List Object Storage presets."
|
|
195
|
+
)
|
|
196
|
+
@options(GLOBAL_OPTIONS)
|
|
197
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
198
|
+
@click.option("--filter", "-f", "filters", default="", help="Filter output.")
|
|
199
|
+
@click.option("--region", help="Use region (location).")
|
|
200
|
+
def storage_list_presets(
|
|
201
|
+
config, profile, verbose, output_format, filters, region
|
|
202
|
+
):
|
|
203
|
+
if filters:
|
|
204
|
+
filters = filters.replace("region", "location")
|
|
205
|
+
if region:
|
|
206
|
+
if filters:
|
|
207
|
+
filters = filters + f",location:{region}"
|
|
208
|
+
else:
|
|
209
|
+
filters = f"location:{region}"
|
|
210
|
+
|
|
211
|
+
client = create_client(config, profile)
|
|
212
|
+
response = _storage_list_presets(client)
|
|
213
|
+
fmt.printer(
|
|
214
|
+
response,
|
|
215
|
+
output_format=output_format,
|
|
216
|
+
filters=filters,
|
|
217
|
+
func=print_storage_presets,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ------------------------------------------------------------- #
|
|
222
|
+
# $ twc storage mb #
|
|
223
|
+
# ------------------------------------------------------------- #
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@storage.command("mb", help="Make bucket.")
|
|
227
|
+
@options(GLOBAL_OPTIONS)
|
|
228
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
229
|
+
@click.option("--preset-id", type=int, required=True, help="Bucket preset ID.")
|
|
230
|
+
@click.option(
|
|
231
|
+
"--project-id",
|
|
232
|
+
type=int,
|
|
233
|
+
default=None,
|
|
234
|
+
envvar="TWC_PROJECT",
|
|
235
|
+
callback=set_value_from_config,
|
|
236
|
+
help="Add bucket to specific project.",
|
|
237
|
+
)
|
|
238
|
+
@click.option(
|
|
239
|
+
"--type",
|
|
240
|
+
"bucket_type",
|
|
241
|
+
type=click.Choice(["public", "private"]),
|
|
242
|
+
default="private",
|
|
243
|
+
show_default=True,
|
|
244
|
+
help="Bucket access policy.",
|
|
245
|
+
)
|
|
246
|
+
@click.argument("bucket_name", type=str, required=True)
|
|
247
|
+
def storage_mb(
|
|
248
|
+
config,
|
|
249
|
+
profile,
|
|
250
|
+
verbose,
|
|
251
|
+
output_format,
|
|
252
|
+
preset_id,
|
|
253
|
+
project_id,
|
|
254
|
+
bucket_type,
|
|
255
|
+
bucket_name,
|
|
256
|
+
):
|
|
257
|
+
# pylint: disable=too-many-locals
|
|
258
|
+
|
|
259
|
+
client = create_client(config, profile)
|
|
260
|
+
is_public = False
|
|
261
|
+
if bucket_type == "public":
|
|
262
|
+
is_public = True
|
|
263
|
+
|
|
264
|
+
if project_id:
|
|
265
|
+
debug("Check project_id")
|
|
266
|
+
projects = _project_list(client).json()["projects"]
|
|
267
|
+
if not project_id in [prj["id"] for prj in projects]:
|
|
268
|
+
raise click.BadParameter("Wrong project ID.")
|
|
269
|
+
|
|
270
|
+
debug(f"Create bucket 'NAME-PREFIX-{bucket_name}'")
|
|
271
|
+
response = _storage_mb(
|
|
272
|
+
client, name=bucket_name, preset_id=preset_id, is_public=is_public
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Add created bucket to project if set
|
|
276
|
+
if project_id:
|
|
277
|
+
src_project = get_default_project_id(client)
|
|
278
|
+
# Make useless request to avoid API bug (409 resource_not_found)
|
|
279
|
+
_r = _project_resource_list_buckets(client, src_project)
|
|
280
|
+
new_bucket_id = response.json()["bucket"]["id"]
|
|
281
|
+
debug(f"Add bucket '{new_bucket_id}' to project '{project_id}'")
|
|
282
|
+
project_resp = _project_resource_move(
|
|
283
|
+
client,
|
|
284
|
+
from_project=src_project,
|
|
285
|
+
to_project=project_id,
|
|
286
|
+
resource_id=new_bucket_id,
|
|
287
|
+
resource_type="storage",
|
|
288
|
+
)
|
|
289
|
+
debug(project_resp.text)
|
|
290
|
+
|
|
291
|
+
fmt.printer(
|
|
292
|
+
response,
|
|
293
|
+
output_format=output_format,
|
|
294
|
+
func=lambda response: click.echo(response.json()["bucket"]["name"]),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ------------------------------------------------------------- #
|
|
299
|
+
# $ twc storage rb #
|
|
300
|
+
# ------------------------------------------------------------- #
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_bucket_id_by_name(client, bucket_name: str):
|
|
304
|
+
debug(f"Get bucket_id by name '{bucket_name}'")
|
|
305
|
+
buckets = _storage_list(client).json()["buckets"]
|
|
306
|
+
for bucket in buckets:
|
|
307
|
+
if bucket["name"] == bucket_name:
|
|
308
|
+
return bucket["id"]
|
|
309
|
+
if str(bucket["id"]) == bucket_name:
|
|
310
|
+
return bucket["id"]
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@storage.command("rb", help="Remove bucket.")
|
|
315
|
+
@options(GLOBAL_OPTIONS)
|
|
316
|
+
@click.confirmation_option(prompt="This action cannot be undone. Continue?")
|
|
317
|
+
@click.argument("buckets", nargs=-1, required=True)
|
|
318
|
+
def storage_rb(config, profile, verbose, buckets):
|
|
319
|
+
client = create_client(config, profile)
|
|
320
|
+
for bucket in buckets:
|
|
321
|
+
bucket_id = get_bucket_id_by_name(client, bucket)
|
|
322
|
+
if not bucket_id:
|
|
323
|
+
sys.exit(f"Error: Bucket '{bucket}' not found.")
|
|
324
|
+
|
|
325
|
+
response = _storage_rb(client, bucket_id)
|
|
326
|
+
|
|
327
|
+
if response.status_code == 200:
|
|
328
|
+
del_hash = response.json()["bucket_delete"]["hash"]
|
|
329
|
+
del_code = click.prompt("Please enter confirmation code", type=int)
|
|
330
|
+
response = _storage_rb(
|
|
331
|
+
client, bucket_id, delete_hash=del_hash, code=del_code
|
|
332
|
+
)
|
|
333
|
+
if response.status_code == 204:
|
|
334
|
+
click.echo(bucket)
|
|
335
|
+
else:
|
|
336
|
+
fmt.printer(response)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ------------------------------------------------------------- #
|
|
340
|
+
# $ twc storage set #
|
|
341
|
+
# ------------------------------------------------------------- #
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@storage.command("set", help="Set bucket parameters and properties.")
|
|
345
|
+
@options(GLOBAL_OPTIONS)
|
|
346
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
347
|
+
@click.option("--preset-id", type=int, help="Bucket preset ID.")
|
|
348
|
+
@click.option(
|
|
349
|
+
"--type",
|
|
350
|
+
"bucket_type",
|
|
351
|
+
type=click.Choice(["public", "private"]),
|
|
352
|
+
default=None,
|
|
353
|
+
help="Bucket access policy.",
|
|
354
|
+
)
|
|
355
|
+
@click.argument("bucket", required=True)
|
|
356
|
+
def storage_set(
|
|
357
|
+
config,
|
|
358
|
+
profile,
|
|
359
|
+
verbose,
|
|
360
|
+
output_format,
|
|
361
|
+
preset_id,
|
|
362
|
+
bucket_type,
|
|
363
|
+
bucket,
|
|
364
|
+
):
|
|
365
|
+
client = create_client(config, profile)
|
|
366
|
+
|
|
367
|
+
bucket_id = get_bucket_id_by_name(client, bucket)
|
|
368
|
+
if not bucket_id:
|
|
369
|
+
sys.exit(f"Error: Bucket '{bucket}' not found.")
|
|
370
|
+
|
|
371
|
+
payload = {}
|
|
372
|
+
|
|
373
|
+
if preset_id:
|
|
374
|
+
payload["preset_id"] = preset_id
|
|
375
|
+
|
|
376
|
+
if bucket_type:
|
|
377
|
+
if bucket_type == "public":
|
|
378
|
+
payload["is_public"] = True
|
|
379
|
+
else:
|
|
380
|
+
payload["is_public"] = False
|
|
381
|
+
|
|
382
|
+
response = _storage_set(client, bucket_id, **payload)
|
|
383
|
+
|
|
384
|
+
fmt.printer(
|
|
385
|
+
response,
|
|
386
|
+
output_format=output_format,
|
|
387
|
+
func=lambda response: click.echo(response.json()["bucket"]["name"]),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------- #
|
|
392
|
+
# $ twc storage user #
|
|
393
|
+
# ------------------------------------------------------------- #
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@storage.group("user", cls=ClickAliasedGroup)
|
|
397
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
398
|
+
def storage_user():
|
|
399
|
+
"""Manage Object Storage users."""
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ------------------------------------------------------------- #
|
|
403
|
+
# $ twc storage user list #
|
|
404
|
+
# ------------------------------------------------------------- #
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def print_storage_users(response: object):
|
|
408
|
+
users = response.json()["users"]
|
|
409
|
+
|
|
410
|
+
table = fmt.Table()
|
|
411
|
+
table.header(
|
|
412
|
+
[
|
|
413
|
+
"ID",
|
|
414
|
+
"ACCESS KEY",
|
|
415
|
+
]
|
|
416
|
+
)
|
|
417
|
+
for user in users:
|
|
418
|
+
table.row(
|
|
419
|
+
[
|
|
420
|
+
user["id"],
|
|
421
|
+
user["access_key"],
|
|
422
|
+
]
|
|
423
|
+
)
|
|
424
|
+
table.print()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@storage_user.command("list", aliases=["ls"], help="List storage users.")
|
|
428
|
+
@options(GLOBAL_OPTIONS)
|
|
429
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
430
|
+
def storage_user_list(config, profile, verbose, output_format):
|
|
431
|
+
client = create_client(config, profile)
|
|
432
|
+
response = _storage_user_list(client)
|
|
433
|
+
fmt.printer(
|
|
434
|
+
response,
|
|
435
|
+
output_format=output_format,
|
|
436
|
+
func=print_storage_users,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ------------------------------------------------------------- #
|
|
441
|
+
# $ twc storage user passwd #
|
|
442
|
+
# ------------------------------------------------------------- #
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_storage_user(client, access_key: str = None):
|
|
446
|
+
"""Return user_id and access_key. If customer have only one storage
|
|
447
|
+
user on account `access_key` is optional, user_id and access_key will
|
|
448
|
+
taken from user info. If customer have multile storage users `access_key`
|
|
449
|
+
is required.
|
|
450
|
+
"""
|
|
451
|
+
users = _storage_user_list(client).json()
|
|
452
|
+
user_id = None
|
|
453
|
+
|
|
454
|
+
# If access_key argument is set, set effective access key
|
|
455
|
+
if users["meta"]["total"] == 1:
|
|
456
|
+
user_id = users["users"][0]["id"]
|
|
457
|
+
access_key = users["users"][0]["access_key"]
|
|
458
|
+
else:
|
|
459
|
+
if access_key:
|
|
460
|
+
for user in users["users"]:
|
|
461
|
+
if user["access_key"] == access_key:
|
|
462
|
+
user_id = user["id"]
|
|
463
|
+
return user_id, access_key
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@storage_user.command("passwd", help="Set new secret_key for storage user.")
|
|
467
|
+
@options(GLOBAL_OPTIONS)
|
|
468
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
469
|
+
@click.option(
|
|
470
|
+
"--secret-key", prompt=True, hide_input=True, confirmation_prompt=True
|
|
471
|
+
)
|
|
472
|
+
@click.argument("access_key", metavar="[ACCESS_KEY]", nargs=-1, type=str)
|
|
473
|
+
def storage_user_passwd(
|
|
474
|
+
config, profile, verbose, output_format, secret_key, access_key
|
|
475
|
+
):
|
|
476
|
+
client = create_client(config, profile)
|
|
477
|
+
if access_key:
|
|
478
|
+
access_key = list(access_key)[0] # get element from tuple
|
|
479
|
+
user_id, access_key = get_storage_user(client, access_key)
|
|
480
|
+
if not user_id:
|
|
481
|
+
sys.exit(f"User with access key '{access_key}' not found.")
|
|
482
|
+
debug(f"User ID is '{user_id}'")
|
|
483
|
+
debug(f"Change secret_key for '{user_id}'")
|
|
484
|
+
response = _storage_user_passwd(
|
|
485
|
+
client, user_id=user_id, secret_key=secret_key
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
fmt.printer(
|
|
489
|
+
response,
|
|
490
|
+
output_format=output_format,
|
|
491
|
+
func=lambda response: click.echo(
|
|
492
|
+
response.json()["user"]["access_key"]
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ------------------------------------------------------------- #
|
|
498
|
+
# $ twc storage transfer #
|
|
499
|
+
# ------------------------------------------------------------- #
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@storage.group(
|
|
503
|
+
"transfer",
|
|
504
|
+
short_help="File transfer between object storage buckets.",
|
|
505
|
+
hidden=True,
|
|
506
|
+
)
|
|
507
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
508
|
+
def storage_transfer():
|
|
509
|
+
"""File transfer between object storage buckets.
|
|
510
|
+
|
|
511
|
+
You can start file transfer from any S3-compatible object storage
|
|
512
|
+
(including Timeweb Cloud Object Storage) to specified destination
|
|
513
|
+
bucket.
|
|
514
|
+
|
|
515
|
+
WARNING: This feature have unstable API, may occur errors.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ------------------------------------------------------------- #
|
|
520
|
+
# $ twc storage transfer new #
|
|
521
|
+
# ------------------------------------------------------------- #
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@storage_transfer.command("new", help="Start new file tranfer.")
|
|
525
|
+
@options(GLOBAL_OPTIONS)
|
|
526
|
+
@click.option(
|
|
527
|
+
"--bucket", "src_bucket", required=True, help="Source bucket name."
|
|
528
|
+
)
|
|
529
|
+
@click.option("--access-key", required=True, help="Source bucket access key.")
|
|
530
|
+
@click.option("--secret-key", required=True, help="Source bucket secret key.")
|
|
531
|
+
@click.option("--region", default="", help="Source region.")
|
|
532
|
+
@click.option("--endpoint", default=None, help="Source storage endpoint.")
|
|
533
|
+
@click.option(
|
|
534
|
+
"--force-path-style", is_flag=True, help="Force path-style bucket address."
|
|
535
|
+
)
|
|
536
|
+
@click.argument("dst_bucket", required=True)
|
|
537
|
+
def storage_transfer_new(
|
|
538
|
+
config,
|
|
539
|
+
profile,
|
|
540
|
+
verbose,
|
|
541
|
+
access_key,
|
|
542
|
+
secret_key,
|
|
543
|
+
region,
|
|
544
|
+
endpoint,
|
|
545
|
+
force_path_style,
|
|
546
|
+
src_bucket,
|
|
547
|
+
dst_bucket,
|
|
548
|
+
):
|
|
549
|
+
client = create_client(config, profile)
|
|
550
|
+
response = _storage_transfer_new(
|
|
551
|
+
client,
|
|
552
|
+
src_bucket=src_bucket,
|
|
553
|
+
dst_bucket=dst_bucket,
|
|
554
|
+
access_key=access_key,
|
|
555
|
+
secret_key=secret_key,
|
|
556
|
+
endpoint=endpoint,
|
|
557
|
+
location=region,
|
|
558
|
+
force_path_style=force_path_style,
|
|
559
|
+
)
|
|
560
|
+
if response.status_code == 204:
|
|
561
|
+
click.echo(dst_bucket)
|
|
562
|
+
else:
|
|
563
|
+
fmt.printer(response)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# ------------------------------------------------------------- #
|
|
567
|
+
# $ twc storage transfer status #
|
|
568
|
+
# ------------------------------------------------------------- #
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def print_transfer_status(response):
|
|
572
|
+
transfer = response.json()["transfer_status"]
|
|
573
|
+
|
|
574
|
+
table = fmt.Table()
|
|
575
|
+
translated_keys = {
|
|
576
|
+
"status": "Status",
|
|
577
|
+
"tries": "Tries",
|
|
578
|
+
"total_count": "Total objects",
|
|
579
|
+
"total_size": "Total size",
|
|
580
|
+
"uploaded_count": "Uploaded objects",
|
|
581
|
+
"uploaded_size": "Uploaded (size)",
|
|
582
|
+
"errors": "Errors",
|
|
583
|
+
}
|
|
584
|
+
for key in transfer.keys():
|
|
585
|
+
table.row([translated_keys[key], ":", transfer[key]])
|
|
586
|
+
table.print()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@storage_transfer.command("status", help="Display file tranfer status.")
|
|
590
|
+
@options(GLOBAL_OPTIONS)
|
|
591
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
592
|
+
@click.argument("bucket", required=True)
|
|
593
|
+
def storage_transfer_status(config, profile, verbose, output_format, bucket):
|
|
594
|
+
client = create_client(config, profile)
|
|
595
|
+
bucket_id = get_bucket_id_by_name(client, bucket)
|
|
596
|
+
response = _storage_transfer_status(client, bucket_id)
|
|
597
|
+
fmt.printer(
|
|
598
|
+
response,
|
|
599
|
+
output_format=output_format,
|
|
600
|
+
func=print_transfer_status,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# ------------------------------------------------------------- #
|
|
605
|
+
# $ twc storage subdomain #
|
|
606
|
+
# ------------------------------------------------------------- #
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@storage.group("subdomain", cls=ClickAliasedGroup)
|
|
610
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
611
|
+
def storage_subdomain():
|
|
612
|
+
"""Manage subdomains."""
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# ------------------------------------------------------------- #
|
|
616
|
+
# $ twc storage subdomain list #
|
|
617
|
+
# ------------------------------------------------------------- #
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def print_subdomains(response):
|
|
621
|
+
subdomains = response.json()["subdomains"]
|
|
622
|
+
table = fmt.Table()
|
|
623
|
+
table.header(
|
|
624
|
+
[
|
|
625
|
+
"ID",
|
|
626
|
+
"SUBDOMAIN",
|
|
627
|
+
"CERT RELEASED",
|
|
628
|
+
"STATUS",
|
|
629
|
+
]
|
|
630
|
+
)
|
|
631
|
+
for sub in subdomains:
|
|
632
|
+
table.row(
|
|
633
|
+
[
|
|
634
|
+
sub["id"],
|
|
635
|
+
sub["subdomain"],
|
|
636
|
+
sub["cert_released"],
|
|
637
|
+
sub["status"],
|
|
638
|
+
]
|
|
639
|
+
)
|
|
640
|
+
table.print()
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@storage_subdomain.command(
|
|
644
|
+
"list", aliases=["ls"], help="List subdomains attached to bucket."
|
|
645
|
+
)
|
|
646
|
+
@options(GLOBAL_OPTIONS)
|
|
647
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
648
|
+
@click.argument("bucket", required=True)
|
|
649
|
+
def storage_subdomain_list(config, profile, verbose, output_format, bucket):
|
|
650
|
+
client = create_client(config, profile)
|
|
651
|
+
bucket_id = get_bucket_id_by_name(client, bucket)
|
|
652
|
+
debug(f"bucket_id {bucket_id}")
|
|
653
|
+
response = _storage_subdomain_list(client, bucket_id)
|
|
654
|
+
fmt.printer(
|
|
655
|
+
response,
|
|
656
|
+
output_format=output_format,
|
|
657
|
+
func=print_subdomains,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# ------------------------------------------------------------- #
|
|
662
|
+
# $ twc storage subdomain add #
|
|
663
|
+
# ------------------------------------------------------------- #
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def print_subdomains_state(response):
|
|
667
|
+
subdomains = response.json()["subdomains"]
|
|
668
|
+
table = fmt.Table()
|
|
669
|
+
table.header(
|
|
670
|
+
[
|
|
671
|
+
"SUBDOMAIN",
|
|
672
|
+
"STATUS",
|
|
673
|
+
]
|
|
674
|
+
)
|
|
675
|
+
for sub in subdomains:
|
|
676
|
+
table.row(
|
|
677
|
+
[
|
|
678
|
+
sub["subdomain"],
|
|
679
|
+
sub["status"],
|
|
680
|
+
]
|
|
681
|
+
)
|
|
682
|
+
table.print()
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
@storage_subdomain.command("add", help="Attach subdomains to bucket.")
|
|
686
|
+
@options(GLOBAL_OPTIONS)
|
|
687
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
688
|
+
@click.argument("subdomains", nargs=-1, required=True)
|
|
689
|
+
@click.argument("bucket", required=True)
|
|
690
|
+
def storage_subdomain_add(
|
|
691
|
+
config, profile, verbose, output_format, bucket, subdomains
|
|
692
|
+
):
|
|
693
|
+
client = create_client(config, profile)
|
|
694
|
+
bucket_id = get_bucket_id_by_name(client, bucket)
|
|
695
|
+
response = _storage_subdomain_add(client, bucket_id, list(subdomains))
|
|
696
|
+
fmt.printer(
|
|
697
|
+
response,
|
|
698
|
+
output_format=output_format,
|
|
699
|
+
func=print_subdomains_state,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# ------------------------------------------------------------- #
|
|
704
|
+
# $ twc storage subdomain remove #
|
|
705
|
+
# ------------------------------------------------------------- #
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@storage_subdomain.command("remove", aliases=["rm"], help="Remove subdomains.")
|
|
709
|
+
@options(GLOBAL_OPTIONS)
|
|
710
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
711
|
+
@click.confirmation_option(prompt="Subdomains will be deleted. Continue?")
|
|
712
|
+
@click.argument("bucket", required=True)
|
|
713
|
+
@click.argument("subdomains", nargs=-1, required=True)
|
|
714
|
+
def storage_subdomain_remove(
|
|
715
|
+
config, profile, verbose, output_format, bucket, subdomains
|
|
716
|
+
):
|
|
717
|
+
client = create_client(config, profile)
|
|
718
|
+
bucket_id = get_bucket_id_by_name(client, bucket)
|
|
719
|
+
response = _storage_subdomain_remove(client, bucket_id, list(subdomains))
|
|
720
|
+
fmt.printer(
|
|
721
|
+
response,
|
|
722
|
+
output_format=output_format,
|
|
723
|
+
func=print_subdomains_state,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
# ------------------------------------------------------------- #
|
|
728
|
+
# $ twc storage subdomain gencert #
|
|
729
|
+
# ------------------------------------------------------------- #
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@storage_subdomain.command(
|
|
733
|
+
"gencert", help="Request TLS certificate for subdomains."
|
|
734
|
+
)
|
|
735
|
+
@options(GLOBAL_OPTIONS)
|
|
736
|
+
@click.argument("subdomains", nargs=-1, required=True)
|
|
737
|
+
def storage_subdomain_gencert(config, profile, verbose, subdomains):
|
|
738
|
+
client = create_client(config, profile)
|
|
739
|
+
for subdomain in subdomains:
|
|
740
|
+
response = _storage_subdomain_gencert(client, subdomain)
|
|
741
|
+
if response.status_code == 201:
|
|
742
|
+
click.echo(subdomain)
|
|
743
|
+
else:
|
|
744
|
+
fmt.printer(response)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# ------------------------------------------------------------- #
|
|
748
|
+
# $ twc storage genconfig #
|
|
749
|
+
# ------------------------------------------------------------- #
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
S3CMD_CONFIG_TEMPLATE = """
|
|
753
|
+
[default]
|
|
754
|
+
access_key = {access_key}
|
|
755
|
+
secret_key = {secret_key}
|
|
756
|
+
bucket_location = ru-1
|
|
757
|
+
host_base = {endpoint}
|
|
758
|
+
host_bucket = {endpoint}
|
|
759
|
+
use_https = True
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
RCLONE_CONFIG_TEMPLATE = """
|
|
763
|
+
[twc]
|
|
764
|
+
type = s3
|
|
765
|
+
provider = Other
|
|
766
|
+
env_auth = false
|
|
767
|
+
access_key_id = {access_key}
|
|
768
|
+
secret_access_key = {secret_key}
|
|
769
|
+
region = ru-1
|
|
770
|
+
endpoint = https://{endpoint}
|
|
771
|
+
"""
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@storage.command("genconfig", help="Generate config file for S3 clients.")
|
|
775
|
+
@options(GLOBAL_OPTIONS)
|
|
776
|
+
@click.option("--user-id", type=int, help="Object Storage user ID.")
|
|
777
|
+
@click.option(
|
|
778
|
+
"--client",
|
|
779
|
+
"s3_client",
|
|
780
|
+
type=click.Choice(["s3cmd", "rclone"]),
|
|
781
|
+
required=True,
|
|
782
|
+
help="S3 client.",
|
|
783
|
+
)
|
|
784
|
+
@click.option(
|
|
785
|
+
"--save-to", help="Path to file. NOTE: Existing file will be overwitten."
|
|
786
|
+
)
|
|
787
|
+
def storage_genconfig(config, profile, verbose, user_id, s3_client, save_to):
|
|
788
|
+
client = create_client(config, profile)
|
|
789
|
+
|
|
790
|
+
# Get access_key and secret_key by user_id (or not)
|
|
791
|
+
storage_users = _storage_user_list(client).json()["users"]
|
|
792
|
+
if user_id:
|
|
793
|
+
for user in storage_users:
|
|
794
|
+
if user_id == user["id"]:
|
|
795
|
+
access_key = user["access_key"]
|
|
796
|
+
secret_key = user[user_id]["secret_key"]
|
|
797
|
+
else:
|
|
798
|
+
sys.exit(f"Error: user with ID '{user_id}' not found.")
|
|
799
|
+
else:
|
|
800
|
+
user_id, access_key = get_storage_user(client)
|
|
801
|
+
secret_key = storage_users[0]["secret_key"]
|
|
802
|
+
|
|
803
|
+
templates = {
|
|
804
|
+
"s3cmd": S3CMD_CONFIG_TEMPLATE.strip(),
|
|
805
|
+
"rclone": RCLONE_CONFIG_TEMPLATE.strip(),
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
file_content = templates[s3_client].format(
|
|
809
|
+
access_key=access_key,
|
|
810
|
+
secret_key=secret_key,
|
|
811
|
+
endpoint=TWC_S3_ENDPOINT,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if save_to:
|
|
815
|
+
with open(save_to, "w", encoding="utf-8") as s3_config:
|
|
816
|
+
s3_config.write(file_content)
|
|
817
|
+
else:
|
|
818
|
+
fmt.print_colored(file_content, lang="ini")
|