twc-cli 2.3.0__py3-none-any.whl → 2.4.1__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/domain.py ADDED
@@ -0,0 +1,519 @@
1
+ """Manage domains and DNS records."""
2
+
3
+ import re
4
+ import sys
5
+ from typing import Optional, List
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from requests import Response
10
+
11
+ from twc import fmt
12
+ from twc.typerx import TyperAlias
13
+ from twc.apiwrap import create_client
14
+ from twc.api import DNSRecordType
15
+ from .common import (
16
+ verbose_option,
17
+ config_option,
18
+ profile_option,
19
+ filter_option,
20
+ output_format_option,
21
+ yes_option,
22
+ )
23
+
24
+
25
+ domain = TyperAlias(help=__doc__)
26
+ domain_subdomain = TyperAlias(help="Manage subdomains.")
27
+ domain_record = TyperAlias(help="Manage DNS records.")
28
+ domain.add_typer(domain_record, name="record", aliases=["records", "rec"])
29
+ domain.add_typer(
30
+ domain_subdomain, name="subdomain", aliases=["subdomains", "sub"]
31
+ )
32
+
33
+
34
+ # ------------------------------------------------------------- #
35
+ # $ twc domain list #
36
+ # ------------------------------------------------------------- #
37
+
38
+
39
+ def print_domains(
40
+ response: Response,
41
+ filters: Optional[str] = None,
42
+ with_subdomains: bool = False,
43
+ ):
44
+ """Print table with domains list."""
45
+ domains = response.json()["domains"]
46
+ if filters:
47
+ domains = fmt.filter_list(domains, filters)
48
+ table = fmt.Table()
49
+ table.header(
50
+ [
51
+ "FQDN",
52
+ "STATUS",
53
+ "EXPIRATION",
54
+ ]
55
+ )
56
+ for domain_json in domains:
57
+ table.row(
58
+ [
59
+ domain_json["fqdn"],
60
+ domain_json["domain_status"],
61
+ domain_json["expiration"],
62
+ ]
63
+ )
64
+ if with_subdomains:
65
+ for subdomain in domain_json["subdomains"]:
66
+ table.row(
67
+ [
68
+ subdomain["fqdn"],
69
+ "",
70
+ "",
71
+ ]
72
+ )
73
+ table.print()
74
+
75
+
76
+ @domain.command("list", "ls")
77
+ def domains_list(
78
+ verbose: Optional[bool] = verbose_option,
79
+ config: Optional[Path] = config_option,
80
+ profile: Optional[str] = profile_option,
81
+ output_format: Optional[str] = output_format_option,
82
+ filters: Optional[str] = filter_option,
83
+ limit: Optional[int] = typer.Option(
84
+ 100,
85
+ "--limit",
86
+ "-l",
87
+ help="Number of items to display.",
88
+ ),
89
+ with_subdomains: bool = typer.Option(
90
+ False,
91
+ "--all",
92
+ "-a",
93
+ help="Show subdomains too.",
94
+ ),
95
+ ):
96
+ """List domains."""
97
+ client = create_client(config, profile)
98
+ response = client.get_domains(limit=limit)
99
+ dom_count = response.json()["meta"]["total"]
100
+ if dom_count > limit:
101
+ print(
102
+ f"NOTE: Only {limit} of {dom_count} domain names is displayed.\n"
103
+ "NOTE: Use '--limit' option to set number of domains to display.",
104
+ file=sys.stderr,
105
+ )
106
+ fmt.printer(
107
+ response,
108
+ output_format=output_format,
109
+ filters=filters,
110
+ with_subdomains=with_subdomains,
111
+ func=print_domains,
112
+ )
113
+
114
+
115
+ # ------------------------------------------------------------- #
116
+ # $ twc domain info #
117
+ # ------------------------------------------------------------- #
118
+
119
+
120
+ def print_domain_info(response: Response):
121
+ """Print domain info."""
122
+ domain_json = response.json()["domain"]
123
+
124
+ output = (
125
+ f'Domain: {domain_json["fqdn"]}\n'
126
+ + f'Exp date: {domain_json["expiration"]}\n'
127
+ + f'Registrar: {domain_json["provider"]}\n'
128
+ + f'ID: {domain_json["id"]}\n'
129
+ + f'Technical: {domain_json["is_technical"]}\n'
130
+ + "Subdomains: \n"
131
+ + "".join(
132
+ (f' FQDN: {sub["fqdn"]}\n' + f' ID: {sub["id"]}\n')
133
+ for sub in domain_json["subdomains"]
134
+ )
135
+ )
136
+ print(output.strip())
137
+
138
+
139
+ @domain.command("info")
140
+ def domain_info(
141
+ domain_name: str,
142
+ verbose: Optional[bool] = verbose_option,
143
+ config: Optional[Path] = config_option,
144
+ profile: Optional[str] = profile_option,
145
+ output_format: Optional[str] = output_format_option,
146
+ ):
147
+ """Get domain info."""
148
+ client = create_client(config, profile)
149
+ response = client.get_domain(domain_name)
150
+ fmt.printer(
151
+ response,
152
+ output_format=output_format,
153
+ func=print_domain_info,
154
+ )
155
+
156
+
157
+ # ------------------------------------------------------------- #
158
+ # $ twc domain rm #
159
+ # ------------------------------------------------------------- #
160
+
161
+
162
+ @domain.command("remove", "rm")
163
+ def domain_delete(
164
+ domain_names: List[str] = typer.Argument(..., metavar="DOMAIN_NAME..."),
165
+ verbose: Optional[bool] = verbose_option,
166
+ config: Optional[Path] = config_option,
167
+ profile: Optional[str] = profile_option,
168
+ yes: Optional[bool] = yes_option,
169
+ force: bool = typer.Option(False, "--force", help="Force removal."),
170
+ ):
171
+ """Remove domain names."""
172
+ if not yes:
173
+ typer.confirm("This action cannot be undone. Continue?", abort=True)
174
+ client = create_client(config, profile)
175
+ for domain_name in domain_names:
176
+ # API Issue: API removes domain if subdomain is passed
177
+ # Prevent domain removal!
178
+ if re.match(r"^(.+\.){2}.+$", domain_name) and not force:
179
+ sys.exit(
180
+ "Error: It looks like you want to delete a subdomain.\n"
181
+ "Please use command 'twc domain rmsub SUBDOMAIN' for this.\n"
182
+ "NOTE: This command will delete the domain itself even if its"
183
+ " subdomain is passed. If you are sure you want to continue "
184
+ "use the '--force' option."
185
+ )
186
+ response = client.delete_domain(domain_name)
187
+ if response.status_code == 204:
188
+ print(domain_name)
189
+ else:
190
+ sys.exit(fmt.printer(response))
191
+
192
+
193
+ # ------------------------------------------------------------- #
194
+ # $ twc domain add #
195
+ # ------------------------------------------------------------- #
196
+
197
+
198
+ @domain.command("add")
199
+ def domain_add(
200
+ domain_name: str,
201
+ verbose: Optional[bool] = verbose_option,
202
+ config: Optional[Path] = config_option,
203
+ profile: Optional[str] = profile_option,
204
+ ):
205
+ """Add domain to account."""
206
+ client = create_client(config, profile)
207
+ response = client.add_domain(domain_name)
208
+ if response.status_code == 204:
209
+ print(domain_name)
210
+ else:
211
+ sys.exit(fmt.printer(response))
212
+
213
+
214
+ # ------------------------------------------------------------- #
215
+ # $ twc domain record list #
216
+ # ------------------------------------------------------------- #
217
+
218
+
219
+ def print_domain_record_list(
220
+ response: Response,
221
+ requested_domain: str,
222
+ filters: Optional[str] = None,
223
+ with_subdomains: bool = False,
224
+ ):
225
+ """Print domain records."""
226
+ records = response.json()["dns_records"]
227
+
228
+ if not with_subdomains:
229
+ records = filter(lambda x: "subdomain" not in x["data"], records)
230
+
231
+ if filters:
232
+ records = fmt.filter_list(records, filters)
233
+
234
+ table = fmt.Table()
235
+ table.header(
236
+ [
237
+ "NAME",
238
+ "ID",
239
+ "TYPE",
240
+ "VALUE",
241
+ ]
242
+ )
243
+
244
+ for record in records:
245
+ if "subdomain" in record["data"]:
246
+ _sub = record["data"]["subdomain"]
247
+ if _sub is None:
248
+ fqdn = requested_domain
249
+ else:
250
+ fqdn = _sub + "." + requested_domain
251
+ else:
252
+ fqdn = requested_domain
253
+ table.row(
254
+ [
255
+ fqdn,
256
+ record["id"],
257
+ record["type"],
258
+ record["data"]["value"],
259
+ ]
260
+ )
261
+ table.print()
262
+
263
+
264
+ @domain_record.command("list", "ls")
265
+ def domain_records_list(
266
+ domain_name: str,
267
+ verbose: Optional[bool] = verbose_option,
268
+ config: Optional[Path] = config_option,
269
+ profile: Optional[str] = profile_option,
270
+ output_format: Optional[str] = output_format_option,
271
+ filters: Optional[str] = filter_option,
272
+ with_subdomains: bool = typer.Option(
273
+ False,
274
+ "--all",
275
+ "-a",
276
+ help="Show subdomain records too.",
277
+ ),
278
+ ):
279
+ """List DNS-records on domain."""
280
+ client = create_client(config, profile)
281
+ response = client.get_domain_dns_records(domain_name)
282
+ fmt.printer(
283
+ response,
284
+ output_format=output_format,
285
+ filters=filters,
286
+ with_subdomains=with_subdomains,
287
+ requested_domain=domain_name,
288
+ func=print_domain_record_list,
289
+ )
290
+
291
+
292
+ # ------------------------------------------------------------- #
293
+ # $ twc domain record remove #
294
+ # ------------------------------------------------------------- #
295
+
296
+
297
+ @domain_record.command("remove", "rm")
298
+ def domain_remove_dns_record(
299
+ domain_name: str,
300
+ record_id: int,
301
+ verbose: Optional[bool] = verbose_option,
302
+ config: Optional[Path] = config_option,
303
+ profile: Optional[str] = profile_option,
304
+ ):
305
+ """Delete one DNS-record on domain."""
306
+ client = create_client(config, profile)
307
+ response = client.delete_domain_dns_record(domain_name, record_id)
308
+ if response.status_code == 204:
309
+ print(record_id)
310
+ else:
311
+ sys.exit(fmt.printer(response))
312
+
313
+
314
+ # ------------------------------------------------------------- #
315
+ # $ twc domain record add #
316
+ # ------------------------------------------------------------- #
317
+
318
+
319
+ @domain_record.command("add")
320
+ def domain_add_dns_record(
321
+ domain_name: str,
322
+ verbose: Optional[bool] = verbose_option,
323
+ config: Optional[Path] = config_option,
324
+ profile: Optional[str] = profile_option,
325
+ output_format: Optional[str] = output_format_option,
326
+ filters: Optional[str] = filter_option,
327
+ record_type: DNSRecordType = typer.Option(
328
+ ...,
329
+ "--type",
330
+ case_sensitive=False,
331
+ metavar="TYPE",
332
+ help=f"[{'|'.join([k.value for k in DNSRecordType])}]",
333
+ ),
334
+ value: Optional[str] = typer.Option(...),
335
+ priority: Optional[int] = typer.Option(
336
+ None,
337
+ "--prio",
338
+ help="Record priority. Supported for MX, SRV records.",
339
+ ),
340
+ second_ld: Optional[bool] = typer.Option(
341
+ False,
342
+ "--2ld",
343
+ help="Parse subdomain as 2LD.",
344
+ ),
345
+ ):
346
+ """Add dns record for domain or subdomain."""
347
+ client = create_client(config, profile)
348
+
349
+ if second_ld:
350
+ offset = 3
351
+ else:
352
+ offset = 2
353
+
354
+ subdomain = domain_name
355
+ domain_name = ".".join(domain_name.split(".")[-offset:])
356
+
357
+ if subdomain == domain_name:
358
+ subdomain = None
359
+
360
+ # API issue: see text below
361
+ # API can add TXT record (only TXT, why?) with non-existent subdomain,
362
+ # but 'subdomain' option must not be passed as FQDN!
363
+ # API issue: You cannot create subdomains with underscore. Why?
364
+ # Use previous described bug for this! Pass your subdomain with
365
+ # underscores to this function.
366
+ if record_type.lower() == "txt":
367
+ # 'ftp.example.org' --> 'ftp'
368
+ subdomain = ".".join(subdomain.split(".")[:-offset])
369
+
370
+ response = client.add_domain_dns_record(
371
+ domain_name, record_type, value, subdomain, priority
372
+ )
373
+ fmt.printer(
374
+ response,
375
+ output_format=output_format,
376
+ func=lambda response: print(response.json()["dns_record"]["id"]),
377
+ )
378
+
379
+
380
+ # ------------------------------------------------------------- #
381
+ # $ twc domain record update #
382
+ # ------------------------------------------------------------- #
383
+
384
+
385
+ @domain_record.command("update", "upd")
386
+ def domain_update_dns_records(
387
+ domain_name: str,
388
+ record_id: int,
389
+ verbose: Optional[bool] = verbose_option,
390
+ config: Optional[Path] = config_option,
391
+ profile: Optional[str] = profile_option,
392
+ output_format: Optional[str] = output_format_option,
393
+ filters: Optional[str] = filter_option,
394
+ record_type: DNSRecordType = typer.Option(
395
+ ...,
396
+ "--type",
397
+ case_sensitive=False,
398
+ metavar="TYPE",
399
+ help=f"[{'|'.join([k.value for k in DNSRecordType])}]",
400
+ ),
401
+ value: Optional[str] = typer.Option(...),
402
+ priority: Optional[int] = typer.Option(
403
+ None,
404
+ "--prio",
405
+ help="Record priority. Supported for MX, SRV records.",
406
+ ),
407
+ second_ld: Optional[bool] = typer.Option(
408
+ False,
409
+ "--2ld",
410
+ help="Parse subdomain as 2LD.",
411
+ ),
412
+ ):
413
+ """Update DNS record."""
414
+ client = create_client(config, profile)
415
+
416
+ if second_ld:
417
+ offset = 3
418
+ else:
419
+ offset = 2
420
+
421
+ subdomain = domain_name
422
+ domain_name = ".".join(domain_name.split(".")[-offset:])
423
+
424
+ if subdomain == domain_name:
425
+ subdomain = None
426
+
427
+ response = client.update_domain_dns_record(
428
+ domain_name, record_id, record_type, value, subdomain, priority
429
+ )
430
+ fmt.printer(
431
+ response,
432
+ output_format=output_format,
433
+ func=lambda response: print(response.json()["dns_record"]["id"]),
434
+ )
435
+
436
+
437
+ # ------------------------------------------------------------- #
438
+ # $ twc domain sub add #
439
+ # ------------------------------------------------------------- #
440
+
441
+
442
+ @domain_subdomain.command("add")
443
+ def domain_add_subdomain(
444
+ subdomain: str = typer.Argument(..., metavar="FQDN"),
445
+ verbose: Optional[bool] = verbose_option,
446
+ config: Optional[Path] = config_option,
447
+ profile: Optional[str] = profile_option,
448
+ output_format: Optional[str] = output_format_option,
449
+ filters: Optional[str] = filter_option,
450
+ second_ld: Optional[bool] = typer.Option(
451
+ False,
452
+ "--2ld",
453
+ help="Parse subdomain as 2LD.",
454
+ ),
455
+ ):
456
+ """Create subdomain."""
457
+ client = create_client(config, profile)
458
+
459
+ if second_ld:
460
+ offset = 3
461
+ else:
462
+ offset = 2
463
+
464
+ domain_name = ".".join(subdomain.split(".")[-offset:])
465
+ subdomain = ".".join(subdomain.split(".")[:-offset])
466
+
467
+ # API issue: You cannot create 'www' subdomain
468
+ if subdomain.startswith("www."):
469
+ sys.exit(
470
+ "Error: API does not support custom www subdomains. "
471
+ "www subdomain always have the same A-record with @"
472
+ )
473
+
474
+ response = client.add_subdomain(domain_name, subdomain)
475
+ fmt.printer(
476
+ response,
477
+ output_format=output_format,
478
+ func=lambda response: print(response.json()["subdomain"]["fqdn"]),
479
+ )
480
+
481
+
482
+ # ------------------------------------------------------------- #
483
+ # $ twc domain sub remove #
484
+ # ------------------------------------------------------------- #
485
+
486
+
487
+ @domain_subdomain.command("remove", "rm")
488
+ def domain_rm_subdomain(
489
+ subdomain: str = typer.Argument(..., metavar="FQDN"),
490
+ verbose: Optional[bool] = verbose_option,
491
+ config: Optional[Path] = config_option,
492
+ profile: Optional[str] = profile_option,
493
+ output_format: Optional[str] = output_format_option,
494
+ filters: Optional[str] = filter_option,
495
+ second_ld: Optional[bool] = typer.Option(
496
+ False,
497
+ "--2ld",
498
+ help="Parse subdomain as 2LD.",
499
+ ),
500
+ yes: Optional[bool] = yes_option,
501
+ ):
502
+ """Delete subdomain with they DNS records."""
503
+ if not yes:
504
+ typer.confirm("This action cannot be undone. Continue?", abort=True)
505
+ client = create_client(config, profile)
506
+
507
+ if second_ld:
508
+ offset = 3
509
+ else:
510
+ offset = 2
511
+
512
+ domain_name = ".".join(subdomain.split(".")[-offset:])
513
+ subdomain = ".".join(subdomain.split(".")[:-offset])
514
+
515
+ response = client.delete_subdomain(domain_name, subdomain)
516
+ if response.status_code == 204:
517
+ print(subdomain)
518
+ else:
519
+ sys.exit(fmt.printer(response))