dnctl 0.1.2__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.
- dnctl-0.1.2.dist-info/METADATA +124 -0
- dnctl-0.1.2.dist-info/RECORD +7 -0
- dnctl-0.1.2.dist-info/WHEEL +5 -0
- dnctl-0.1.2.dist-info/entry_points.txt +2 -0
- dnctl-0.1.2.dist-info/licenses/LICENSE +21 -0
- dnctl-0.1.2.dist-info/top_level.txt +1 -0
- dnctl.py +3297 -0
dnctl.py
ADDED
|
@@ -0,0 +1,3297 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "typer>=0.12",
|
|
6
|
+
# "httpx>=0.27",
|
|
7
|
+
# "rich>=13",
|
|
8
|
+
# "cryptography>=42",
|
|
9
|
+
# ]
|
|
10
|
+
# ///
|
|
11
|
+
"""dnctl - manage DevNomads services from the command line.
|
|
12
|
+
|
|
13
|
+
Single-file CLI for the DevNomads public API (https://api.devnomads.nl).
|
|
14
|
+
Create an API key in the control panel, run `dnctl configure`, and go.
|
|
15
|
+
See PLAN.md in the repository for the design.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
import configparser
|
|
22
|
+
import hashlib
|
|
23
|
+
import hmac
|
|
24
|
+
import io
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import secrets
|
|
28
|
+
import stat
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
import urllib.parse
|
|
32
|
+
from contextlib import nullcontext
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Annotated, Any, ContextManager, Iterable, Protocol
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
import typer
|
|
40
|
+
from rich.console import Console
|
|
41
|
+
from rich.table import Table
|
|
42
|
+
from typer.core import TyperGroup
|
|
43
|
+
|
|
44
|
+
__version__ = "0.1.2"
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Constants
|
|
48
|
+
|
|
49
|
+
DEFAULT_API_URL = "https://api.devnomads.nl"
|
|
50
|
+
DEFAULT_PROFILE = "default"
|
|
51
|
+
ENV_API_KEY = "DN_API_KEY"
|
|
52
|
+
ENV_PROFILE = "DN_PROFILE"
|
|
53
|
+
ENV_CONFIG_DIR = "DN_CONFIG_DIR"
|
|
54
|
+
PROVIDERS = ("auroradns", "transip")
|
|
55
|
+
REQUEST_TIMEOUT = 30.0
|
|
56
|
+
MAX_ATTEMPTS = 3
|
|
57
|
+
MAX_RETRY_DELAY = 30.0
|
|
58
|
+
RECORD_COLUMNS = ["name", "type", "ttl", "content", "disabled"]
|
|
59
|
+
|
|
60
|
+
# Data on stdout, everything meant for humans on stderr.
|
|
61
|
+
out_console = Console()
|
|
62
|
+
err_console = Console(stderr=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CliError(typer.Exit):
|
|
66
|
+
"""Fatal user-facing error: one line on stderr, exit code 1."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, message: str) -> None:
|
|
69
|
+
# soft_wrap keeps the error a single grep-able line
|
|
70
|
+
err_console.print(f"[red]error:[/] {message}", soft_wrap=True)
|
|
71
|
+
self.message = message
|
|
72
|
+
super().__init__(code=1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Config layer
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def config_dir() -> Path:
|
|
80
|
+
if env_dir := os.environ.get(ENV_CONFIG_DIR):
|
|
81
|
+
return Path(env_dir)
|
|
82
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
83
|
+
base = Path(xdg) if xdg else Path.home() / ".config"
|
|
84
|
+
return base / "dnctl"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def credentials_path() -> Path:
|
|
88
|
+
return config_dir() / "credentials"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_credentials() -> configparser.ConfigParser:
|
|
92
|
+
parser = configparser.ConfigParser()
|
|
93
|
+
path = credentials_path()
|
|
94
|
+
if path.exists():
|
|
95
|
+
mode = stat.S_IMODE(path.stat().st_mode)
|
|
96
|
+
if mode & 0o077:
|
|
97
|
+
err_console.print(
|
|
98
|
+
f"[yellow]warning:[/] {path} is readable by others "
|
|
99
|
+
f"(mode {mode:03o}); run: chmod 600 {path}"
|
|
100
|
+
)
|
|
101
|
+
parser.read(path)
|
|
102
|
+
return parser
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _write_private(path: Path, content: str) -> None:
|
|
106
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
path.parent.chmod(0o700)
|
|
108
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
109
|
+
with os.fdopen(fd, "w") as fh:
|
|
110
|
+
fh.write(content)
|
|
111
|
+
path.chmod(0o600)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def save_credentials(parser: configparser.ConfigParser) -> None:
|
|
115
|
+
buffer = io.StringIO()
|
|
116
|
+
parser.write(buffer)
|
|
117
|
+
_write_private(credentials_path(), buffer.getvalue())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def devnomads_profiles(parser: configparser.ConfigParser) -> list[str]:
|
|
121
|
+
# Plain sections are DevNomads accounts; "provider:name" sections
|
|
122
|
+
# belong to transfer source drivers.
|
|
123
|
+
return [section for section in parser.sections() if ":" not in section]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resolve_credentials(state: AppState) -> tuple[str, str]:
|
|
127
|
+
"""Resolve (api_key, api_url): flag > env > profile > [default]."""
|
|
128
|
+
|
|
129
|
+
parser = load_credentials()
|
|
130
|
+
profile = state.profile or os.environ.get(ENV_PROFILE) or DEFAULT_PROFILE
|
|
131
|
+
explicit = bool(state.profile or os.environ.get(ENV_PROFILE))
|
|
132
|
+
section = parser[profile] if parser.has_section(profile) else None
|
|
133
|
+
api_url = section.get("api_url", DEFAULT_API_URL) if section else DEFAULT_API_URL
|
|
134
|
+
api_key = (
|
|
135
|
+
state.api_key
|
|
136
|
+
or os.environ.get(ENV_API_KEY)
|
|
137
|
+
or (section.get("api_key") if section else None)
|
|
138
|
+
)
|
|
139
|
+
if not api_key:
|
|
140
|
+
if explicit and section is None:
|
|
141
|
+
available = ", ".join(devnomads_profiles(parser)) or "none"
|
|
142
|
+
raise CliError(
|
|
143
|
+
f"profile '{profile}' not found in {credentials_path()} "
|
|
144
|
+
f"(available: {available})"
|
|
145
|
+
)
|
|
146
|
+
raise CliError(f"no API key found; run `dnctl configure` or set {ENV_API_KEY}")
|
|
147
|
+
return api_key, api_url
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Output
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class OutputFormat(str, Enum):
|
|
155
|
+
table = "table"
|
|
156
|
+
json = "json"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class AppState:
|
|
161
|
+
profile: str | None = None
|
|
162
|
+
api_key: str | None = None
|
|
163
|
+
output: OutputFormat | None = None
|
|
164
|
+
debug: bool = False
|
|
165
|
+
client: DevNomadsClient | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def state_from(ctx: typer.Context, output: OutputFormat | None = None) -> AppState:
|
|
169
|
+
state = ctx.obj
|
|
170
|
+
if not isinstance(state, AppState): # direct invocation in tests
|
|
171
|
+
state = AppState()
|
|
172
|
+
ctx.obj = state
|
|
173
|
+
if output is not None: # per-command --output overrides the global one
|
|
174
|
+
state.output = output
|
|
175
|
+
return state
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def resolve_format(state: AppState) -> OutputFormat:
|
|
179
|
+
if state.output:
|
|
180
|
+
return state.output
|
|
181
|
+
return OutputFormat.table if sys.stdout.isatty() else OutputFormat.json
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def render(
|
|
185
|
+
state: AppState,
|
|
186
|
+
data: Any,
|
|
187
|
+
*,
|
|
188
|
+
columns: list[str] | None = None,
|
|
189
|
+
title: str | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Render data on stdout: JSON for machines, a rich table for humans."""
|
|
192
|
+
|
|
193
|
+
if resolve_format(state) is OutputFormat.json:
|
|
194
|
+
sys.stdout.write(json.dumps(data, indent=2, default=str) + "\n")
|
|
195
|
+
return
|
|
196
|
+
if isinstance(data, dict):
|
|
197
|
+
_render_kv(data, title)
|
|
198
|
+
elif isinstance(data, list):
|
|
199
|
+
_render_rows(data, columns, title)
|
|
200
|
+
else:
|
|
201
|
+
out_console.print(str(data), soft_wrap=True)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _cell(value: Any) -> str:
|
|
205
|
+
if value is None:
|
|
206
|
+
return ""
|
|
207
|
+
if isinstance(value, bool):
|
|
208
|
+
return "yes" if value else "no"
|
|
209
|
+
if isinstance(value, list) and not any(
|
|
210
|
+
isinstance(item, (dict, list)) for item in value
|
|
211
|
+
):
|
|
212
|
+
return ", ".join(str(item) for item in value)
|
|
213
|
+
if isinstance(value, (dict, list)):
|
|
214
|
+
return json.dumps(value)
|
|
215
|
+
return str(value)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _flatten_row(row: dict[str, Any]) -> dict[str, Any]:
|
|
219
|
+
"""Merge one level of nested objects into the row, so type-specific
|
|
220
|
+
details (e.g. the "proxy" object on proxy services) become real
|
|
221
|
+
columns. On a name collision the nested key gets a parent_ prefix."""
|
|
222
|
+
|
|
223
|
+
flat: dict[str, Any] = {}
|
|
224
|
+
nested: list[tuple[str, dict[str, Any]]] = []
|
|
225
|
+
for key, value in row.items():
|
|
226
|
+
if isinstance(value, dict):
|
|
227
|
+
nested.append((key, value))
|
|
228
|
+
else:
|
|
229
|
+
flat[key] = value
|
|
230
|
+
for parent, obj in nested:
|
|
231
|
+
for key, value in obj.items():
|
|
232
|
+
column = key if key not in flat else f"{parent}_{key}"
|
|
233
|
+
flat[column] = value
|
|
234
|
+
return flat
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _render_kv(data: dict[str, Any], title: str | None) -> None:
|
|
238
|
+
table = Table(title=title, show_header=False)
|
|
239
|
+
table.add_column(style="bold")
|
|
240
|
+
table.add_column()
|
|
241
|
+
for key, value in _flatten_row(data).items():
|
|
242
|
+
table.add_row(key, _cell(value))
|
|
243
|
+
out_console.print(table)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _render_rows(
|
|
247
|
+
rows: list[dict[str, Any]], columns: list[str] | None, title: str | None
|
|
248
|
+
) -> None:
|
|
249
|
+
if not rows:
|
|
250
|
+
err_console.print("[dim]no results[/]")
|
|
251
|
+
return
|
|
252
|
+
rows = [_flatten_row(row) if isinstance(row, dict) else row for row in rows]
|
|
253
|
+
cols = columns or list(rows[0].keys())
|
|
254
|
+
table = Table(title=title)
|
|
255
|
+
for col in cols:
|
|
256
|
+
table.add_column(col)
|
|
257
|
+
for row in rows:
|
|
258
|
+
table.add_row(*(_cell(row.get(col)) for col in cols))
|
|
259
|
+
out_console.print(table)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def sort_rows(rows: list[dict[str, Any]], sort: str | None) -> list[dict[str, Any]]:
|
|
263
|
+
"""Sort rows by a field; a leading '-' reverses. None sorts last."""
|
|
264
|
+
|
|
265
|
+
if not sort or not rows:
|
|
266
|
+
return rows
|
|
267
|
+
reverse = sort.startswith("-")
|
|
268
|
+
field = sort.lstrip("-")
|
|
269
|
+
if not any(field in row for row in rows):
|
|
270
|
+
available = ", ".join(rows[0].keys())
|
|
271
|
+
raise CliError(f"unknown sort field '{field}' (available: {available})")
|
|
272
|
+
values = [row.get(field) for row in rows]
|
|
273
|
+
numeric = all(
|
|
274
|
+
isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
275
|
+
for value in values
|
|
276
|
+
if value is not None
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def key(row: dict[str, Any]) -> tuple[int, Any]:
|
|
280
|
+
value = row.get(field)
|
|
281
|
+
if value is None:
|
|
282
|
+
return (1, 0 if numeric else "")
|
|
283
|
+
return (0, value if numeric else str(value).lower())
|
|
284
|
+
|
|
285
|
+
return sorted(rows, key=key, reverse=reverse)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _confirm(question: str, yes: bool) -> None:
|
|
289
|
+
if yes:
|
|
290
|
+
return
|
|
291
|
+
if not sys.stdin.isatty():
|
|
292
|
+
raise CliError("confirmation required; pass --yes to confirm non-interactively")
|
|
293
|
+
if not typer.confirm(question, err=True):
|
|
294
|
+
raise typer.Abort()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _mask(secret: str) -> str:
|
|
298
|
+
if not secret:
|
|
299
|
+
return ""
|
|
300
|
+
if len(secret) <= 8:
|
|
301
|
+
return "****"
|
|
302
|
+
return f"{secret[:4]}...{secret[-4:]}"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def working(message: str) -> ContextManager[Any]:
|
|
306
|
+
"""Spinner on stderr while a request is in flight. Inert when stderr
|
|
307
|
+
is not a terminal (pipelines, CI, redirects), so output stays clean.
|
|
308
|
+
Never nest two of these: rich allows one live display at a time."""
|
|
309
|
+
|
|
310
|
+
if err_console.is_terminal:
|
|
311
|
+
return err_console.status(message, spinner="dots")
|
|
312
|
+
return nullcontext()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# API client
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class DevNomadsClient:
|
|
320
|
+
"""Thin wrapper owning auth, errors and retries; commands never use
|
|
321
|
+
httpx directly."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, api_url: str, api_key: str, *, debug: bool = False) -> None:
|
|
324
|
+
self.debug = debug
|
|
325
|
+
self._http = httpx.Client(
|
|
326
|
+
base_url=api_url,
|
|
327
|
+
headers={
|
|
328
|
+
"Authorization": f"Bearer {api_key}",
|
|
329
|
+
"Accept": "application/json",
|
|
330
|
+
"User-Agent": f"dnctl/{__version__}",
|
|
331
|
+
},
|
|
332
|
+
timeout=REQUEST_TIMEOUT,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def request(
|
|
336
|
+
self,
|
|
337
|
+
method: str,
|
|
338
|
+
path: str,
|
|
339
|
+
*,
|
|
340
|
+
params: dict[str, Any] | None = None,
|
|
341
|
+
json_body: Any = None,
|
|
342
|
+
) -> Any:
|
|
343
|
+
with working(f"{method} {path}"):
|
|
344
|
+
response = self._send(method, path, params, json_body)
|
|
345
|
+
for attempt in range(1, MAX_ATTEMPTS):
|
|
346
|
+
if response.status_code != 429 and response.status_code < 500:
|
|
347
|
+
break
|
|
348
|
+
delay = _retry_delay(response, attempt)
|
|
349
|
+
err_console.print(
|
|
350
|
+
f"[yellow]got {response.status_code}, retrying in {delay:.0f}s[/]"
|
|
351
|
+
)
|
|
352
|
+
time.sleep(delay)
|
|
353
|
+
response = self._send(method, path, params, json_body)
|
|
354
|
+
if response.status_code >= 400:
|
|
355
|
+
raise CliError(_error_message(response))
|
|
356
|
+
if response.status_code == 204 or not response.content:
|
|
357
|
+
return None
|
|
358
|
+
try:
|
|
359
|
+
return _unwrap(response.json())
|
|
360
|
+
except ValueError as exc:
|
|
361
|
+
raise CliError(f"API returned invalid JSON: {exc}") from exc
|
|
362
|
+
|
|
363
|
+
def _send(
|
|
364
|
+
self,
|
|
365
|
+
method: str,
|
|
366
|
+
path: str,
|
|
367
|
+
params: dict[str, Any] | None,
|
|
368
|
+
json_body: Any,
|
|
369
|
+
) -> httpx.Response:
|
|
370
|
+
if self.debug:
|
|
371
|
+
suffix = f" {json.dumps(json_body)}" if json_body is not None else ""
|
|
372
|
+
err_console.print(f"[dim]> {method} {path}{suffix}[/]")
|
|
373
|
+
try:
|
|
374
|
+
response = self._http.request(method, path, params=params, json=json_body)
|
|
375
|
+
except httpx.HTTPError as exc:
|
|
376
|
+
raise CliError(f"request failed: {exc}") from exc
|
|
377
|
+
if self.debug:
|
|
378
|
+
err_console.print(f"[dim]< {response.status_code} {response.text[:500]}[/]")
|
|
379
|
+
return response
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _unwrap(body: Any) -> Any:
|
|
383
|
+
"""Strip the Laravel API resource envelope ({"data": ...}, optionally
|
|
384
|
+
with links/meta) so commands and json output see the actual data."""
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
isinstance(body, dict)
|
|
388
|
+
and "data" in body
|
|
389
|
+
and set(body)
|
|
390
|
+
<= {
|
|
391
|
+
"data",
|
|
392
|
+
"links",
|
|
393
|
+
"meta",
|
|
394
|
+
}
|
|
395
|
+
):
|
|
396
|
+
return body["data"]
|
|
397
|
+
return body
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _retry_delay(response: httpx.Response, attempt: int) -> float:
|
|
401
|
+
try:
|
|
402
|
+
delay = float(response.headers.get("Retry-After", ""))
|
|
403
|
+
except ValueError:
|
|
404
|
+
delay = float(2 ** (attempt - 1))
|
|
405
|
+
return min(delay, MAX_RETRY_DELAY)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _error_message(response: httpx.Response) -> str:
|
|
409
|
+
try:
|
|
410
|
+
body = response.json()
|
|
411
|
+
except ValueError:
|
|
412
|
+
body = None
|
|
413
|
+
detail = ""
|
|
414
|
+
if isinstance(body, dict):
|
|
415
|
+
detail = str(body.get("message") or body.get("error") or "")
|
|
416
|
+
errors = body.get("errors")
|
|
417
|
+
if isinstance(errors, dict):
|
|
418
|
+
flat = "; ".join(
|
|
419
|
+
(
|
|
420
|
+
f"{field}: {' '.join(map(str, messages))}"
|
|
421
|
+
if isinstance(messages, list)
|
|
422
|
+
else f"{field}: {messages}"
|
|
423
|
+
)
|
|
424
|
+
for field, messages in errors.items()
|
|
425
|
+
)
|
|
426
|
+
detail = f"{detail} ({flat})" if detail else flat
|
|
427
|
+
message = f"API error {response.status_code}"
|
|
428
|
+
if detail:
|
|
429
|
+
message += f": {detail}"
|
|
430
|
+
if response.status_code == 401:
|
|
431
|
+
message += " - check your API key (run `dnctl configure`)"
|
|
432
|
+
return message
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def get_client(state: AppState) -> DevNomadsClient:
|
|
436
|
+
if state.client is None:
|
|
437
|
+
api_key, api_url = resolve_credentials(state)
|
|
438
|
+
state.client = DevNomadsClient(api_url, api_key, debug=state.debug)
|
|
439
|
+
return state.client
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
# DNS record helpers (PowerDNS rrsets)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def zone_id(zone: str) -> str:
|
|
447
|
+
"""PowerDNS zone ids carry a trailing dot; accept names without it."""
|
|
448
|
+
|
|
449
|
+
return zone if zone.endswith(".") else f"{zone}."
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def fqdn(name: str, zone: str) -> str:
|
|
453
|
+
"""Absolute record name with trailing dot, PowerDNS style."""
|
|
454
|
+
|
|
455
|
+
zone_root = zone.rstrip(".")
|
|
456
|
+
relative = name.rstrip(".")
|
|
457
|
+
if relative in ("@", ""):
|
|
458
|
+
return f"{zone_root}."
|
|
459
|
+
if relative == zone_root or relative.endswith(f".{zone_root}"):
|
|
460
|
+
return f"{relative}."
|
|
461
|
+
return f"{relative}.{zone_root}."
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def flatten_rrsets(rrsets: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
465
|
+
"""One row per record value, for display and `records list`."""
|
|
466
|
+
|
|
467
|
+
rows = []
|
|
468
|
+
for rrset in rrsets or []:
|
|
469
|
+
for record in rrset.get("records", []):
|
|
470
|
+
rows.append(
|
|
471
|
+
{
|
|
472
|
+
"name": rrset.get("name"),
|
|
473
|
+
"type": rrset.get("type"),
|
|
474
|
+
"ttl": rrset.get("ttl"),
|
|
475
|
+
"content": record.get("content"),
|
|
476
|
+
"disabled": record.get("disabled", False),
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
return sorted(rows, key=lambda row: (row["name"] or "", row["type"] or ""))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def build_rrset(
|
|
483
|
+
zone: str,
|
|
484
|
+
name: str,
|
|
485
|
+
rtype: str,
|
|
486
|
+
*,
|
|
487
|
+
changetype: str,
|
|
488
|
+
ttl: int | None = None,
|
|
489
|
+
contents: Iterable[str] = (),
|
|
490
|
+
) -> dict[str, Any]:
|
|
491
|
+
rrset: dict[str, Any] = {
|
|
492
|
+
"name": fqdn(name, zone),
|
|
493
|
+
"type": rtype.upper(),
|
|
494
|
+
"changetype": changetype,
|
|
495
|
+
}
|
|
496
|
+
if changetype == "REPLACE":
|
|
497
|
+
rrset["ttl"] = ttl
|
|
498
|
+
rrset["records"] = [
|
|
499
|
+
{"content": content, "disabled": False} for content in contents
|
|
500
|
+
]
|
|
501
|
+
return rrset
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
# DNS transfer: pull a zone from another provider into DevNomads.
|
|
506
|
+
# Drivers are read-only by design; writing happens exclusively on the
|
|
507
|
+
# DevNomads side through the PowerDNS rrsets PATCH.
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@dataclass(frozen=True)
|
|
511
|
+
class TransferRecord:
|
|
512
|
+
"""Provider-neutral record: relative name ("@" for the apex), TTL in
|
|
513
|
+
seconds, and content in PowerDNS style (MX/SRV priority embedded)."""
|
|
514
|
+
|
|
515
|
+
name: str
|
|
516
|
+
type: str
|
|
517
|
+
content: str
|
|
518
|
+
ttl: int
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class DnsSource(Protocol):
|
|
522
|
+
name: str
|
|
523
|
+
|
|
524
|
+
def get_records(self, zone: str) -> list[TransferRecord]: ...
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
HOST_CONTENT_TYPES = {"CNAME", "NS", "PTR", "ALIAS", "MX", "SRV"}
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def normalize_content(rtype: str, content: str) -> str:
|
|
531
|
+
"""Canonicalize record content the PowerDNS way, so source and target
|
|
532
|
+
records compare equal: hostname targets get a trailing dot, TXT values
|
|
533
|
+
get quoted. Idempotent on already-canonical content."""
|
|
534
|
+
|
|
535
|
+
content = content.strip()
|
|
536
|
+
if rtype == "TXT":
|
|
537
|
+
return content if content.startswith('"') else f'"{content}"'
|
|
538
|
+
if rtype in HOST_CONTENT_TYPES:
|
|
539
|
+
parts = content.split()
|
|
540
|
+
if parts and not parts[-1].endswith("."):
|
|
541
|
+
parts[-1] += "."
|
|
542
|
+
return " ".join(parts)
|
|
543
|
+
return content
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _transfer_skip(name: str, rtype: str, zone: str) -> bool:
|
|
547
|
+
# the target zone owns its SOA and apex NS records
|
|
548
|
+
if rtype == "SOA":
|
|
549
|
+
return True
|
|
550
|
+
return rtype == "NS" and name == f"{zone.rstrip('.')}."
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def desired_rrsets(
|
|
554
|
+
records: Iterable[TransferRecord], zone: str
|
|
555
|
+
) -> dict[tuple[str, str], dict[str, Any]]:
|
|
556
|
+
"""Group normalized source records into PowerDNS-comparable rrsets."""
|
|
557
|
+
|
|
558
|
+
out: dict[tuple[str, str], dict[str, Any]] = {}
|
|
559
|
+
for record in records:
|
|
560
|
+
rtype = record.type.upper()
|
|
561
|
+
name = fqdn(record.name, zone)
|
|
562
|
+
if _transfer_skip(name, rtype, zone):
|
|
563
|
+
continue
|
|
564
|
+
entry = out.setdefault((name, rtype), {"ttl": record.ttl, "contents": set()})
|
|
565
|
+
entry["ttl"] = min(entry["ttl"], record.ttl)
|
|
566
|
+
entry["contents"].add(normalize_content(rtype, record.content))
|
|
567
|
+
return out
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def current_rrsets(
|
|
571
|
+
rrsets: Iterable[dict[str, Any]], zone: str
|
|
572
|
+
) -> dict[tuple[str, str], dict[str, Any]]:
|
|
573
|
+
"""The same comparable shape, from a DevNomads (PowerDNS) zone."""
|
|
574
|
+
|
|
575
|
+
out: dict[tuple[str, str], dict[str, Any]] = {}
|
|
576
|
+
for rrset in rrsets or []:
|
|
577
|
+
rtype = str(rrset.get("type", "")).upper()
|
|
578
|
+
name = str(rrset.get("name", ""))
|
|
579
|
+
if _transfer_skip(name, rtype, zone):
|
|
580
|
+
continue
|
|
581
|
+
out[(name, rtype)] = {
|
|
582
|
+
"ttl": rrset.get("ttl"),
|
|
583
|
+
"contents": {
|
|
584
|
+
normalize_content(rtype, record["content"])
|
|
585
|
+
for record in rrset.get("records", [])
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
return out
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def diff_rrsets(
|
|
592
|
+
current: dict[tuple[str, str], dict[str, Any]],
|
|
593
|
+
desired: dict[tuple[str, str], dict[str, Any]],
|
|
594
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
595
|
+
"""Compute (display rows, PATCH rrsets) to turn current into desired."""
|
|
596
|
+
|
|
597
|
+
changes: list[dict[str, Any]] = []
|
|
598
|
+
patch: list[dict[str, Any]] = []
|
|
599
|
+
for key in sorted(set(current) | set(desired)):
|
|
600
|
+
old, new = current.get(key), desired.get(key)
|
|
601
|
+
if old == new:
|
|
602
|
+
continue
|
|
603
|
+
name, rtype = key
|
|
604
|
+
if new is None:
|
|
605
|
+
if old is None:
|
|
606
|
+
continue
|
|
607
|
+
changes.append(
|
|
608
|
+
{
|
|
609
|
+
"action": "delete",
|
|
610
|
+
"name": name,
|
|
611
|
+
"type": rtype,
|
|
612
|
+
"ttl": old["ttl"],
|
|
613
|
+
"content": "; ".join(sorted(old["contents"])),
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
patch.append({"name": name, "type": rtype, "changetype": "DELETE"})
|
|
617
|
+
continue
|
|
618
|
+
changes.append(
|
|
619
|
+
{
|
|
620
|
+
"action": "create" if old is None else "update",
|
|
621
|
+
"name": name,
|
|
622
|
+
"type": rtype,
|
|
623
|
+
"ttl": new["ttl"],
|
|
624
|
+
"content": "; ".join(sorted(new["contents"])),
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
patch.append(
|
|
628
|
+
{
|
|
629
|
+
"name": name,
|
|
630
|
+
"type": rtype,
|
|
631
|
+
"ttl": new["ttl"],
|
|
632
|
+
"changetype": "REPLACE",
|
|
633
|
+
"records": [
|
|
634
|
+
{"content": content, "disabled": False}
|
|
635
|
+
for content in sorted(new["contents"])
|
|
636
|
+
],
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
return changes, patch
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class TransipSource:
|
|
643
|
+
"""TransIP REST API v6: RSA-SHA512 signed auth request for a read-only
|
|
644
|
+
JWT, then plain bearer requests."""
|
|
645
|
+
|
|
646
|
+
name = "transip"
|
|
647
|
+
|
|
648
|
+
def __init__(
|
|
649
|
+
self, login: str, private_key_pem: str, api_url: str | None = None
|
|
650
|
+
) -> None:
|
|
651
|
+
self.login = login
|
|
652
|
+
self.private_key_pem = private_key_pem
|
|
653
|
+
self._http = httpx.Client(
|
|
654
|
+
base_url=api_url or "https://api.transip.nl/v6",
|
|
655
|
+
headers={"User-Agent": f"dnctl/{__version__}"},
|
|
656
|
+
timeout=REQUEST_TIMEOUT,
|
|
657
|
+
)
|
|
658
|
+
self._authenticated = False
|
|
659
|
+
|
|
660
|
+
def _authenticate(self) -> None:
|
|
661
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
662
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
663
|
+
|
|
664
|
+
try:
|
|
665
|
+
key = serialization.load_pem_private_key(
|
|
666
|
+
self.private_key_pem.encode(), password=None
|
|
667
|
+
)
|
|
668
|
+
except ValueError as exc:
|
|
669
|
+
raise CliError(f"cannot load TransIP private key: {exc}") from exc
|
|
670
|
+
if not isinstance(key, rsa.RSAPrivateKey):
|
|
671
|
+
raise CliError("the TransIP private key must be an RSA key")
|
|
672
|
+
nonce = secrets.token_hex(16)
|
|
673
|
+
body = json.dumps(
|
|
674
|
+
{
|
|
675
|
+
"login": self.login,
|
|
676
|
+
"nonce": nonce,
|
|
677
|
+
"read_only": True,
|
|
678
|
+
"expiration_time": "30 minutes",
|
|
679
|
+
"label": f"dnctl-{nonce[:10]}",
|
|
680
|
+
"global_key": True,
|
|
681
|
+
}
|
|
682
|
+
)
|
|
683
|
+
signature = base64.b64encode(
|
|
684
|
+
key.sign(body.encode(), padding.PKCS1v15(), hashes.SHA512())
|
|
685
|
+
).decode()
|
|
686
|
+
with working("authenticating with TransIP"):
|
|
687
|
+
response = self._http.post(
|
|
688
|
+
"/auth",
|
|
689
|
+
content=body,
|
|
690
|
+
headers={"Signature": signature, "Content-Type": "application/json"},
|
|
691
|
+
)
|
|
692
|
+
if response.status_code >= 400:
|
|
693
|
+
raise CliError(
|
|
694
|
+
f"TransIP authentication failed ({response.status_code}): "
|
|
695
|
+
f"{_provider_error(response)} - check the login name and private "
|
|
696
|
+
"key, and that non-whitelisted API keys are allowed in the "
|
|
697
|
+
"TransIP control panel"
|
|
698
|
+
)
|
|
699
|
+
self._http.headers["Authorization"] = f"Bearer {response.json()['token']}"
|
|
700
|
+
self._authenticated = True
|
|
701
|
+
|
|
702
|
+
def get_records(self, zone: str) -> list[TransferRecord]:
|
|
703
|
+
if not self._authenticated:
|
|
704
|
+
self._authenticate()
|
|
705
|
+
with working(f"TransIP: GET /domains/{zone}/dns"):
|
|
706
|
+
response = self._http.get(f"/domains/{zone}/dns")
|
|
707
|
+
if response.status_code >= 400:
|
|
708
|
+
raise CliError(
|
|
709
|
+
f"TransIP returned {response.status_code} for zone '{zone}': "
|
|
710
|
+
f"{_provider_error(response)}"
|
|
711
|
+
)
|
|
712
|
+
return [
|
|
713
|
+
TransferRecord(
|
|
714
|
+
name=str(entry["name"]),
|
|
715
|
+
type=str(entry["type"]).upper(),
|
|
716
|
+
content=str(entry["content"]),
|
|
717
|
+
ttl=int(entry["expire"]),
|
|
718
|
+
)
|
|
719
|
+
for entry in response.json().get("dnsEntries", [])
|
|
720
|
+
]
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
class AuroraDnsSource:
|
|
724
|
+
"""AuroraDNS (PCextreme): HMAC-SHA256 signed requests, protocol as
|
|
725
|
+
implemented in Apache Libcloud's auroradns driver."""
|
|
726
|
+
|
|
727
|
+
name = "auroradns"
|
|
728
|
+
|
|
729
|
+
def __init__(
|
|
730
|
+
self, api_key: str, secret_key: str, api_url: str | None = None
|
|
731
|
+
) -> None:
|
|
732
|
+
self.api_key = api_key
|
|
733
|
+
self.secret_key = secret_key
|
|
734
|
+
self._http = httpx.Client(
|
|
735
|
+
base_url=api_url or "https://api.auroradns.eu",
|
|
736
|
+
headers={"User-Agent": f"dnctl/{__version__}"},
|
|
737
|
+
timeout=REQUEST_TIMEOUT,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def _headers(self, method: str, path: str) -> dict[str, str]:
|
|
741
|
+
timestamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
|
742
|
+
signature = base64.b64encode(
|
|
743
|
+
hmac.new(
|
|
744
|
+
self.secret_key.encode(),
|
|
745
|
+
f"{method}{path}{timestamp}".encode(),
|
|
746
|
+
hashlib.sha256,
|
|
747
|
+
).digest()
|
|
748
|
+
).decode()
|
|
749
|
+
token = base64.b64encode(f"{self.api_key}:{signature}".encode()).decode()
|
|
750
|
+
return {
|
|
751
|
+
"X-AuroraDNS-Date": timestamp,
|
|
752
|
+
"Authorization": f"AuroraDNSv1 {token}",
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
def _get(self, path: str) -> Any:
|
|
756
|
+
with working(f"AuroraDNS: GET {path}"):
|
|
757
|
+
response = self._http.get(path, headers=self._headers("GET", path))
|
|
758
|
+
if response.status_code >= 400:
|
|
759
|
+
raise CliError(
|
|
760
|
+
f"AuroraDNS returned {response.status_code} for {path}: "
|
|
761
|
+
f"{_provider_error(response)}"
|
|
762
|
+
)
|
|
763
|
+
return response.json()
|
|
764
|
+
|
|
765
|
+
def get_records(self, zone: str) -> list[TransferRecord]:
|
|
766
|
+
zones = self._get("/zones")
|
|
767
|
+
zone_id = next(
|
|
768
|
+
(
|
|
769
|
+
item["id"]
|
|
770
|
+
for item in zones
|
|
771
|
+
if str(item.get("name", "")).rstrip(".") == zone.rstrip(".")
|
|
772
|
+
),
|
|
773
|
+
None,
|
|
774
|
+
)
|
|
775
|
+
if zone_id is None:
|
|
776
|
+
raise CliError(f"zone '{zone}' not found in this AuroraDNS account")
|
|
777
|
+
records = []
|
|
778
|
+
for record in self._get(f"/zones/{zone_id}/records"):
|
|
779
|
+
rtype = str(record["type"]).upper()
|
|
780
|
+
content = str(record["content"])
|
|
781
|
+
prio = record.get("prio")
|
|
782
|
+
if rtype in ("MX", "SRV") and prio is not None:
|
|
783
|
+
content = f"{prio} {content}"
|
|
784
|
+
records.append(
|
|
785
|
+
TransferRecord(
|
|
786
|
+
name=str(record.get("name") or "@"),
|
|
787
|
+
type=rtype,
|
|
788
|
+
content=content,
|
|
789
|
+
ttl=int(record["ttl"]),
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
return records
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _provider_error(response: httpx.Response) -> str:
|
|
796
|
+
try:
|
|
797
|
+
body = response.json()
|
|
798
|
+
except ValueError:
|
|
799
|
+
return response.text[:200]
|
|
800
|
+
if isinstance(body, dict):
|
|
801
|
+
return str(body.get("error") or body.get("errormsg") or body)
|
|
802
|
+
return str(body)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _source_value(
|
|
806
|
+
stored: dict[str, str], key: str, env_var: str, label: str, *, hide: bool = False
|
|
807
|
+
) -> str:
|
|
808
|
+
value = stored.get(key) or os.environ.get(env_var)
|
|
809
|
+
if value:
|
|
810
|
+
return value
|
|
811
|
+
if not sys.stdin.isatty():
|
|
812
|
+
raise CliError(
|
|
813
|
+
f"missing source credential '{label}'; store it with "
|
|
814
|
+
f"`dnctl configure --provider <driver>` or set {env_var}"
|
|
815
|
+
)
|
|
816
|
+
return typer.prompt(label, hide_input=hide, err=True)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def build_source(driver: str, source_profile: str | None) -> DnsSource:
|
|
820
|
+
if driver not in PROVIDERS:
|
|
821
|
+
raise CliError(f"unknown driver '{driver}' (available: {', '.join(PROVIDERS)})")
|
|
822
|
+
parser = load_credentials()
|
|
823
|
+
section = f"{driver}:{source_profile or DEFAULT_PROFILE}"
|
|
824
|
+
stored = dict(parser[section]) if parser.has_section(section) else {}
|
|
825
|
+
if source_profile and not stored:
|
|
826
|
+
raise CliError(
|
|
827
|
+
f"profile '{section}' not found; run: "
|
|
828
|
+
f"dnctl configure --provider {driver} --profile {source_profile}"
|
|
829
|
+
)
|
|
830
|
+
api_url = stored.get("api_url") # testing escape hatch, like the main client
|
|
831
|
+
if driver == "transip":
|
|
832
|
+
login = _source_value(stored, "login", "DN_TRANSIP_LOGIN", "TransIP login name")
|
|
833
|
+
key_file = _source_value(
|
|
834
|
+
stored,
|
|
835
|
+
"private_key_file",
|
|
836
|
+
"DN_TRANSIP_PRIVATE_KEY_FILE",
|
|
837
|
+
"Path to TransIP private key (PEM)",
|
|
838
|
+
)
|
|
839
|
+
try:
|
|
840
|
+
pem = Path(key_file).expanduser().read_text()
|
|
841
|
+
except OSError as exc:
|
|
842
|
+
raise CliError(f"cannot read TransIP private key: {exc}") from exc
|
|
843
|
+
return TransipSource(login, pem, api_url)
|
|
844
|
+
api_key = _source_value(
|
|
845
|
+
stored, "api_key", "DN_AURORADNS_API_KEY", "AuroraDNS API key", hide=True
|
|
846
|
+
)
|
|
847
|
+
secret_key = _source_value(
|
|
848
|
+
stored,
|
|
849
|
+
"secret_key",
|
|
850
|
+
"DN_AURORADNS_SECRET_KEY",
|
|
851
|
+
"AuroraDNS secret key",
|
|
852
|
+
hide=True,
|
|
853
|
+
)
|
|
854
|
+
return AuroraDnsSource(api_key, secret_key, api_url)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
# ---------------------------------------------------------------------------
|
|
858
|
+
# CLI
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class PrefixGroup(TyperGroup):
|
|
862
|
+
"""Resolve unique command prefixes: `dnctl e l` runs `dnctl emails list`.
|
|
863
|
+
|
|
864
|
+
Exact names always win; an ambiguous prefix fails listing the matches."""
|
|
865
|
+
|
|
866
|
+
# typer vendors click, so the context/command annotations stay loose
|
|
867
|
+
def get_command(self, ctx: Any, cmd_name: str) -> Any:
|
|
868
|
+
command = super().get_command(ctx, cmd_name)
|
|
869
|
+
if command is not None:
|
|
870
|
+
return command
|
|
871
|
+
matches = [
|
|
872
|
+
name for name in self.list_commands(ctx) if name.startswith(cmd_name)
|
|
873
|
+
]
|
|
874
|
+
if len(matches) == 1:
|
|
875
|
+
return super().get_command(ctx, matches[0])
|
|
876
|
+
if matches:
|
|
877
|
+
ctx.fail(f"'{cmd_name}' is ambiguous: {', '.join(sorted(matches))}")
|
|
878
|
+
return None
|
|
879
|
+
|
|
880
|
+
def resolve_command(self, ctx: Any, args: Any) -> Any:
|
|
881
|
+
# report the resolved full command name, not the typed prefix
|
|
882
|
+
_, command, remaining = super().resolve_command(ctx, args)
|
|
883
|
+
return command.name if command else None, command, remaining
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
app = typer.Typer(
|
|
887
|
+
name="dnctl",
|
|
888
|
+
cls=PrefixGroup,
|
|
889
|
+
help="Manage your DevNomads services from the command line.",
|
|
890
|
+
no_args_is_help=True,
|
|
891
|
+
)
|
|
892
|
+
configure_app = typer.Typer(cls=PrefixGroup, help="Manage stored credential profiles.")
|
|
893
|
+
services_app = typer.Typer(
|
|
894
|
+
cls=PrefixGroup, help="Your DevNomads services.", no_args_is_help=True
|
|
895
|
+
)
|
|
896
|
+
dns_app = typer.Typer(
|
|
897
|
+
cls=PrefixGroup, help="DNS zones and records.", no_args_is_help=True
|
|
898
|
+
)
|
|
899
|
+
zones_app = typer.Typer(cls=PrefixGroup, help="DNS zones.", no_args_is_help=True)
|
|
900
|
+
records_app = typer.Typer(cls=PrefixGroup, help="DNS records.", no_args_is_help=True)
|
|
901
|
+
|
|
902
|
+
app.add_typer(configure_app, name="configure")
|
|
903
|
+
app.add_typer(services_app, name="services")
|
|
904
|
+
app.add_typer(dns_app, name="dns")
|
|
905
|
+
dns_app.add_typer(zones_app, name="zones")
|
|
906
|
+
dns_app.add_typer(records_app, name="records")
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
SortOption = Annotated[
|
|
910
|
+
str | None,
|
|
911
|
+
typer.Option(
|
|
912
|
+
"--sort",
|
|
913
|
+
help="Sort by field; prefix with - for descending (e.g. --sort -ttl).",
|
|
914
|
+
),
|
|
915
|
+
]
|
|
916
|
+
OutputOption = Annotated[
|
|
917
|
+
OutputFormat | None,
|
|
918
|
+
typer.Option("--output", "-o", help="Output format (table or json)."),
|
|
919
|
+
]
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _version_callback(value: bool) -> None:
|
|
923
|
+
if value:
|
|
924
|
+
sys.stdout.write(f"dnctl {__version__}\n")
|
|
925
|
+
raise typer.Exit()
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
@app.callback()
|
|
929
|
+
def main(
|
|
930
|
+
ctx: typer.Context,
|
|
931
|
+
profile: Annotated[
|
|
932
|
+
str | None,
|
|
933
|
+
typer.Option(
|
|
934
|
+
"--profile", "-p", help=f"Credentials profile (env: {ENV_PROFILE})."
|
|
935
|
+
),
|
|
936
|
+
] = None,
|
|
937
|
+
api_key: Annotated[
|
|
938
|
+
str | None,
|
|
939
|
+
typer.Option(
|
|
940
|
+
"--api-key", help=f"API key, beats any profile (env: {ENV_API_KEY})."
|
|
941
|
+
),
|
|
942
|
+
] = None,
|
|
943
|
+
output: Annotated[
|
|
944
|
+
OutputFormat | None,
|
|
945
|
+
typer.Option(
|
|
946
|
+
"--output",
|
|
947
|
+
"-o",
|
|
948
|
+
help="Output format; default: table on a TTY, json when piped.",
|
|
949
|
+
),
|
|
950
|
+
] = None,
|
|
951
|
+
debug: Annotated[
|
|
952
|
+
bool, typer.Option("--debug", help="Log the HTTP exchange to stderr.")
|
|
953
|
+
] = False,
|
|
954
|
+
version: Annotated[
|
|
955
|
+
bool,
|
|
956
|
+
typer.Option(
|
|
957
|
+
"--version",
|
|
958
|
+
callback=_version_callback,
|
|
959
|
+
is_eager=True,
|
|
960
|
+
help="Print version and exit.",
|
|
961
|
+
),
|
|
962
|
+
] = False,
|
|
963
|
+
) -> None:
|
|
964
|
+
ctx.obj = AppState(profile=profile, api_key=api_key, output=output, debug=debug)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
# --- configure -------------------------------------------------------------
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
@configure_app.callback(invoke_without_command=True)
|
|
971
|
+
def configure(
|
|
972
|
+
ctx: typer.Context,
|
|
973
|
+
profile: Annotated[
|
|
974
|
+
str, typer.Option(help="Profile name to create or update.")
|
|
975
|
+
] = DEFAULT_PROFILE,
|
|
976
|
+
provider: Annotated[
|
|
977
|
+
str | None,
|
|
978
|
+
typer.Option(
|
|
979
|
+
help="Configure a DNS transfer source profile "
|
|
980
|
+
f"({', '.join(PROVIDERS)}) instead of a DevNomads profile."
|
|
981
|
+
),
|
|
982
|
+
] = None,
|
|
983
|
+
) -> None:
|
|
984
|
+
"""Interactively store credentials in your home directory."""
|
|
985
|
+
|
|
986
|
+
if ctx.invoked_subcommand:
|
|
987
|
+
return
|
|
988
|
+
if provider is None:
|
|
989
|
+
section = profile
|
|
990
|
+
values = {
|
|
991
|
+
"api_key": typer.prompt("DevNomads API key", hide_input=True, err=True)
|
|
992
|
+
}
|
|
993
|
+
elif provider == "auroradns":
|
|
994
|
+
section = f"auroradns:{profile}"
|
|
995
|
+
values = {
|
|
996
|
+
"api_key": typer.prompt("AuroraDNS API key", hide_input=True, err=True),
|
|
997
|
+
"secret_key": typer.prompt(
|
|
998
|
+
"AuroraDNS secret key", hide_input=True, err=True
|
|
999
|
+
),
|
|
1000
|
+
}
|
|
1001
|
+
elif provider == "transip":
|
|
1002
|
+
section = f"transip:{profile}"
|
|
1003
|
+
login = typer.prompt("TransIP login name", err=True)
|
|
1004
|
+
key_source = typer.prompt("Path to TransIP private key (PEM)", err=True)
|
|
1005
|
+
key_path = _import_private_key(Path(key_source).expanduser(), profile)
|
|
1006
|
+
values = {"login": login, "private_key_file": str(key_path)}
|
|
1007
|
+
else:
|
|
1008
|
+
raise CliError(
|
|
1009
|
+
f"unknown provider '{provider}' (available: {', '.join(PROVIDERS)})"
|
|
1010
|
+
)
|
|
1011
|
+
parser = load_credentials()
|
|
1012
|
+
if not parser.has_section(section):
|
|
1013
|
+
parser.add_section(section)
|
|
1014
|
+
for key, value in values.items():
|
|
1015
|
+
parser.set(section, key, value)
|
|
1016
|
+
save_credentials(parser)
|
|
1017
|
+
err_console.print(f"profile '{section}' written to {credentials_path()}")
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _import_private_key(source: Path, profile: str) -> Path:
|
|
1021
|
+
try:
|
|
1022
|
+
pem = source.read_text()
|
|
1023
|
+
except OSError as exc:
|
|
1024
|
+
raise CliError(f"cannot read private key: {exc}") from exc
|
|
1025
|
+
if "PRIVATE KEY" not in pem:
|
|
1026
|
+
raise CliError(f"{source} does not look like a PEM private key")
|
|
1027
|
+
target = config_dir() / f"transip-{profile}.pem"
|
|
1028
|
+
_write_private(target, pem)
|
|
1029
|
+
return target
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
@configure_app.command("list")
|
|
1033
|
+
def configure_list(
|
|
1034
|
+
ctx: typer.Context, sort: SortOption = None, output: OutputOption = None
|
|
1035
|
+
) -> None:
|
|
1036
|
+
"""List stored profiles (secrets masked)."""
|
|
1037
|
+
|
|
1038
|
+
parser = load_credentials()
|
|
1039
|
+
rows = []
|
|
1040
|
+
for section in parser.sections():
|
|
1041
|
+
provider, _, name = section.partition(":")
|
|
1042
|
+
values = parser[section]
|
|
1043
|
+
secret = values.get("api_key") or values.get("secret_key") or ""
|
|
1044
|
+
rows.append(
|
|
1045
|
+
{
|
|
1046
|
+
"profile": section,
|
|
1047
|
+
"type": provider if name else "devnomads",
|
|
1048
|
+
"api_key": _mask(secret) or values.get("login", ""),
|
|
1049
|
+
}
|
|
1050
|
+
)
|
|
1051
|
+
render(
|
|
1052
|
+
state_from(ctx, output),
|
|
1053
|
+
sort_rows(rows, sort),
|
|
1054
|
+
columns=["profile", "type", "api_key"],
|
|
1055
|
+
title="Profiles",
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
# --- services ----------------------------------------------------------------
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
@services_app.command("list")
|
|
1063
|
+
def services_list(
|
|
1064
|
+
ctx: typer.Context, sort: SortOption = None, output: OutputOption = None
|
|
1065
|
+
) -> None:
|
|
1066
|
+
"""List all your services."""
|
|
1067
|
+
|
|
1068
|
+
state = state_from(ctx, output)
|
|
1069
|
+
data = get_client(state).request("GET", "/services")
|
|
1070
|
+
if isinstance(data, list):
|
|
1071
|
+
data = sort_rows(data, sort)
|
|
1072
|
+
render(
|
|
1073
|
+
state,
|
|
1074
|
+
data,
|
|
1075
|
+
columns=["service_id", "type", "entity", "started_at", "ended_at"],
|
|
1076
|
+
title="Services",
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
@services_app.command("show")
|
|
1081
|
+
def services_show(
|
|
1082
|
+
ctx: typer.Context,
|
|
1083
|
+
service_id: Annotated[int, typer.Argument(help="Service ID.")],
|
|
1084
|
+
output: OutputOption = None,
|
|
1085
|
+
) -> None:
|
|
1086
|
+
"""Show one service."""
|
|
1087
|
+
|
|
1088
|
+
state = state_from(ctx, output)
|
|
1089
|
+
data = get_client(state).request("GET", f"/services/{service_id}")
|
|
1090
|
+
render(state, data, title=f"Service {service_id}")
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
# --- dns zones ---------------------------------------------------------------
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
@zones_app.command("list")
|
|
1097
|
+
def zones_list(
|
|
1098
|
+
ctx: typer.Context, sort: SortOption = None, output: OutputOption = None
|
|
1099
|
+
) -> None:
|
|
1100
|
+
"""List your DNS zones."""
|
|
1101
|
+
|
|
1102
|
+
state = state_from(ctx, output)
|
|
1103
|
+
data = get_client(state).request("GET", "/services/dns/zones")
|
|
1104
|
+
if isinstance(data, list):
|
|
1105
|
+
data = sort_rows(data, sort)
|
|
1106
|
+
render(
|
|
1107
|
+
state,
|
|
1108
|
+
data,
|
|
1109
|
+
columns=["name", "kind", "serial", "dnssec"],
|
|
1110
|
+
title="DNS zones",
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
@zones_app.command("show")
|
|
1115
|
+
def zones_show(
|
|
1116
|
+
ctx: typer.Context,
|
|
1117
|
+
zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
|
|
1118
|
+
output: OutputOption = None,
|
|
1119
|
+
) -> None:
|
|
1120
|
+
"""Show a DNS zone including its records."""
|
|
1121
|
+
|
|
1122
|
+
state = state_from(ctx, output)
|
|
1123
|
+
data = get_client(state).request("GET", f"/services/dns/zones/{zone_id(zone)}")
|
|
1124
|
+
if resolve_format(state) is OutputFormat.json or not isinstance(data, dict):
|
|
1125
|
+
render(state, data)
|
|
1126
|
+
return
|
|
1127
|
+
meta = {key: value for key, value in data.items() if key != "rrsets"}
|
|
1128
|
+
render(state, meta, title=zone)
|
|
1129
|
+
render(
|
|
1130
|
+
state,
|
|
1131
|
+
flatten_rrsets(data.get("rrsets", [])),
|
|
1132
|
+
columns=RECORD_COLUMNS,
|
|
1133
|
+
title="Records",
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
# --- dns records -------------------------------------------------------------
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
@records_app.command("list")
|
|
1141
|
+
def records_list(
|
|
1142
|
+
ctx: typer.Context,
|
|
1143
|
+
zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
|
|
1144
|
+
sort: SortOption = None,
|
|
1145
|
+
output: OutputOption = None,
|
|
1146
|
+
) -> None:
|
|
1147
|
+
"""List the records in a zone, one row per value."""
|
|
1148
|
+
|
|
1149
|
+
state = state_from(ctx, output)
|
|
1150
|
+
data = get_client(state).request("GET", f"/services/dns/zones/{zone_id(zone)}")
|
|
1151
|
+
rrsets = data.get("rrsets", []) if isinstance(data, dict) else []
|
|
1152
|
+
render(
|
|
1153
|
+
state,
|
|
1154
|
+
sort_rows(flatten_rrsets(rrsets), sort),
|
|
1155
|
+
columns=RECORD_COLUMNS,
|
|
1156
|
+
title=f"Records in {zone}",
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
@records_app.command("set")
|
|
1161
|
+
def records_set(
|
|
1162
|
+
ctx: typer.Context,
|
|
1163
|
+
zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
|
|
1164
|
+
name: Annotated[
|
|
1165
|
+
str, typer.Argument(help="Record name relative to the zone; @ for the apex.")
|
|
1166
|
+
],
|
|
1167
|
+
rtype: Annotated[
|
|
1168
|
+
str, typer.Argument(metavar="TYPE", help="Record type, e.g. A, MX, TXT.")
|
|
1169
|
+
],
|
|
1170
|
+
content: Annotated[
|
|
1171
|
+
list[str],
|
|
1172
|
+
typer.Argument(
|
|
1173
|
+
help="One or more record values; replaces the whole record set."
|
|
1174
|
+
),
|
|
1175
|
+
],
|
|
1176
|
+
ttl: Annotated[int, typer.Option(help="TTL in seconds.")] = 3600,
|
|
1177
|
+
) -> None:
|
|
1178
|
+
"""Create or replace a record set (PowerDNS REPLACE semantics)."""
|
|
1179
|
+
|
|
1180
|
+
state = state_from(ctx)
|
|
1181
|
+
rrset = build_rrset(
|
|
1182
|
+
zone, name, rtype, changetype="REPLACE", ttl=ttl, contents=content
|
|
1183
|
+
)
|
|
1184
|
+
payload = {"rrsets": [rrset]}
|
|
1185
|
+
get_client(state).request(
|
|
1186
|
+
"PATCH", f"/services/dns/zones/{zone_id(zone)}", json_body=payload
|
|
1187
|
+
)
|
|
1188
|
+
err_console.print(
|
|
1189
|
+
f"set {rrset['name']} {rrset['type']} "
|
|
1190
|
+
f"({len(rrset['records'])} value(s), ttl {ttl})"
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
@records_app.command("delete")
|
|
1195
|
+
def records_delete(
|
|
1196
|
+
ctx: typer.Context,
|
|
1197
|
+
zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
|
|
1198
|
+
name: Annotated[
|
|
1199
|
+
str, typer.Argument(help="Record name relative to the zone; @ for the apex.")
|
|
1200
|
+
],
|
|
1201
|
+
rtype: Annotated[
|
|
1202
|
+
str, typer.Argument(metavar="TYPE", help="Record type, e.g. A, MX, TXT.")
|
|
1203
|
+
],
|
|
1204
|
+
yes: Annotated[
|
|
1205
|
+
bool, typer.Option("--yes", "-y", help="Do not ask for confirmation.")
|
|
1206
|
+
] = False,
|
|
1207
|
+
) -> None:
|
|
1208
|
+
"""Delete a whole record set (all values for name + type)."""
|
|
1209
|
+
|
|
1210
|
+
state = state_from(ctx)
|
|
1211
|
+
rrset = build_rrset(zone, name, rtype, changetype="DELETE")
|
|
1212
|
+
_confirm(f"Delete all {rrset['type']} records for {rrset['name']}?", yes)
|
|
1213
|
+
payload = {"rrsets": [rrset]}
|
|
1214
|
+
get_client(state).request(
|
|
1215
|
+
"PATCH", f"/services/dns/zones/{zone_id(zone)}", json_body=payload
|
|
1216
|
+
)
|
|
1217
|
+
err_console.print(f"deleted {rrset['name']} {rrset['type']}")
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
@dns_app.command("transfer")
|
|
1221
|
+
def dns_transfer(
|
|
1222
|
+
ctx: typer.Context,
|
|
1223
|
+
from_: Annotated[
|
|
1224
|
+
str,
|
|
1225
|
+
typer.Option(
|
|
1226
|
+
"--from",
|
|
1227
|
+
metavar="DRIVER",
|
|
1228
|
+
help=f"Source provider ({', '.join(PROVIDERS)}).",
|
|
1229
|
+
),
|
|
1230
|
+
],
|
|
1231
|
+
zone: Annotated[
|
|
1232
|
+
str, typer.Option("--zone", help="Zone name on both sides, e.g. example.com.")
|
|
1233
|
+
],
|
|
1234
|
+
source_profile: Annotated[
|
|
1235
|
+
str | None,
|
|
1236
|
+
typer.Option(
|
|
1237
|
+
"--source-profile",
|
|
1238
|
+
help="Stored provider profile to read credentials from "
|
|
1239
|
+
"(default: the provider's 'default' profile).",
|
|
1240
|
+
),
|
|
1241
|
+
] = None,
|
|
1242
|
+
dry_run: Annotated[
|
|
1243
|
+
bool, typer.Option("--dry-run", help="Show the changes without applying them.")
|
|
1244
|
+
] = False,
|
|
1245
|
+
yes: Annotated[
|
|
1246
|
+
bool, typer.Option("--yes", "-y", help="Do not ask for confirmation.")
|
|
1247
|
+
] = False,
|
|
1248
|
+
output: OutputOption = None,
|
|
1249
|
+
) -> None:
|
|
1250
|
+
"""Copy a DNS zone from another provider into DevNomads.
|
|
1251
|
+
|
|
1252
|
+
Fetches the records at the source (read-only), diffs them against the
|
|
1253
|
+
DevNomads zone, shows the changes, and applies them after confirmation.
|
|
1254
|
+
SOA and apex NS records stay untouched - the platform owns those.
|
|
1255
|
+
"""
|
|
1256
|
+
|
|
1257
|
+
state = state_from(ctx, output)
|
|
1258
|
+
client = get_client(state) # resolve DevNomads credentials first: fail fast
|
|
1259
|
+
source = build_source(from_, source_profile)
|
|
1260
|
+
zones = client.request("GET", "/services/dns/zones")
|
|
1261
|
+
known = {str(item.get("name", "")).rstrip(".") for item in zones or []}
|
|
1262
|
+
if zone.rstrip(".") not in known:
|
|
1263
|
+
raise CliError(
|
|
1264
|
+
f"zone '{zone}' does not exist at DevNomads; register or transfer "
|
|
1265
|
+
"the domain first (dnctl dns zones list shows your zones)"
|
|
1266
|
+
)
|
|
1267
|
+
err_console.print(f"fetching {zone} from {source.name}")
|
|
1268
|
+
records = source.get_records(zone)
|
|
1269
|
+
data = client.request("GET", f"/services/dns/zones/{zone_id(zone)}")
|
|
1270
|
+
rrsets = data.get("rrsets", []) if isinstance(data, dict) else []
|
|
1271
|
+
changes, patch = diff_rrsets(
|
|
1272
|
+
current_rrsets(rrsets, zone), desired_rrsets(records, zone)
|
|
1273
|
+
)
|
|
1274
|
+
if not changes:
|
|
1275
|
+
err_console.print(f"{zone} is already in sync with {source.name}")
|
|
1276
|
+
return
|
|
1277
|
+
render(
|
|
1278
|
+
state,
|
|
1279
|
+
changes,
|
|
1280
|
+
columns=["action", "name", "type", "ttl", "content"],
|
|
1281
|
+
title=f"Transfer {zone} from {source.name}",
|
|
1282
|
+
)
|
|
1283
|
+
if dry_run:
|
|
1284
|
+
err_console.print(f"dry run: {len(changes)} change(s) not applied")
|
|
1285
|
+
return
|
|
1286
|
+
_confirm(f"Apply {len(changes)} change(s) to {zone}", yes)
|
|
1287
|
+
client.request(
|
|
1288
|
+
"PATCH", f"/services/dns/zones/{zone_id(zone)}", json_body={"rrsets": patch}
|
|
1289
|
+
)
|
|
1290
|
+
err_console.print(f"applied {len(changes)} change(s) to {zone}")
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
# ---------------------------------------------------------------------------
|
|
1294
|
+
# Generated commands. tools/generate.py renders one thin command per
|
|
1295
|
+
# OpenAPI operation between the markers below, from openapi.json plus
|
|
1296
|
+
# the curation overlay in overlay.json. Do not edit the region by hand.
|
|
1297
|
+
|
|
1298
|
+
_SORT_OPT = typer.Option(
|
|
1299
|
+
None,
|
|
1300
|
+
"--sort",
|
|
1301
|
+
help="Sort by field; prefix with - for descending (e.g. --sort -ttl).",
|
|
1302
|
+
)
|
|
1303
|
+
_YES_OPT = typer.Option(False, "--yes", "-y", help="Do not ask for confirmation.")
|
|
1304
|
+
_OUTPUT_OPT = typer.Option(
|
|
1305
|
+
None, "--output", "-o", help="Output format (table or json)."
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def _generated_call(
|
|
1310
|
+
ctx: typer.Context,
|
|
1311
|
+
method: str,
|
|
1312
|
+
path_template: str,
|
|
1313
|
+
path_params: dict[str, Any],
|
|
1314
|
+
*,
|
|
1315
|
+
body: dict[str, Any] | None = None,
|
|
1316
|
+
sort: str | None = None,
|
|
1317
|
+
columns: list[str] | None = None,
|
|
1318
|
+
confirm: str | None = None,
|
|
1319
|
+
yes: bool = False,
|
|
1320
|
+
output: OutputFormat | None = None,
|
|
1321
|
+
) -> None:
|
|
1322
|
+
"""Shared runtime for generated commands: build the path, confirm if
|
|
1323
|
+
needed, call the API, render the result."""
|
|
1324
|
+
|
|
1325
|
+
state = state_from(ctx, output)
|
|
1326
|
+
if confirm:
|
|
1327
|
+
detail = ", ".join(f"{key}={value}" for key, value in path_params.items())
|
|
1328
|
+
_confirm(f"{confirm} ({detail})?" if detail else f"{confirm}?", yes)
|
|
1329
|
+
quoted = {
|
|
1330
|
+
key: urllib.parse.quote(str(value), safe="")
|
|
1331
|
+
for key, value in path_params.items()
|
|
1332
|
+
}
|
|
1333
|
+
path = path_template.format(**quoted)
|
|
1334
|
+
payload = {key: value for key, value in (body or {}).items() if value is not None}
|
|
1335
|
+
data = get_client(state).request(method, path, json_body=payload or None)
|
|
1336
|
+
if data is None:
|
|
1337
|
+
err_console.print("ok")
|
|
1338
|
+
return
|
|
1339
|
+
if isinstance(data, list):
|
|
1340
|
+
data = sort_rows(data, sort)
|
|
1341
|
+
render(state, data, columns=columns, title=path)
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
# --- BEGIN GENERATED COMMANDS (tools/generate.py; do not edit by hand) ---
|
|
1345
|
+
|
|
1346
|
+
gen_apps = typer.Typer(cls=PrefixGroup, help="Manage apps.", no_args_is_help=True)
|
|
1347
|
+
app.add_typer(gen_apps, name="apps")
|
|
1348
|
+
gen_buckets = typer.Typer(cls=PrefixGroup, help="Manage buckets.", no_args_is_help=True)
|
|
1349
|
+
app.add_typer(gen_buckets, name="buckets")
|
|
1350
|
+
gen_containers = typer.Typer(
|
|
1351
|
+
cls=PrefixGroup, help="Manage containers.", no_args_is_help=True
|
|
1352
|
+
)
|
|
1353
|
+
app.add_typer(gen_containers, name="containers")
|
|
1354
|
+
gen_databases = typer.Typer(
|
|
1355
|
+
cls=PrefixGroup, help="Manage databases.", no_args_is_help=True
|
|
1356
|
+
)
|
|
1357
|
+
app.add_typer(gen_databases, name="databases")
|
|
1358
|
+
gen_domains = typer.Typer(cls=PrefixGroup, help="Manage domains.", no_args_is_help=True)
|
|
1359
|
+
app.add_typer(gen_domains, name="domains")
|
|
1360
|
+
gen_emails = typer.Typer(cls=PrefixGroup, help="Manage emails.", no_args_is_help=True)
|
|
1361
|
+
app.add_typer(gen_emails, name="emails")
|
|
1362
|
+
gen_forwards = typer.Typer(
|
|
1363
|
+
cls=PrefixGroup, help="Manage forwards.", no_args_is_help=True
|
|
1364
|
+
)
|
|
1365
|
+
app.add_typer(gen_forwards, name="forwards")
|
|
1366
|
+
gen_handles = typer.Typer(cls=PrefixGroup, help="Manage handles.", no_args_is_help=True)
|
|
1367
|
+
app.add_typer(gen_handles, name="handles")
|
|
1368
|
+
gen_proxies = typer.Typer(cls=PrefixGroup, help="Manage proxies.", no_args_is_help=True)
|
|
1369
|
+
app.add_typer(gen_proxies, name="proxies")
|
|
1370
|
+
gen_searches = typer.Typer(
|
|
1371
|
+
cls=PrefixGroup, help="Manage searches.", no_args_is_help=True
|
|
1372
|
+
)
|
|
1373
|
+
app.add_typer(gen_searches, name="searches")
|
|
1374
|
+
gen_servers = typer.Typer(cls=PrefixGroup, help="Manage servers.", no_args_is_help=True)
|
|
1375
|
+
app.add_typer(gen_servers, name="servers")
|
|
1376
|
+
gen_sites = typer.Typer(cls=PrefixGroup, help="Manage sites.", no_args_is_help=True)
|
|
1377
|
+
app.add_typer(gen_sites, name="sites")
|
|
1378
|
+
gen_spams = typer.Typer(cls=PrefixGroup, help="Manage spams.", no_args_is_help=True)
|
|
1379
|
+
app.add_typer(gen_spams, name="spams")
|
|
1380
|
+
gen_containers_instances = typer.Typer(
|
|
1381
|
+
cls=PrefixGroup, help="Manage containers instances.", no_args_is_help=True
|
|
1382
|
+
)
|
|
1383
|
+
gen_containers.add_typer(gen_containers_instances, name="instances")
|
|
1384
|
+
gen_databases_clusters = typer.Typer(
|
|
1385
|
+
cls=PrefixGroup, help="Manage databases clusters.", no_args_is_help=True
|
|
1386
|
+
)
|
|
1387
|
+
gen_databases.add_typer(gen_databases_clusters, name="clusters")
|
|
1388
|
+
gen_databases_permissions = typer.Typer(
|
|
1389
|
+
cls=PrefixGroup, help="Manage databases permissions.", no_args_is_help=True
|
|
1390
|
+
)
|
|
1391
|
+
gen_databases.add_typer(gen_databases_permissions, name="permissions")
|
|
1392
|
+
gen_databases_users = typer.Typer(
|
|
1393
|
+
cls=PrefixGroup, help="Manage databases users.", no_args_is_help=True
|
|
1394
|
+
)
|
|
1395
|
+
gen_databases.add_typer(gen_databases_users, name="users")
|
|
1396
|
+
gen_emails_aliases = typer.Typer(
|
|
1397
|
+
cls=PrefixGroup, help="Manage emails aliases.", no_args_is_help=True
|
|
1398
|
+
)
|
|
1399
|
+
gen_emails.add_typer(gen_emails_aliases, name="aliases")
|
|
1400
|
+
gen_emails_dkim = typer.Typer(
|
|
1401
|
+
cls=PrefixGroup, help="Manage emails dkim.", no_args_is_help=True
|
|
1402
|
+
)
|
|
1403
|
+
gen_emails.add_typer(gen_emails_dkim, name="dkim")
|
|
1404
|
+
gen_emails_forwardings = typer.Typer(
|
|
1405
|
+
cls=PrefixGroup, help="Manage emails forwardings.", no_args_is_help=True
|
|
1406
|
+
)
|
|
1407
|
+
gen_emails.add_typer(gen_emails_forwardings, name="forwardings")
|
|
1408
|
+
gen_emails_mailboxes = typer.Typer(
|
|
1409
|
+
cls=PrefixGroup, help="Manage emails mailboxes.", no_args_is_help=True
|
|
1410
|
+
)
|
|
1411
|
+
gen_emails.add_typer(gen_emails_mailboxes, name="mailboxes")
|
|
1412
|
+
gen_emails_records = typer.Typer(
|
|
1413
|
+
cls=PrefixGroup, help="Manage emails records.", no_args_is_help=True
|
|
1414
|
+
)
|
|
1415
|
+
gen_emails.add_typer(gen_emails_records, name="records")
|
|
1416
|
+
gen_emails_transactional = typer.Typer(
|
|
1417
|
+
cls=PrefixGroup, help="Manage emails transactional.", no_args_is_help=True
|
|
1418
|
+
)
|
|
1419
|
+
gen_emails.add_typer(gen_emails_transactional, name="transactional")
|
|
1420
|
+
gen_spams_clusters = typer.Typer(
|
|
1421
|
+
cls=PrefixGroup, help="Manage spams clusters.", no_args_is_help=True
|
|
1422
|
+
)
|
|
1423
|
+
gen_spams.add_typer(gen_spams_clusters, name="clusters")
|
|
1424
|
+
gen_spams_domain = typer.Typer(
|
|
1425
|
+
cls=PrefixGroup, help="Manage spams domain.", no_args_is_help=True
|
|
1426
|
+
)
|
|
1427
|
+
gen_spams.add_typer(gen_spams_domain, name="domain")
|
|
1428
|
+
gen_spams_transports = typer.Typer(
|
|
1429
|
+
cls=PrefixGroup, help="Manage spams transports.", no_args_is_help=True
|
|
1430
|
+
)
|
|
1431
|
+
gen_spams.add_typer(gen_spams_transports, name="transports")
|
|
1432
|
+
gen_containers_instances_volumes = typer.Typer(
|
|
1433
|
+
cls=PrefixGroup, help="Manage containers instances volumes.", no_args_is_help=True
|
|
1434
|
+
)
|
|
1435
|
+
gen_containers_instances.add_typer(gen_containers_instances_volumes, name="volumes")
|
|
1436
|
+
gen_databases_clusters_users = typer.Typer(
|
|
1437
|
+
cls=PrefixGroup, help="Manage databases clusters users.", no_args_is_help=True
|
|
1438
|
+
)
|
|
1439
|
+
gen_databases_clusters.add_typer(gen_databases_clusters_users, name="users")
|
|
1440
|
+
gen_emails_transactional_aliases = typer.Typer(
|
|
1441
|
+
cls=PrefixGroup, help="Manage emails transactional aliases.", no_args_is_help=True
|
|
1442
|
+
)
|
|
1443
|
+
gen_emails_transactional.add_typer(gen_emails_transactional_aliases, name="aliases")
|
|
1444
|
+
gen_emails_transactional_keys = typer.Typer(
|
|
1445
|
+
cls=PrefixGroup, help="Manage emails transactional keys.", no_args_is_help=True
|
|
1446
|
+
)
|
|
1447
|
+
gen_emails_transactional.add_typer(gen_emails_transactional_keys, name="keys")
|
|
1448
|
+
gen_emails_transactional_users = typer.Typer(
|
|
1449
|
+
cls=PrefixGroup, help="Manage emails transactional users.", no_args_is_help=True
|
|
1450
|
+
)
|
|
1451
|
+
gen_emails_transactional.add_typer(gen_emails_transactional_users, name="users")
|
|
1452
|
+
gen_spams_domain_dkim = typer.Typer(
|
|
1453
|
+
cls=PrefixGroup, help="Manage spams domain dkim.", no_args_is_help=True
|
|
1454
|
+
)
|
|
1455
|
+
gen_spams_domain.add_typer(gen_spams_domain_dkim, name="dkim")
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
@gen_handles.command("create")
|
|
1459
|
+
def gen_handles_create(
|
|
1460
|
+
ctx: typer.Context,
|
|
1461
|
+
client_id: int | None = typer.Option(None),
|
|
1462
|
+
firstname: str | None = typer.Option(None),
|
|
1463
|
+
prefix: str | None = typer.Option(None),
|
|
1464
|
+
lastname: str | None = typer.Option(None),
|
|
1465
|
+
company: str | None = typer.Option(None),
|
|
1466
|
+
vat_id: str | None = typer.Option(None),
|
|
1467
|
+
email: str | None = typer.Option(None),
|
|
1468
|
+
phone_country_code: str | None = typer.Option(None),
|
|
1469
|
+
phone_area_code: str | None = typer.Option(None),
|
|
1470
|
+
phone_number: str | None = typer.Option(None),
|
|
1471
|
+
street: str | None = typer.Option(None),
|
|
1472
|
+
number: str | None = typer.Option(None),
|
|
1473
|
+
zipcode: str | None = typer.Option(None),
|
|
1474
|
+
city: str | None = typer.Option(None),
|
|
1475
|
+
region: str | None = typer.Option(None),
|
|
1476
|
+
country: str | None = typer.Option(None),
|
|
1477
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1478
|
+
) -> None:
|
|
1479
|
+
"""Create a contact handle."""
|
|
1480
|
+
|
|
1481
|
+
_generated_call(
|
|
1482
|
+
ctx,
|
|
1483
|
+
"POST",
|
|
1484
|
+
"/handles",
|
|
1485
|
+
{},
|
|
1486
|
+
body={
|
|
1487
|
+
"client_id": client_id,
|
|
1488
|
+
"firstname": firstname,
|
|
1489
|
+
"prefix": prefix,
|
|
1490
|
+
"lastname": lastname,
|
|
1491
|
+
"company": company,
|
|
1492
|
+
"vat_id": vat_id,
|
|
1493
|
+
"email": email,
|
|
1494
|
+
"phone_country_code": phone_country_code,
|
|
1495
|
+
"phone_area_code": phone_area_code,
|
|
1496
|
+
"phone_number": phone_number,
|
|
1497
|
+
"street": street,
|
|
1498
|
+
"number": number,
|
|
1499
|
+
"zipcode": zipcode,
|
|
1500
|
+
"city": city,
|
|
1501
|
+
"region": region,
|
|
1502
|
+
"country": country,
|
|
1503
|
+
},
|
|
1504
|
+
output=output,
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
@gen_handles.command("list")
|
|
1509
|
+
def gen_handles_index(
|
|
1510
|
+
ctx: typer.Context,
|
|
1511
|
+
sort: str | None = _SORT_OPT,
|
|
1512
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1513
|
+
) -> None:
|
|
1514
|
+
"""List your contact handles."""
|
|
1515
|
+
|
|
1516
|
+
_generated_call(
|
|
1517
|
+
ctx,
|
|
1518
|
+
"GET",
|
|
1519
|
+
"/handles",
|
|
1520
|
+
{},
|
|
1521
|
+
sort=sort,
|
|
1522
|
+
output=output,
|
|
1523
|
+
)
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
@gen_handles.command("show")
|
|
1527
|
+
def gen_handles_show(
|
|
1528
|
+
ctx: typer.Context,
|
|
1529
|
+
handle_id: int = typer.Argument(..., metavar="HANDLE_ID"),
|
|
1530
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1531
|
+
) -> None:
|
|
1532
|
+
"""Show one contact handle."""
|
|
1533
|
+
|
|
1534
|
+
_generated_call(
|
|
1535
|
+
ctx,
|
|
1536
|
+
"GET",
|
|
1537
|
+
"/handles/{handleId}",
|
|
1538
|
+
{"handleId": handle_id},
|
|
1539
|
+
output=output,
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
@gen_apps.command("list")
|
|
1544
|
+
def gen_services_apps_index(
|
|
1545
|
+
ctx: typer.Context,
|
|
1546
|
+
sort: str | None = _SORT_OPT,
|
|
1547
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1548
|
+
) -> None:
|
|
1549
|
+
"""List apps."""
|
|
1550
|
+
|
|
1551
|
+
_generated_call(
|
|
1552
|
+
ctx,
|
|
1553
|
+
"GET",
|
|
1554
|
+
"/services/apps",
|
|
1555
|
+
{},
|
|
1556
|
+
sort=sort,
|
|
1557
|
+
output=output,
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
@gen_apps.command("show")
|
|
1562
|
+
def gen_services_apps_show(
|
|
1563
|
+
ctx: typer.Context,
|
|
1564
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1565
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1566
|
+
) -> None:
|
|
1567
|
+
"""Show apps details."""
|
|
1568
|
+
|
|
1569
|
+
_generated_call(
|
|
1570
|
+
ctx,
|
|
1571
|
+
"GET",
|
|
1572
|
+
"/services/apps/{serviceId}",
|
|
1573
|
+
{"serviceId": service_id},
|
|
1574
|
+
output=output,
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
@gen_apps.command("state")
|
|
1579
|
+
def gen_services_apps_state(
|
|
1580
|
+
ctx: typer.Context,
|
|
1581
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1582
|
+
state: str = typer.Argument(..., metavar="STATE"),
|
|
1583
|
+
yes: bool = _YES_OPT,
|
|
1584
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1585
|
+
) -> None:
|
|
1586
|
+
"""Change the state of an app service."""
|
|
1587
|
+
|
|
1588
|
+
_generated_call(
|
|
1589
|
+
ctx,
|
|
1590
|
+
"GET",
|
|
1591
|
+
"/services/apps/{serviceId}/state/{state}",
|
|
1592
|
+
{"serviceId": service_id, "state": state},
|
|
1593
|
+
confirm="State apps",
|
|
1594
|
+
yes=yes,
|
|
1595
|
+
output=output,
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
@gen_buckets.command("list")
|
|
1600
|
+
def gen_services_buckets_index(
|
|
1601
|
+
ctx: typer.Context,
|
|
1602
|
+
sort: str | None = _SORT_OPT,
|
|
1603
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1604
|
+
) -> None:
|
|
1605
|
+
"""List buckets."""
|
|
1606
|
+
|
|
1607
|
+
_generated_call(
|
|
1608
|
+
ctx,
|
|
1609
|
+
"GET",
|
|
1610
|
+
"/services/buckets",
|
|
1611
|
+
{},
|
|
1612
|
+
sort=sort,
|
|
1613
|
+
output=output,
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
@gen_buckets.command("show")
|
|
1618
|
+
def gen_services_buckets_show(
|
|
1619
|
+
ctx: typer.Context,
|
|
1620
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1621
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1622
|
+
) -> None:
|
|
1623
|
+
"""Show buckets details."""
|
|
1624
|
+
|
|
1625
|
+
_generated_call(
|
|
1626
|
+
ctx,
|
|
1627
|
+
"GET",
|
|
1628
|
+
"/services/buckets/{serviceId}",
|
|
1629
|
+
{"serviceId": service_id},
|
|
1630
|
+
output=output,
|
|
1631
|
+
)
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
@gen_containers.command("deploy")
|
|
1635
|
+
def gen_services_containers_deploy(
|
|
1636
|
+
ctx: typer.Context,
|
|
1637
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1638
|
+
yes: bool = _YES_OPT,
|
|
1639
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1640
|
+
) -> None:
|
|
1641
|
+
"""Deploy a container service."""
|
|
1642
|
+
|
|
1643
|
+
_generated_call(
|
|
1644
|
+
ctx,
|
|
1645
|
+
"GET",
|
|
1646
|
+
"/services/containers/{serviceId}/deploy",
|
|
1647
|
+
{"serviceId": service_id},
|
|
1648
|
+
confirm="Deploy containers",
|
|
1649
|
+
yes=yes,
|
|
1650
|
+
output=output,
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
@gen_containers.command("list")
|
|
1655
|
+
def gen_services_containers_index(
|
|
1656
|
+
ctx: typer.Context,
|
|
1657
|
+
sort: str | None = _SORT_OPT,
|
|
1658
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1659
|
+
) -> None:
|
|
1660
|
+
"""List containers."""
|
|
1661
|
+
|
|
1662
|
+
_generated_call(
|
|
1663
|
+
ctx,
|
|
1664
|
+
"GET",
|
|
1665
|
+
"/services/containers",
|
|
1666
|
+
{},
|
|
1667
|
+
sort=sort,
|
|
1668
|
+
output=output,
|
|
1669
|
+
)
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
@gen_containers_instances.command("deploy")
|
|
1673
|
+
def gen_services_containers_instances_deploy(
|
|
1674
|
+
ctx: typer.Context,
|
|
1675
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1676
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1677
|
+
yes: bool = _YES_OPT,
|
|
1678
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1679
|
+
) -> None:
|
|
1680
|
+
"""Deploy a container instance."""
|
|
1681
|
+
|
|
1682
|
+
_generated_call(
|
|
1683
|
+
ctx,
|
|
1684
|
+
"GET",
|
|
1685
|
+
"/services/containers/{serviceId}/instances/{instanceId}/deploy",
|
|
1686
|
+
{"serviceId": service_id, "instanceId": instance_id},
|
|
1687
|
+
confirm="Deploy containers instances",
|
|
1688
|
+
yes=yes,
|
|
1689
|
+
output=output,
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
@gen_containers_instances.command("list")
|
|
1694
|
+
def gen_services_containers_instances_index(
|
|
1695
|
+
ctx: typer.Context,
|
|
1696
|
+
service_id: str = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1697
|
+
sort: str | None = _SORT_OPT,
|
|
1698
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1699
|
+
) -> None:
|
|
1700
|
+
"""List containers instances."""
|
|
1701
|
+
|
|
1702
|
+
_generated_call(
|
|
1703
|
+
ctx,
|
|
1704
|
+
"GET",
|
|
1705
|
+
"/services/containers/{serviceId}/instances",
|
|
1706
|
+
{"serviceId": service_id},
|
|
1707
|
+
sort=sort,
|
|
1708
|
+
output=output,
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
@gen_containers_instances.command("logs")
|
|
1713
|
+
def gen_services_containers_instances_logs(
|
|
1714
|
+
ctx: typer.Context,
|
|
1715
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1716
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1717
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1718
|
+
) -> None:
|
|
1719
|
+
"""Show the logs of a container instance."""
|
|
1720
|
+
|
|
1721
|
+
_generated_call(
|
|
1722
|
+
ctx,
|
|
1723
|
+
"GET",
|
|
1724
|
+
"/services/containers/{serviceId}/instances/{instanceId}/logs",
|
|
1725
|
+
{"serviceId": service_id, "instanceId": instance_id},
|
|
1726
|
+
output=output,
|
|
1727
|
+
)
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
@gen_containers_instances.command("show")
|
|
1731
|
+
def gen_services_containers_instances_show(
|
|
1732
|
+
ctx: typer.Context,
|
|
1733
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1734
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1735
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1736
|
+
) -> None:
|
|
1737
|
+
"""Show containers instances details."""
|
|
1738
|
+
|
|
1739
|
+
_generated_call(
|
|
1740
|
+
ctx,
|
|
1741
|
+
"GET",
|
|
1742
|
+
"/services/containers/{serviceId}/instances/{instanceId}",
|
|
1743
|
+
{"serviceId": service_id, "instanceId": instance_id},
|
|
1744
|
+
output=output,
|
|
1745
|
+
)
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
@gen_containers_instances.command("state")
|
|
1749
|
+
def gen_services_containers_instances_state(
|
|
1750
|
+
ctx: typer.Context,
|
|
1751
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1752
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1753
|
+
state: str = typer.Argument(..., metavar="STATE"),
|
|
1754
|
+
yes: bool = _YES_OPT,
|
|
1755
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1756
|
+
) -> None:
|
|
1757
|
+
"""Change the state of a container instance."""
|
|
1758
|
+
|
|
1759
|
+
_generated_call(
|
|
1760
|
+
ctx,
|
|
1761
|
+
"GET",
|
|
1762
|
+
"/services/containers/{serviceId}/instances/{instanceId}/state/{state}",
|
|
1763
|
+
{"serviceId": service_id, "instanceId": instance_id, "state": state},
|
|
1764
|
+
confirm="State containers instances",
|
|
1765
|
+
yes=yes,
|
|
1766
|
+
output=output,
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
@gen_containers_instances.command("update")
|
|
1771
|
+
def gen_services_containers_instances_update(
|
|
1772
|
+
ctx: typer.Context,
|
|
1773
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1774
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1775
|
+
image: str | None = typer.Option(None),
|
|
1776
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1777
|
+
) -> None:
|
|
1778
|
+
"""Update containers instances."""
|
|
1779
|
+
|
|
1780
|
+
_generated_call(
|
|
1781
|
+
ctx,
|
|
1782
|
+
"PUT",
|
|
1783
|
+
"/services/containers/{serviceId}/instances/{instanceId}",
|
|
1784
|
+
{"serviceId": service_id, "instanceId": instance_id},
|
|
1785
|
+
body={"image": image},
|
|
1786
|
+
output=output,
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
@gen_containers_instances_volumes.command("list")
|
|
1791
|
+
def gen_services_containers_instances_volumes_index(
|
|
1792
|
+
ctx: typer.Context,
|
|
1793
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1794
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1795
|
+
sort: str | None = _SORT_OPT,
|
|
1796
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1797
|
+
) -> None:
|
|
1798
|
+
"""List containers instances volumes."""
|
|
1799
|
+
|
|
1800
|
+
_generated_call(
|
|
1801
|
+
ctx,
|
|
1802
|
+
"GET",
|
|
1803
|
+
"/services/containers/{serviceId}/instances/{instanceId}/volumes",
|
|
1804
|
+
{"serviceId": service_id, "instanceId": instance_id},
|
|
1805
|
+
sort=sort,
|
|
1806
|
+
output=output,
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
@gen_containers_instances_volumes.command("show")
|
|
1811
|
+
def gen_services_containers_instances_volumes_show(
|
|
1812
|
+
ctx: typer.Context,
|
|
1813
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1814
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1815
|
+
volume_id: int = typer.Argument(..., metavar="VOLUME_ID"),
|
|
1816
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1817
|
+
) -> None:
|
|
1818
|
+
"""Show containers instances volumes details."""
|
|
1819
|
+
|
|
1820
|
+
_generated_call(
|
|
1821
|
+
ctx,
|
|
1822
|
+
"GET",
|
|
1823
|
+
"/services/containers/{serviceId}/instances/{instanceId}/volumes/{volumeId}",
|
|
1824
|
+
{"serviceId": service_id, "instanceId": instance_id, "volumeId": volume_id},
|
|
1825
|
+
output=output,
|
|
1826
|
+
)
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
@gen_containers_instances_volumes.command("update")
|
|
1830
|
+
def gen_services_containers_instances_volumes_update(
|
|
1831
|
+
ctx: typer.Context,
|
|
1832
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1833
|
+
instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
|
|
1834
|
+
volume_id: int = typer.Argument(..., metavar="VOLUME_ID"),
|
|
1835
|
+
path: str | None = typer.Option(None),
|
|
1836
|
+
uid: str | None = typer.Option(None),
|
|
1837
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1838
|
+
) -> None:
|
|
1839
|
+
"""Update containers instances volumes."""
|
|
1840
|
+
|
|
1841
|
+
_generated_call(
|
|
1842
|
+
ctx,
|
|
1843
|
+
"PUT",
|
|
1844
|
+
"/services/containers/{serviceId}/instances/{instanceId}/volumes/{volumeId}",
|
|
1845
|
+
{"serviceId": service_id, "instanceId": instance_id, "volumeId": volume_id},
|
|
1846
|
+
body={"path": path, "uid": uid},
|
|
1847
|
+
output=output,
|
|
1848
|
+
)
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
@gen_containers.command("show")
|
|
1852
|
+
def gen_services_containers_show(
|
|
1853
|
+
ctx: typer.Context,
|
|
1854
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1855
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1856
|
+
) -> None:
|
|
1857
|
+
"""Show containers details."""
|
|
1858
|
+
|
|
1859
|
+
_generated_call(
|
|
1860
|
+
ctx,
|
|
1861
|
+
"GET",
|
|
1862
|
+
"/services/containers/{serviceId}",
|
|
1863
|
+
{"serviceId": service_id},
|
|
1864
|
+
output=output,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
@gen_containers.command("update")
|
|
1869
|
+
def gen_services_containers_update(
|
|
1870
|
+
ctx: typer.Context,
|
|
1871
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1872
|
+
registry_url: str | None = typer.Option(None),
|
|
1873
|
+
description: str | None = typer.Option(None),
|
|
1874
|
+
port: int | None = typer.Option(None),
|
|
1875
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1876
|
+
) -> None:
|
|
1877
|
+
"""Update containers."""
|
|
1878
|
+
|
|
1879
|
+
_generated_call(
|
|
1880
|
+
ctx,
|
|
1881
|
+
"PUT",
|
|
1882
|
+
"/services/container/{serviceId}",
|
|
1883
|
+
{"serviceId": service_id},
|
|
1884
|
+
body={"registry_url": registry_url, "description": description, "port": port},
|
|
1885
|
+
output=output,
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
|
|
1889
|
+
@gen_databases_clusters.command("list")
|
|
1890
|
+
def gen_services_databases_clusters_index(
|
|
1891
|
+
ctx: typer.Context,
|
|
1892
|
+
sort: str | None = _SORT_OPT,
|
|
1893
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1894
|
+
) -> None:
|
|
1895
|
+
"""List databases clusters."""
|
|
1896
|
+
|
|
1897
|
+
_generated_call(
|
|
1898
|
+
ctx,
|
|
1899
|
+
"GET",
|
|
1900
|
+
"/services/databases/clusters",
|
|
1901
|
+
{},
|
|
1902
|
+
sort=sort,
|
|
1903
|
+
output=output,
|
|
1904
|
+
)
|
|
1905
|
+
|
|
1906
|
+
|
|
1907
|
+
@gen_databases_clusters.command("show")
|
|
1908
|
+
def gen_services_databases_clusters_show(
|
|
1909
|
+
ctx: typer.Context,
|
|
1910
|
+
cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
|
|
1911
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1912
|
+
) -> None:
|
|
1913
|
+
"""Show databases clusters details."""
|
|
1914
|
+
|
|
1915
|
+
_generated_call(
|
|
1916
|
+
ctx,
|
|
1917
|
+
"GET",
|
|
1918
|
+
"/services/databases/clusters/{clusterId}",
|
|
1919
|
+
{"clusterId": cluster_id},
|
|
1920
|
+
output=output,
|
|
1921
|
+
)
|
|
1922
|
+
|
|
1923
|
+
|
|
1924
|
+
@gen_databases_clusters_users.command("list")
|
|
1925
|
+
def gen_services_databases_clusters_users_index(
|
|
1926
|
+
ctx: typer.Context,
|
|
1927
|
+
cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
|
|
1928
|
+
sort: str | None = _SORT_OPT,
|
|
1929
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1930
|
+
) -> None:
|
|
1931
|
+
"""List databases clusters users."""
|
|
1932
|
+
|
|
1933
|
+
_generated_call(
|
|
1934
|
+
ctx,
|
|
1935
|
+
"GET",
|
|
1936
|
+
"/services/databases/clusters/{clusterId}/users",
|
|
1937
|
+
{"clusterId": cluster_id},
|
|
1938
|
+
sort=sort,
|
|
1939
|
+
output=output,
|
|
1940
|
+
)
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
@gen_databases_clusters_users.command("show")
|
|
1944
|
+
def gen_services_databases_clusters_users_show(
|
|
1945
|
+
ctx: typer.Context,
|
|
1946
|
+
cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
|
|
1947
|
+
user_id: int = typer.Argument(..., metavar="USER_ID"),
|
|
1948
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1949
|
+
) -> None:
|
|
1950
|
+
"""Show databases clusters users details."""
|
|
1951
|
+
|
|
1952
|
+
_generated_call(
|
|
1953
|
+
ctx,
|
|
1954
|
+
"GET",
|
|
1955
|
+
"/services/databases/clusters/{clusterId}/users/{userId}",
|
|
1956
|
+
{"clusterId": cluster_id, "userId": user_id},
|
|
1957
|
+
output=output,
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
@gen_databases.command("list")
|
|
1962
|
+
def gen_services_databases_index(
|
|
1963
|
+
ctx: typer.Context,
|
|
1964
|
+
sort: str | None = _SORT_OPT,
|
|
1965
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1966
|
+
) -> None:
|
|
1967
|
+
"""List databases."""
|
|
1968
|
+
|
|
1969
|
+
_generated_call(
|
|
1970
|
+
ctx,
|
|
1971
|
+
"GET",
|
|
1972
|
+
"/services/databases",
|
|
1973
|
+
{},
|
|
1974
|
+
sort=sort,
|
|
1975
|
+
output=output,
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
@gen_databases_permissions.command("create")
|
|
1980
|
+
def gen_services_databases_permissions_create(
|
|
1981
|
+
ctx: typer.Context,
|
|
1982
|
+
service_id: str = typer.Argument(..., metavar="SERVICE_ID"),
|
|
1983
|
+
user_id: str | None = typer.Option(None),
|
|
1984
|
+
permissions: list[str] | None = typer.Option(None),
|
|
1985
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
1986
|
+
) -> None:
|
|
1987
|
+
"""Create databases permissions."""
|
|
1988
|
+
|
|
1989
|
+
_generated_call(
|
|
1990
|
+
ctx,
|
|
1991
|
+
"POST",
|
|
1992
|
+
"/services/databases/{serviceId}/permissions",
|
|
1993
|
+
{"serviceId": service_id},
|
|
1994
|
+
body={"user_id": user_id, "permissions": permissions},
|
|
1995
|
+
output=output,
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
@gen_databases.command("show")
|
|
2000
|
+
def gen_services_databases_show(
|
|
2001
|
+
ctx: typer.Context,
|
|
2002
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2003
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2004
|
+
) -> None:
|
|
2005
|
+
"""Show databases details."""
|
|
2006
|
+
|
|
2007
|
+
_generated_call(
|
|
2008
|
+
ctx,
|
|
2009
|
+
"GET",
|
|
2010
|
+
"/services/databases/{serviceId}",
|
|
2011
|
+
{"serviceId": service_id},
|
|
2012
|
+
output=output,
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
@gen_databases.command("create")
|
|
2017
|
+
def gen_services_databases_store(
|
|
2018
|
+
ctx: typer.Context,
|
|
2019
|
+
name: str | None = typer.Option(None),
|
|
2020
|
+
client_id: int | None = typer.Option(None),
|
|
2021
|
+
cluster_id: int | None = typer.Option(None),
|
|
2022
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2023
|
+
) -> None:
|
|
2024
|
+
"""Create databases."""
|
|
2025
|
+
|
|
2026
|
+
_generated_call(
|
|
2027
|
+
ctx,
|
|
2028
|
+
"POST",
|
|
2029
|
+
"/services/databases",
|
|
2030
|
+
{},
|
|
2031
|
+
body={"name": name, "client_id": client_id, "cluster_id": cluster_id},
|
|
2032
|
+
output=output,
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
@gen_databases_users.command("create")
|
|
2037
|
+
def gen_services_databases_users_store(
|
|
2038
|
+
ctx: typer.Context,
|
|
2039
|
+
cluster_id: str = typer.Argument(..., metavar="CLUSTER_ID"),
|
|
2040
|
+
username: str | None = typer.Option(None),
|
|
2041
|
+
password: str | None = typer.Option(None),
|
|
2042
|
+
client_id: int | None = typer.Option(None),
|
|
2043
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2044
|
+
) -> None:
|
|
2045
|
+
"""Create databases users."""
|
|
2046
|
+
|
|
2047
|
+
_generated_call(
|
|
2048
|
+
ctx,
|
|
2049
|
+
"POST",
|
|
2050
|
+
"/services/databases/clusters/{clusterId}/users",
|
|
2051
|
+
{"clusterId": cluster_id},
|
|
2052
|
+
body={"username": username, "password": password, "client_id": client_id},
|
|
2053
|
+
output=output,
|
|
2054
|
+
)
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
@gen_databases_users.command("update")
|
|
2058
|
+
def gen_services_databases_users_update(
|
|
2059
|
+
ctx: typer.Context,
|
|
2060
|
+
cluster_id: str = typer.Argument(..., metavar="CLUSTER_ID"),
|
|
2061
|
+
user_id: str = typer.Argument(..., metavar="USER_ID"),
|
|
2062
|
+
password: str | None = typer.Option(None),
|
|
2063
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2064
|
+
) -> None:
|
|
2065
|
+
"""Update databases users."""
|
|
2066
|
+
|
|
2067
|
+
_generated_call(
|
|
2068
|
+
ctx,
|
|
2069
|
+
"PUT",
|
|
2070
|
+
"/services/databases/clusters/{clusterId}/users/{userId}",
|
|
2071
|
+
{"clusterId": cluster_id, "userId": user_id},
|
|
2072
|
+
body={"password": password},
|
|
2073
|
+
output=output,
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
@gen_domains.command("create")
|
|
2078
|
+
def gen_services_domains_create(
|
|
2079
|
+
ctx: typer.Context,
|
|
2080
|
+
nameserver_group: str | None = typer.Option(None),
|
|
2081
|
+
domain: str | None = typer.Option(None),
|
|
2082
|
+
client_id: int | None = typer.Option(None),
|
|
2083
|
+
type: str | None = typer.Option(None),
|
|
2084
|
+
token: str | None = typer.Option(None),
|
|
2085
|
+
handle_id_owner: int | None = typer.Option(None),
|
|
2086
|
+
handle_id_administrative: int | None = typer.Option(None),
|
|
2087
|
+
handle_id_technical: int | None = typer.Option(None),
|
|
2088
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2089
|
+
) -> None:
|
|
2090
|
+
"""Create domains."""
|
|
2091
|
+
|
|
2092
|
+
_generated_call(
|
|
2093
|
+
ctx,
|
|
2094
|
+
"POST",
|
|
2095
|
+
"/services/domains",
|
|
2096
|
+
{},
|
|
2097
|
+
body={
|
|
2098
|
+
"nameserver_group": nameserver_group,
|
|
2099
|
+
"domain": domain,
|
|
2100
|
+
"client_id": client_id,
|
|
2101
|
+
"type": type,
|
|
2102
|
+
"token": token,
|
|
2103
|
+
"handle_id_owner": handle_id_owner,
|
|
2104
|
+
"handle_id_administrative": handle_id_administrative,
|
|
2105
|
+
"handle_id_technical": handle_id_technical,
|
|
2106
|
+
},
|
|
2107
|
+
output=output,
|
|
2108
|
+
)
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
@gen_domains.command("list")
|
|
2112
|
+
def gen_services_domains_index(
|
|
2113
|
+
ctx: typer.Context,
|
|
2114
|
+
sort: str | None = _SORT_OPT,
|
|
2115
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2116
|
+
) -> None:
|
|
2117
|
+
"""List domains."""
|
|
2118
|
+
|
|
2119
|
+
_generated_call(
|
|
2120
|
+
ctx,
|
|
2121
|
+
"GET",
|
|
2122
|
+
"/services/domains",
|
|
2123
|
+
{},
|
|
2124
|
+
sort=sort,
|
|
2125
|
+
output=output,
|
|
2126
|
+
)
|
|
2127
|
+
|
|
2128
|
+
|
|
2129
|
+
@gen_domains.command("show")
|
|
2130
|
+
def gen_services_domains_show(
|
|
2131
|
+
ctx: typer.Context,
|
|
2132
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2133
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2134
|
+
) -> None:
|
|
2135
|
+
"""Show domains details."""
|
|
2136
|
+
|
|
2137
|
+
_generated_call(
|
|
2138
|
+
ctx,
|
|
2139
|
+
"GET",
|
|
2140
|
+
"/services/domains/{serviceId}",
|
|
2141
|
+
{"serviceId": service_id},
|
|
2142
|
+
output=output,
|
|
2143
|
+
)
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
@gen_emails_aliases.command("create")
|
|
2147
|
+
def gen_services_emails_aliases_create(
|
|
2148
|
+
ctx: typer.Context,
|
|
2149
|
+
service_id: str = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2150
|
+
emailaddress: str | None = typer.Option(None),
|
|
2151
|
+
alias: str | None = typer.Option(None),
|
|
2152
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2153
|
+
) -> None:
|
|
2154
|
+
"""Create emails aliases."""
|
|
2155
|
+
|
|
2156
|
+
_generated_call(
|
|
2157
|
+
ctx,
|
|
2158
|
+
"POST",
|
|
2159
|
+
"/services/emails/{serviceId}/aliases",
|
|
2160
|
+
{"serviceId": service_id},
|
|
2161
|
+
body={"emailaddress": emailaddress, "alias": alias},
|
|
2162
|
+
output=output,
|
|
2163
|
+
)
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
@gen_emails_aliases.command("delete")
|
|
2167
|
+
def gen_services_emails_aliases_delete(
|
|
2168
|
+
ctx: typer.Context,
|
|
2169
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2170
|
+
alias_id: int = typer.Argument(..., metavar="ALIAS_ID"),
|
|
2171
|
+
yes: bool = _YES_OPT,
|
|
2172
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2173
|
+
) -> None:
|
|
2174
|
+
"""Delete emails aliases."""
|
|
2175
|
+
|
|
2176
|
+
_generated_call(
|
|
2177
|
+
ctx,
|
|
2178
|
+
"DELETE",
|
|
2179
|
+
"/services/emails/{serviceId}/aliases/{aliasId}",
|
|
2180
|
+
{"serviceId": service_id, "aliasId": alias_id},
|
|
2181
|
+
confirm="Delete emails aliases",
|
|
2182
|
+
yes=yes,
|
|
2183
|
+
output=output,
|
|
2184
|
+
)
|
|
2185
|
+
|
|
2186
|
+
|
|
2187
|
+
@gen_emails_aliases.command("list")
|
|
2188
|
+
def gen_services_emails_aliases_index(
|
|
2189
|
+
ctx: typer.Context,
|
|
2190
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2191
|
+
sort: str | None = _SORT_OPT,
|
|
2192
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2193
|
+
) -> None:
|
|
2194
|
+
"""List emails aliases."""
|
|
2195
|
+
|
|
2196
|
+
_generated_call(
|
|
2197
|
+
ctx,
|
|
2198
|
+
"GET",
|
|
2199
|
+
"/services/emails/{serviceId}/aliases",
|
|
2200
|
+
{"serviceId": service_id},
|
|
2201
|
+
sort=sort,
|
|
2202
|
+
output=output,
|
|
2203
|
+
)
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
@gen_emails_aliases.command("show")
|
|
2207
|
+
def gen_services_emails_aliases_show(
|
|
2208
|
+
ctx: typer.Context,
|
|
2209
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2210
|
+
alias_id: int = typer.Argument(..., metavar="ALIAS_ID"),
|
|
2211
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2212
|
+
) -> None:
|
|
2213
|
+
"""Show emails aliases details."""
|
|
2214
|
+
|
|
2215
|
+
_generated_call(
|
|
2216
|
+
ctx,
|
|
2217
|
+
"GET",
|
|
2218
|
+
"/services/emails/{serviceId}/aliases/{aliasId}",
|
|
2219
|
+
{"serviceId": service_id, "aliasId": alias_id},
|
|
2220
|
+
output=output,
|
|
2221
|
+
)
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
@gen_emails.command("create")
|
|
2225
|
+
def gen_services_emails_create(
|
|
2226
|
+
ctx: typer.Context,
|
|
2227
|
+
domain: str | None = typer.Option(None),
|
|
2228
|
+
client_id: int | None = typer.Option(None),
|
|
2229
|
+
type: str | None = typer.Option(None),
|
|
2230
|
+
return_path_domain: str | None = typer.Option(None),
|
|
2231
|
+
relay_hosts: list[str] | None = typer.Option(None),
|
|
2232
|
+
delivery_rate: int | None = typer.Option(None),
|
|
2233
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2234
|
+
) -> None:
|
|
2235
|
+
"""Create emails."""
|
|
2236
|
+
|
|
2237
|
+
_generated_call(
|
|
2238
|
+
ctx,
|
|
2239
|
+
"POST",
|
|
2240
|
+
"/services/emails",
|
|
2241
|
+
{},
|
|
2242
|
+
body={
|
|
2243
|
+
"domain": domain,
|
|
2244
|
+
"client_id": client_id,
|
|
2245
|
+
"type": type,
|
|
2246
|
+
"return_path_domain": return_path_domain,
|
|
2247
|
+
"relay_hosts": relay_hosts,
|
|
2248
|
+
"delivery_rate": delivery_rate,
|
|
2249
|
+
},
|
|
2250
|
+
output=output,
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
@gen_emails.command("delete")
|
|
2255
|
+
def gen_services_emails_delete(
|
|
2256
|
+
ctx: typer.Context,
|
|
2257
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2258
|
+
yes: bool = _YES_OPT,
|
|
2259
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2260
|
+
) -> None:
|
|
2261
|
+
"""Delete emails."""
|
|
2262
|
+
|
|
2263
|
+
_generated_call(
|
|
2264
|
+
ctx,
|
|
2265
|
+
"DELETE",
|
|
2266
|
+
"/services/emails/{serviceId}",
|
|
2267
|
+
{"serviceId": service_id},
|
|
2268
|
+
confirm="Delete emails",
|
|
2269
|
+
yes=yes,
|
|
2270
|
+
output=output,
|
|
2271
|
+
)
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
@gen_emails_dkim.command("show")
|
|
2275
|
+
def gen_services_emails_dkim_show(
|
|
2276
|
+
ctx: typer.Context,
|
|
2277
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2278
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2279
|
+
) -> None:
|
|
2280
|
+
"""Show the DKIM configuration of an email service."""
|
|
2281
|
+
|
|
2282
|
+
_generated_call(
|
|
2283
|
+
ctx,
|
|
2284
|
+
"GET",
|
|
2285
|
+
"/services/emails/{serviceId}/dkim",
|
|
2286
|
+
{"serviceId": service_id},
|
|
2287
|
+
output=output,
|
|
2288
|
+
)
|
|
2289
|
+
|
|
2290
|
+
|
|
2291
|
+
@gen_emails_forwardings.command("create")
|
|
2292
|
+
def gen_services_emails_forwardings_create(
|
|
2293
|
+
ctx: typer.Context,
|
|
2294
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2295
|
+
emailaddress: str | None = typer.Option(None),
|
|
2296
|
+
target: str | None = typer.Option(None),
|
|
2297
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2298
|
+
) -> None:
|
|
2299
|
+
"""Create emails forwardings."""
|
|
2300
|
+
|
|
2301
|
+
_generated_call(
|
|
2302
|
+
ctx,
|
|
2303
|
+
"POST",
|
|
2304
|
+
"/services/emails/{serviceId}/forwardings",
|
|
2305
|
+
{"serviceId": service_id},
|
|
2306
|
+
body={"emailaddress": emailaddress, "target": target},
|
|
2307
|
+
output=output,
|
|
2308
|
+
)
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
@gen_emails_forwardings.command("delete")
|
|
2312
|
+
def gen_services_emails_forwardings_delete(
|
|
2313
|
+
ctx: typer.Context,
|
|
2314
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2315
|
+
forwarding_id: int = typer.Argument(..., metavar="FORWARDING_ID"),
|
|
2316
|
+
yes: bool = _YES_OPT,
|
|
2317
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2318
|
+
) -> None:
|
|
2319
|
+
"""Delete emails forwardings."""
|
|
2320
|
+
|
|
2321
|
+
_generated_call(
|
|
2322
|
+
ctx,
|
|
2323
|
+
"DELETE",
|
|
2324
|
+
"/services/emails/{serviceId}/forwardings/{forwardingId}",
|
|
2325
|
+
{"serviceId": service_id, "forwardingId": forwarding_id},
|
|
2326
|
+
confirm="Delete emails forwardings",
|
|
2327
|
+
yes=yes,
|
|
2328
|
+
output=output,
|
|
2329
|
+
)
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
@gen_emails_forwardings.command("list")
|
|
2333
|
+
def gen_services_emails_forwardings_index(
|
|
2334
|
+
ctx: typer.Context,
|
|
2335
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2336
|
+
sort: str | None = _SORT_OPT,
|
|
2337
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2338
|
+
) -> None:
|
|
2339
|
+
"""List emails forwardings."""
|
|
2340
|
+
|
|
2341
|
+
_generated_call(
|
|
2342
|
+
ctx,
|
|
2343
|
+
"GET",
|
|
2344
|
+
"/services/emails/{serviceId}/forwardings",
|
|
2345
|
+
{"serviceId": service_id},
|
|
2346
|
+
sort=sort,
|
|
2347
|
+
output=output,
|
|
2348
|
+
)
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
@gen_emails_forwardings.command("show")
|
|
2352
|
+
def gen_services_emails_forwardings_show(
|
|
2353
|
+
ctx: typer.Context,
|
|
2354
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2355
|
+
forwarding_id: int = typer.Argument(..., metavar="FORWARDING_ID"),
|
|
2356
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2357
|
+
) -> None:
|
|
2358
|
+
"""Show emails forwardings details."""
|
|
2359
|
+
|
|
2360
|
+
_generated_call(
|
|
2361
|
+
ctx,
|
|
2362
|
+
"GET",
|
|
2363
|
+
"/services/emails/{serviceId}/forwardings/{forwardingId}",
|
|
2364
|
+
{"serviceId": service_id, "forwardingId": forwarding_id},
|
|
2365
|
+
output=output,
|
|
2366
|
+
)
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
@gen_emails.command("list")
|
|
2370
|
+
def gen_services_emails_index(
|
|
2371
|
+
ctx: typer.Context,
|
|
2372
|
+
sort: str | None = _SORT_OPT,
|
|
2373
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2374
|
+
) -> None:
|
|
2375
|
+
"""List emails."""
|
|
2376
|
+
|
|
2377
|
+
_generated_call(
|
|
2378
|
+
ctx,
|
|
2379
|
+
"GET",
|
|
2380
|
+
"/services/emails",
|
|
2381
|
+
{},
|
|
2382
|
+
sort=sort,
|
|
2383
|
+
output=output,
|
|
2384
|
+
)
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
@gen_emails_mailboxes.command("create")
|
|
2388
|
+
def gen_services_emails_mailboxes_create(
|
|
2389
|
+
ctx: typer.Context,
|
|
2390
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2391
|
+
emailaddress: str | None = typer.Option(None),
|
|
2392
|
+
password: str | None = typer.Option(None),
|
|
2393
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2394
|
+
) -> None:
|
|
2395
|
+
"""Create emails mailboxes."""
|
|
2396
|
+
|
|
2397
|
+
_generated_call(
|
|
2398
|
+
ctx,
|
|
2399
|
+
"POST",
|
|
2400
|
+
"/services/emails/{serviceId}/mailboxes",
|
|
2401
|
+
{"serviceId": service_id},
|
|
2402
|
+
body={"emailaddress": emailaddress, "password": password},
|
|
2403
|
+
output=output,
|
|
2404
|
+
)
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
@gen_emails_mailboxes.command("delete")
|
|
2408
|
+
def gen_services_emails_mailboxes_delete(
|
|
2409
|
+
ctx: typer.Context,
|
|
2410
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2411
|
+
emailaddress: str = typer.Argument(..., metavar="EMAILADDRESS"),
|
|
2412
|
+
yes: bool = _YES_OPT,
|
|
2413
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2414
|
+
) -> None:
|
|
2415
|
+
"""Delete emails mailboxes."""
|
|
2416
|
+
|
|
2417
|
+
_generated_call(
|
|
2418
|
+
ctx,
|
|
2419
|
+
"DELETE",
|
|
2420
|
+
"/services/emails/{serviceId}/mailboxes/{emailaddress}",
|
|
2421
|
+
{"serviceId": service_id, "emailaddress": emailaddress},
|
|
2422
|
+
confirm="Delete emails mailboxes",
|
|
2423
|
+
yes=yes,
|
|
2424
|
+
output=output,
|
|
2425
|
+
)
|
|
2426
|
+
|
|
2427
|
+
|
|
2428
|
+
@gen_emails_mailboxes.command("list")
|
|
2429
|
+
def gen_services_emails_mailboxes_index(
|
|
2430
|
+
ctx: typer.Context,
|
|
2431
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2432
|
+
sort: str | None = _SORT_OPT,
|
|
2433
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2434
|
+
) -> None:
|
|
2435
|
+
"""List emails mailboxes."""
|
|
2436
|
+
|
|
2437
|
+
_generated_call(
|
|
2438
|
+
ctx,
|
|
2439
|
+
"GET",
|
|
2440
|
+
"/services/emails/{serviceId}/mailboxes",
|
|
2441
|
+
{"serviceId": service_id},
|
|
2442
|
+
sort=sort,
|
|
2443
|
+
output=output,
|
|
2444
|
+
)
|
|
2445
|
+
|
|
2446
|
+
|
|
2447
|
+
@gen_emails_mailboxes.command("show")
|
|
2448
|
+
def gen_services_emails_mailboxes_show(
|
|
2449
|
+
ctx: typer.Context,
|
|
2450
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2451
|
+
emailaddress: str = typer.Argument(..., metavar="EMAILADDRESS"),
|
|
2452
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2453
|
+
) -> None:
|
|
2454
|
+
"""Show emails mailboxes details."""
|
|
2455
|
+
|
|
2456
|
+
_generated_call(
|
|
2457
|
+
ctx,
|
|
2458
|
+
"GET",
|
|
2459
|
+
"/services/emails/{serviceId}/mailboxes/{emailaddress}",
|
|
2460
|
+
{"serviceId": service_id, "emailaddress": emailaddress},
|
|
2461
|
+
output=output,
|
|
2462
|
+
)
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
@gen_emails_mailboxes.command("update")
|
|
2466
|
+
def gen_services_emails_mailboxes_update(
|
|
2467
|
+
ctx: typer.Context,
|
|
2468
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2469
|
+
emailaddress: str = typer.Argument(..., metavar="EMAILADDRESS"),
|
|
2470
|
+
password: str | None = typer.Option(None),
|
|
2471
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2472
|
+
) -> None:
|
|
2473
|
+
"""Update emails mailboxes."""
|
|
2474
|
+
|
|
2475
|
+
_generated_call(
|
|
2476
|
+
ctx,
|
|
2477
|
+
"PUT",
|
|
2478
|
+
"/services/emails/{serviceId}/mailboxes/{emailaddress}",
|
|
2479
|
+
{"serviceId": service_id, "emailaddress": emailaddress},
|
|
2480
|
+
body={"password": password},
|
|
2481
|
+
output=output,
|
|
2482
|
+
)
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
@gen_emails_records.command("list")
|
|
2486
|
+
def gen_services_emails_records_index(
|
|
2487
|
+
ctx: typer.Context,
|
|
2488
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2489
|
+
sort: str | None = _SORT_OPT,
|
|
2490
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2491
|
+
) -> None:
|
|
2492
|
+
"""List the DNS records an email service needs."""
|
|
2493
|
+
|
|
2494
|
+
_generated_call(
|
|
2495
|
+
ctx,
|
|
2496
|
+
"GET",
|
|
2497
|
+
"/services/emails/{serviceId}/records",
|
|
2498
|
+
{"serviceId": service_id},
|
|
2499
|
+
sort=sort,
|
|
2500
|
+
output=output,
|
|
2501
|
+
)
|
|
2502
|
+
|
|
2503
|
+
|
|
2504
|
+
@gen_emails.command("show")
|
|
2505
|
+
def gen_services_emails_show(
|
|
2506
|
+
ctx: typer.Context,
|
|
2507
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2508
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2509
|
+
) -> None:
|
|
2510
|
+
"""Show emails details."""
|
|
2511
|
+
|
|
2512
|
+
_generated_call(
|
|
2513
|
+
ctx,
|
|
2514
|
+
"GET",
|
|
2515
|
+
"/services/emails/{serviceId}",
|
|
2516
|
+
{"serviceId": service_id},
|
|
2517
|
+
output=output,
|
|
2518
|
+
)
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
@gen_emails_transactional_aliases.command("create")
|
|
2522
|
+
def gen_services_emails_transactional_aliases_create(
|
|
2523
|
+
ctx: typer.Context,
|
|
2524
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2525
|
+
domain: str | None = typer.Option(None),
|
|
2526
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2527
|
+
) -> None:
|
|
2528
|
+
"""Create emails transactional aliases."""
|
|
2529
|
+
|
|
2530
|
+
_generated_call(
|
|
2531
|
+
ctx,
|
|
2532
|
+
"POST",
|
|
2533
|
+
"/services/emails/{serviceId}/transactional/aliases",
|
|
2534
|
+
{"serviceId": service_id},
|
|
2535
|
+
body={"domain": domain},
|
|
2536
|
+
output=output,
|
|
2537
|
+
)
|
|
2538
|
+
|
|
2539
|
+
|
|
2540
|
+
@gen_emails_transactional_aliases.command("delete")
|
|
2541
|
+
def gen_services_emails_transactional_aliases_delete(
|
|
2542
|
+
ctx: typer.Context,
|
|
2543
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2544
|
+
alias_id: int = typer.Argument(..., metavar="ALIAS_ID"),
|
|
2545
|
+
yes: bool = _YES_OPT,
|
|
2546
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2547
|
+
) -> None:
|
|
2548
|
+
"""Delete emails transactional aliases."""
|
|
2549
|
+
|
|
2550
|
+
_generated_call(
|
|
2551
|
+
ctx,
|
|
2552
|
+
"DELETE",
|
|
2553
|
+
"/services/emails/{serviceId}/transactional/aliases/{aliasId}",
|
|
2554
|
+
{"serviceId": service_id, "aliasId": alias_id},
|
|
2555
|
+
confirm="Delete emails transactional aliases",
|
|
2556
|
+
yes=yes,
|
|
2557
|
+
output=output,
|
|
2558
|
+
)
|
|
2559
|
+
|
|
2560
|
+
|
|
2561
|
+
@gen_emails_transactional_aliases.command("list")
|
|
2562
|
+
def gen_services_emails_transactional_aliases_index(
|
|
2563
|
+
ctx: typer.Context,
|
|
2564
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2565
|
+
sort: str | None = _SORT_OPT,
|
|
2566
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2567
|
+
) -> None:
|
|
2568
|
+
"""List emails transactional aliases."""
|
|
2569
|
+
|
|
2570
|
+
_generated_call(
|
|
2571
|
+
ctx,
|
|
2572
|
+
"GET",
|
|
2573
|
+
"/services/emails/{serviceId}/transactional/aliases",
|
|
2574
|
+
{"serviceId": service_id},
|
|
2575
|
+
sort=sort,
|
|
2576
|
+
output=output,
|
|
2577
|
+
)
|
|
2578
|
+
|
|
2579
|
+
|
|
2580
|
+
@gen_emails_transactional_keys.command("create")
|
|
2581
|
+
def gen_services_emails_transactional_keys_create(
|
|
2582
|
+
ctx: typer.Context,
|
|
2583
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2584
|
+
name: str | None = typer.Option(None),
|
|
2585
|
+
key: str | None = typer.Option(None),
|
|
2586
|
+
active: bool | None = typer.Option(None),
|
|
2587
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2588
|
+
) -> None:
|
|
2589
|
+
"""Create emails transactional keys."""
|
|
2590
|
+
|
|
2591
|
+
_generated_call(
|
|
2592
|
+
ctx,
|
|
2593
|
+
"POST",
|
|
2594
|
+
"/services/emails/{serviceId}/transactional/keys",
|
|
2595
|
+
{"serviceId": service_id},
|
|
2596
|
+
body={"name": name, "key": key, "active": active},
|
|
2597
|
+
output=output,
|
|
2598
|
+
)
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
@gen_emails_transactional_keys.command("delete")
|
|
2602
|
+
def gen_services_emails_transactional_keys_delete(
|
|
2603
|
+
ctx: typer.Context,
|
|
2604
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2605
|
+
key_id: int = typer.Argument(..., metavar="KEY_ID"),
|
|
2606
|
+
yes: bool = _YES_OPT,
|
|
2607
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2608
|
+
) -> None:
|
|
2609
|
+
"""Delete emails transactional keys."""
|
|
2610
|
+
|
|
2611
|
+
_generated_call(
|
|
2612
|
+
ctx,
|
|
2613
|
+
"DELETE",
|
|
2614
|
+
"/services/emails/{serviceId}/transactional/keys/{keyId}",
|
|
2615
|
+
{"serviceId": service_id, "keyId": key_id},
|
|
2616
|
+
confirm="Delete emails transactional keys",
|
|
2617
|
+
yes=yes,
|
|
2618
|
+
output=output,
|
|
2619
|
+
)
|
|
2620
|
+
|
|
2621
|
+
|
|
2622
|
+
@gen_emails_transactional_keys.command("list")
|
|
2623
|
+
def gen_services_emails_transactional_keys_index(
|
|
2624
|
+
ctx: typer.Context,
|
|
2625
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2626
|
+
sort: str | None = _SORT_OPT,
|
|
2627
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2628
|
+
) -> None:
|
|
2629
|
+
"""List emails transactional keys."""
|
|
2630
|
+
|
|
2631
|
+
_generated_call(
|
|
2632
|
+
ctx,
|
|
2633
|
+
"GET",
|
|
2634
|
+
"/services/emails/{serviceId}/transactional/keys",
|
|
2635
|
+
{"serviceId": service_id},
|
|
2636
|
+
sort=sort,
|
|
2637
|
+
output=output,
|
|
2638
|
+
)
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
@gen_emails_transactional_keys.command("show")
|
|
2642
|
+
def gen_services_emails_transactional_keys_show(
|
|
2643
|
+
ctx: typer.Context,
|
|
2644
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2645
|
+
key_id: int = typer.Argument(..., metavar="KEY_ID"),
|
|
2646
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2647
|
+
) -> None:
|
|
2648
|
+
"""Show emails transactional keys details."""
|
|
2649
|
+
|
|
2650
|
+
_generated_call(
|
|
2651
|
+
ctx,
|
|
2652
|
+
"GET",
|
|
2653
|
+
"/services/emails/{serviceId}/transactional/keys/{keyId}",
|
|
2654
|
+
{"serviceId": service_id, "keyId": key_id},
|
|
2655
|
+
output=output,
|
|
2656
|
+
)
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
@gen_emails_transactional_keys.command("update")
|
|
2660
|
+
def gen_services_emails_transactional_keys_update(
|
|
2661
|
+
ctx: typer.Context,
|
|
2662
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2663
|
+
key_id: int = typer.Argument(..., metavar="KEY_ID"),
|
|
2664
|
+
name: str | None = typer.Option(None),
|
|
2665
|
+
key: str | None = typer.Option(None),
|
|
2666
|
+
active: bool | None = typer.Option(None),
|
|
2667
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2668
|
+
) -> None:
|
|
2669
|
+
"""Update emails transactional keys."""
|
|
2670
|
+
|
|
2671
|
+
_generated_call(
|
|
2672
|
+
ctx,
|
|
2673
|
+
"PUT",
|
|
2674
|
+
"/services/emails/{serviceId}/transactional/keys/{keyId}",
|
|
2675
|
+
{"serviceId": service_id, "keyId": key_id},
|
|
2676
|
+
body={"name": name, "key": key, "active": active},
|
|
2677
|
+
output=output,
|
|
2678
|
+
)
|
|
2679
|
+
|
|
2680
|
+
|
|
2681
|
+
@gen_emails_transactional_users.command("create")
|
|
2682
|
+
def gen_services_emails_transactional_users_create(
|
|
2683
|
+
ctx: typer.Context,
|
|
2684
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2685
|
+
emailaddress: str | None = typer.Option(None),
|
|
2686
|
+
password: str | None = typer.Option(None),
|
|
2687
|
+
delivery_mode: str | None = typer.Option(None),
|
|
2688
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2689
|
+
) -> None:
|
|
2690
|
+
"""Create emails transactional users."""
|
|
2691
|
+
|
|
2692
|
+
_generated_call(
|
|
2693
|
+
ctx,
|
|
2694
|
+
"POST",
|
|
2695
|
+
"/services/emails/{serviceId}/transactional/users",
|
|
2696
|
+
{"serviceId": service_id},
|
|
2697
|
+
body={
|
|
2698
|
+
"emailaddress": emailaddress,
|
|
2699
|
+
"password": password,
|
|
2700
|
+
"delivery_mode": delivery_mode,
|
|
2701
|
+
},
|
|
2702
|
+
output=output,
|
|
2703
|
+
)
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
@gen_emails_transactional_users.command("delete")
|
|
2707
|
+
def gen_services_emails_transactional_users_delete(
|
|
2708
|
+
ctx: typer.Context,
|
|
2709
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2710
|
+
user_id: int = typer.Argument(..., metavar="USER_ID"),
|
|
2711
|
+
yes: bool = _YES_OPT,
|
|
2712
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2713
|
+
) -> None:
|
|
2714
|
+
"""Delete emails transactional users."""
|
|
2715
|
+
|
|
2716
|
+
_generated_call(
|
|
2717
|
+
ctx,
|
|
2718
|
+
"DELETE",
|
|
2719
|
+
"/services/emails/{serviceId}/transactional/users/{userId}",
|
|
2720
|
+
{"serviceId": service_id, "userId": user_id},
|
|
2721
|
+
confirm="Delete emails transactional users",
|
|
2722
|
+
yes=yes,
|
|
2723
|
+
output=output,
|
|
2724
|
+
)
|
|
2725
|
+
|
|
2726
|
+
|
|
2727
|
+
@gen_emails_transactional_users.command("list")
|
|
2728
|
+
def gen_services_emails_transactional_users_index(
|
|
2729
|
+
ctx: typer.Context,
|
|
2730
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2731
|
+
sort: str | None = _SORT_OPT,
|
|
2732
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2733
|
+
) -> None:
|
|
2734
|
+
"""List emails transactional users."""
|
|
2735
|
+
|
|
2736
|
+
_generated_call(
|
|
2737
|
+
ctx,
|
|
2738
|
+
"GET",
|
|
2739
|
+
"/services/emails/{serviceId}/transactional/users",
|
|
2740
|
+
{"serviceId": service_id},
|
|
2741
|
+
sort=sort,
|
|
2742
|
+
output=output,
|
|
2743
|
+
)
|
|
2744
|
+
|
|
2745
|
+
|
|
2746
|
+
@gen_emails_transactional_users.command("show")
|
|
2747
|
+
def gen_services_emails_transactional_users_show(
|
|
2748
|
+
ctx: typer.Context,
|
|
2749
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2750
|
+
user_id: int = typer.Argument(..., metavar="USER_ID"),
|
|
2751
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2752
|
+
) -> None:
|
|
2753
|
+
"""Show emails transactional users details."""
|
|
2754
|
+
|
|
2755
|
+
_generated_call(
|
|
2756
|
+
ctx,
|
|
2757
|
+
"GET",
|
|
2758
|
+
"/services/emails/{serviceId}/transactional/users/{userId}",
|
|
2759
|
+
{"serviceId": service_id, "userId": user_id},
|
|
2760
|
+
output=output,
|
|
2761
|
+
)
|
|
2762
|
+
|
|
2763
|
+
|
|
2764
|
+
@gen_emails_transactional_users.command("update")
|
|
2765
|
+
def gen_services_emails_transactional_users_update(
|
|
2766
|
+
ctx: typer.Context,
|
|
2767
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2768
|
+
user_id: int = typer.Argument(..., metavar="USER_ID"),
|
|
2769
|
+
password: str | None = typer.Option(None),
|
|
2770
|
+
delivery_mode: str | None = typer.Option(None),
|
|
2771
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2772
|
+
) -> None:
|
|
2773
|
+
"""Update emails transactional users."""
|
|
2774
|
+
|
|
2775
|
+
_generated_call(
|
|
2776
|
+
ctx,
|
|
2777
|
+
"PUT",
|
|
2778
|
+
"/services/emails/{serviceId}/transactional/users/{userId}",
|
|
2779
|
+
{"serviceId": service_id, "userId": user_id},
|
|
2780
|
+
body={"password": password, "delivery_mode": delivery_mode},
|
|
2781
|
+
output=output,
|
|
2782
|
+
)
|
|
2783
|
+
|
|
2784
|
+
|
|
2785
|
+
@gen_forwards.command("list")
|
|
2786
|
+
def gen_services_forwards_index(
|
|
2787
|
+
ctx: typer.Context,
|
|
2788
|
+
sort: str | None = _SORT_OPT,
|
|
2789
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2790
|
+
) -> None:
|
|
2791
|
+
"""List forwards."""
|
|
2792
|
+
|
|
2793
|
+
_generated_call(
|
|
2794
|
+
ctx,
|
|
2795
|
+
"GET",
|
|
2796
|
+
"/services/forwards",
|
|
2797
|
+
{},
|
|
2798
|
+
sort=sort,
|
|
2799
|
+
output=output,
|
|
2800
|
+
)
|
|
2801
|
+
|
|
2802
|
+
|
|
2803
|
+
@gen_forwards.command("show")
|
|
2804
|
+
def gen_services_forwards_show(
|
|
2805
|
+
ctx: typer.Context,
|
|
2806
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2807
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2808
|
+
) -> None:
|
|
2809
|
+
"""Show forwards details."""
|
|
2810
|
+
|
|
2811
|
+
_generated_call(
|
|
2812
|
+
ctx,
|
|
2813
|
+
"GET",
|
|
2814
|
+
"/services/forwards/{serviceId}",
|
|
2815
|
+
{"serviceId": service_id},
|
|
2816
|
+
output=output,
|
|
2817
|
+
)
|
|
2818
|
+
|
|
2819
|
+
|
|
2820
|
+
@gen_proxies.command("down")
|
|
2821
|
+
def gen_services_proxies_down(
|
|
2822
|
+
ctx: typer.Context,
|
|
2823
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2824
|
+
yes: bool = _YES_OPT,
|
|
2825
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2826
|
+
) -> None:
|
|
2827
|
+
"""Bring a proxy down."""
|
|
2828
|
+
|
|
2829
|
+
_generated_call(
|
|
2830
|
+
ctx,
|
|
2831
|
+
"GET",
|
|
2832
|
+
"/services/proxies/{serviceId}/down",
|
|
2833
|
+
{"serviceId": service_id},
|
|
2834
|
+
confirm="Down proxies",
|
|
2835
|
+
yes=yes,
|
|
2836
|
+
output=output,
|
|
2837
|
+
)
|
|
2838
|
+
|
|
2839
|
+
|
|
2840
|
+
@gen_proxies.command("list")
|
|
2841
|
+
def gen_services_proxies_index(
|
|
2842
|
+
ctx: typer.Context,
|
|
2843
|
+
sort: str | None = _SORT_OPT,
|
|
2844
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2845
|
+
) -> None:
|
|
2846
|
+
"""List proxies."""
|
|
2847
|
+
|
|
2848
|
+
_generated_call(
|
|
2849
|
+
ctx,
|
|
2850
|
+
"GET",
|
|
2851
|
+
"/services/proxies",
|
|
2852
|
+
{},
|
|
2853
|
+
sort=sort,
|
|
2854
|
+
output=output,
|
|
2855
|
+
)
|
|
2856
|
+
|
|
2857
|
+
|
|
2858
|
+
@gen_proxies.command("show")
|
|
2859
|
+
def gen_services_proxies_show(
|
|
2860
|
+
ctx: typer.Context,
|
|
2861
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2862
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2863
|
+
) -> None:
|
|
2864
|
+
"""Show proxies details."""
|
|
2865
|
+
|
|
2866
|
+
_generated_call(
|
|
2867
|
+
ctx,
|
|
2868
|
+
"GET",
|
|
2869
|
+
"/services/proxies/{serviceId}",
|
|
2870
|
+
{"serviceId": service_id},
|
|
2871
|
+
output=output,
|
|
2872
|
+
)
|
|
2873
|
+
|
|
2874
|
+
|
|
2875
|
+
@gen_proxies.command("up")
|
|
2876
|
+
def gen_services_proxies_up(
|
|
2877
|
+
ctx: typer.Context,
|
|
2878
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2879
|
+
yes: bool = _YES_OPT,
|
|
2880
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2881
|
+
) -> None:
|
|
2882
|
+
"""Bring a proxy up."""
|
|
2883
|
+
|
|
2884
|
+
_generated_call(
|
|
2885
|
+
ctx,
|
|
2886
|
+
"GET",
|
|
2887
|
+
"/services/proxies/{serviceId}/up",
|
|
2888
|
+
{"serviceId": service_id},
|
|
2889
|
+
confirm="Up proxies",
|
|
2890
|
+
yes=yes,
|
|
2891
|
+
output=output,
|
|
2892
|
+
)
|
|
2893
|
+
|
|
2894
|
+
|
|
2895
|
+
@gen_searches.command("list")
|
|
2896
|
+
def gen_services_searches_index(
|
|
2897
|
+
ctx: typer.Context,
|
|
2898
|
+
sort: str | None = _SORT_OPT,
|
|
2899
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2900
|
+
) -> None:
|
|
2901
|
+
"""List searches."""
|
|
2902
|
+
|
|
2903
|
+
_generated_call(
|
|
2904
|
+
ctx,
|
|
2905
|
+
"GET",
|
|
2906
|
+
"/services/searches",
|
|
2907
|
+
{},
|
|
2908
|
+
sort=sort,
|
|
2909
|
+
output=output,
|
|
2910
|
+
)
|
|
2911
|
+
|
|
2912
|
+
|
|
2913
|
+
@gen_searches.command("show")
|
|
2914
|
+
def gen_services_searches_show(
|
|
2915
|
+
ctx: typer.Context,
|
|
2916
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2917
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2918
|
+
) -> None:
|
|
2919
|
+
"""Show searches details."""
|
|
2920
|
+
|
|
2921
|
+
_generated_call(
|
|
2922
|
+
ctx,
|
|
2923
|
+
"GET",
|
|
2924
|
+
"/services/searches/{serviceId}",
|
|
2925
|
+
{"serviceId": service_id},
|
|
2926
|
+
output=output,
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
|
|
2930
|
+
@gen_servers.command("list")
|
|
2931
|
+
def gen_services_servers_index(
|
|
2932
|
+
ctx: typer.Context,
|
|
2933
|
+
sort: str | None = _SORT_OPT,
|
|
2934
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2935
|
+
) -> None:
|
|
2936
|
+
"""List servers."""
|
|
2937
|
+
|
|
2938
|
+
_generated_call(
|
|
2939
|
+
ctx,
|
|
2940
|
+
"GET",
|
|
2941
|
+
"/services/servers",
|
|
2942
|
+
{},
|
|
2943
|
+
sort=sort,
|
|
2944
|
+
output=output,
|
|
2945
|
+
)
|
|
2946
|
+
|
|
2947
|
+
|
|
2948
|
+
@gen_servers.command("show")
|
|
2949
|
+
def gen_services_servers_show(
|
|
2950
|
+
ctx: typer.Context,
|
|
2951
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2952
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2953
|
+
) -> None:
|
|
2954
|
+
"""Show servers details."""
|
|
2955
|
+
|
|
2956
|
+
_generated_call(
|
|
2957
|
+
ctx,
|
|
2958
|
+
"GET",
|
|
2959
|
+
"/services/servers/{serviceId}",
|
|
2960
|
+
{"serviceId": service_id},
|
|
2961
|
+
output=output,
|
|
2962
|
+
)
|
|
2963
|
+
|
|
2964
|
+
|
|
2965
|
+
@gen_servers.command("state")
|
|
2966
|
+
def gen_services_servers_state(
|
|
2967
|
+
ctx: typer.Context,
|
|
2968
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
2969
|
+
state: str = typer.Argument(..., metavar="STATE"),
|
|
2970
|
+
yes: bool = _YES_OPT,
|
|
2971
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2972
|
+
) -> None:
|
|
2973
|
+
"""Change the state of a server."""
|
|
2974
|
+
|
|
2975
|
+
_generated_call(
|
|
2976
|
+
ctx,
|
|
2977
|
+
"GET",
|
|
2978
|
+
"/services/servers/{serviceId}/state/{state}",
|
|
2979
|
+
{"serviceId": service_id, "state": state},
|
|
2980
|
+
confirm="State servers",
|
|
2981
|
+
yes=yes,
|
|
2982
|
+
output=output,
|
|
2983
|
+
)
|
|
2984
|
+
|
|
2985
|
+
|
|
2986
|
+
@gen_sites.command("list")
|
|
2987
|
+
def gen_services_sites_index(
|
|
2988
|
+
ctx: typer.Context,
|
|
2989
|
+
sort: str | None = _SORT_OPT,
|
|
2990
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
2991
|
+
) -> None:
|
|
2992
|
+
"""List sites."""
|
|
2993
|
+
|
|
2994
|
+
_generated_call(
|
|
2995
|
+
ctx,
|
|
2996
|
+
"GET",
|
|
2997
|
+
"/services/sites",
|
|
2998
|
+
{},
|
|
2999
|
+
sort=sort,
|
|
3000
|
+
output=output,
|
|
3001
|
+
)
|
|
3002
|
+
|
|
3003
|
+
|
|
3004
|
+
@gen_sites.command("show")
|
|
3005
|
+
def gen_services_sites_show(
|
|
3006
|
+
ctx: typer.Context,
|
|
3007
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3008
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3009
|
+
) -> None:
|
|
3010
|
+
"""Show sites details."""
|
|
3011
|
+
|
|
3012
|
+
_generated_call(
|
|
3013
|
+
ctx,
|
|
3014
|
+
"GET",
|
|
3015
|
+
"/services/sites/{serviceId}",
|
|
3016
|
+
{"serviceId": service_id},
|
|
3017
|
+
output=output,
|
|
3018
|
+
)
|
|
3019
|
+
|
|
3020
|
+
|
|
3021
|
+
@gen_spams_clusters.command("list")
|
|
3022
|
+
def gen_services_spams_clusters_index(
|
|
3023
|
+
ctx: typer.Context,
|
|
3024
|
+
sort: str | None = _SORT_OPT,
|
|
3025
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3026
|
+
) -> None:
|
|
3027
|
+
"""List spams clusters."""
|
|
3028
|
+
|
|
3029
|
+
_generated_call(
|
|
3030
|
+
ctx,
|
|
3031
|
+
"GET",
|
|
3032
|
+
"/services/spams/clusters",
|
|
3033
|
+
{},
|
|
3034
|
+
sort=sort,
|
|
3035
|
+
output=output,
|
|
3036
|
+
)
|
|
3037
|
+
|
|
3038
|
+
|
|
3039
|
+
@gen_spams_clusters.command("show")
|
|
3040
|
+
def gen_services_spams_clusters_show(
|
|
3041
|
+
ctx: typer.Context,
|
|
3042
|
+
cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
|
|
3043
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3044
|
+
) -> None:
|
|
3045
|
+
"""Show spams clusters details."""
|
|
3046
|
+
|
|
3047
|
+
_generated_call(
|
|
3048
|
+
ctx,
|
|
3049
|
+
"GET",
|
|
3050
|
+
"/services/spams/clusters/{clusterId}",
|
|
3051
|
+
{"clusterId": cluster_id},
|
|
3052
|
+
output=output,
|
|
3053
|
+
)
|
|
3054
|
+
|
|
3055
|
+
|
|
3056
|
+
@gen_spams_domain.command("create")
|
|
3057
|
+
def gen_services_spams_domain_create(
|
|
3058
|
+
ctx: typer.Context,
|
|
3059
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3060
|
+
domain: str | None = typer.Option(None),
|
|
3061
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3062
|
+
) -> None:
|
|
3063
|
+
"""Create spams domain."""
|
|
3064
|
+
|
|
3065
|
+
_generated_call(
|
|
3066
|
+
ctx,
|
|
3067
|
+
"POST",
|
|
3068
|
+
"/services/spams/{serviceId}/domains",
|
|
3069
|
+
{"serviceId": service_id},
|
|
3070
|
+
body={"domain": domain},
|
|
3071
|
+
output=output,
|
|
3072
|
+
)
|
|
3073
|
+
|
|
3074
|
+
|
|
3075
|
+
@gen_spams_domain.command("delete")
|
|
3076
|
+
def gen_services_spams_domain_delete(
|
|
3077
|
+
ctx: typer.Context,
|
|
3078
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3079
|
+
domain: str = typer.Argument(..., metavar="DOMAIN"),
|
|
3080
|
+
yes: bool = _YES_OPT,
|
|
3081
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3082
|
+
) -> None:
|
|
3083
|
+
"""Delete spams domain."""
|
|
3084
|
+
|
|
3085
|
+
_generated_call(
|
|
3086
|
+
ctx,
|
|
3087
|
+
"DELETE",
|
|
3088
|
+
"/services/spams/{serviceId}/domains/{domain}",
|
|
3089
|
+
{"serviceId": service_id, "domain": domain},
|
|
3090
|
+
confirm="Delete spams domain",
|
|
3091
|
+
yes=yes,
|
|
3092
|
+
output=output,
|
|
3093
|
+
)
|
|
3094
|
+
|
|
3095
|
+
|
|
3096
|
+
@gen_spams_domain_dkim.command("disable")
|
|
3097
|
+
def gen_services_spams_domain_dkim_disable(
|
|
3098
|
+
ctx: typer.Context,
|
|
3099
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3100
|
+
domain: str = typer.Argument(..., metavar="DOMAIN"),
|
|
3101
|
+
yes: bool = _YES_OPT,
|
|
3102
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3103
|
+
) -> None:
|
|
3104
|
+
"""Disable DKIM for a spam filter domain."""
|
|
3105
|
+
|
|
3106
|
+
_generated_call(
|
|
3107
|
+
ctx,
|
|
3108
|
+
"GET",
|
|
3109
|
+
"/services/spams/{serviceId}/domains/{domain}/dkim/disable",
|
|
3110
|
+
{"serviceId": service_id, "domain": domain},
|
|
3111
|
+
confirm="Disable spams domain dkim",
|
|
3112
|
+
yes=yes,
|
|
3113
|
+
output=output,
|
|
3114
|
+
)
|
|
3115
|
+
|
|
3116
|
+
|
|
3117
|
+
@gen_spams_domain_dkim.command("enable")
|
|
3118
|
+
def gen_services_spams_domain_dkim_enable(
|
|
3119
|
+
ctx: typer.Context,
|
|
3120
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3121
|
+
domain: str = typer.Argument(..., metavar="DOMAIN"),
|
|
3122
|
+
yes: bool = _YES_OPT,
|
|
3123
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3124
|
+
) -> None:
|
|
3125
|
+
"""Enable DKIM for a spam filter domain."""
|
|
3126
|
+
|
|
3127
|
+
_generated_call(
|
|
3128
|
+
ctx,
|
|
3129
|
+
"GET",
|
|
3130
|
+
"/services/spams/{serviceId}/domains/{domain}/dkim/enable",
|
|
3131
|
+
{"serviceId": service_id, "domain": domain},
|
|
3132
|
+
confirm="Enable spams domain dkim",
|
|
3133
|
+
yes=yes,
|
|
3134
|
+
output=output,
|
|
3135
|
+
)
|
|
3136
|
+
|
|
3137
|
+
|
|
3138
|
+
@gen_spams_domain.command("list")
|
|
3139
|
+
def gen_services_spams_domain_index(
|
|
3140
|
+
ctx: typer.Context,
|
|
3141
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3142
|
+
sort: str | None = _SORT_OPT,
|
|
3143
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3144
|
+
) -> None:
|
|
3145
|
+
"""List spams domain."""
|
|
3146
|
+
|
|
3147
|
+
_generated_call(
|
|
3148
|
+
ctx,
|
|
3149
|
+
"GET",
|
|
3150
|
+
"/services/spams/{serviceId}/domains",
|
|
3151
|
+
{"serviceId": service_id},
|
|
3152
|
+
sort=sort,
|
|
3153
|
+
output=output,
|
|
3154
|
+
)
|
|
3155
|
+
|
|
3156
|
+
|
|
3157
|
+
@gen_spams_domain.command("show")
|
|
3158
|
+
def gen_services_spams_domain_show(
|
|
3159
|
+
ctx: typer.Context,
|
|
3160
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3161
|
+
domain: str = typer.Argument(..., metavar="DOMAIN"),
|
|
3162
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3163
|
+
) -> None:
|
|
3164
|
+
"""Show spams domain details."""
|
|
3165
|
+
|
|
3166
|
+
_generated_call(
|
|
3167
|
+
ctx,
|
|
3168
|
+
"GET",
|
|
3169
|
+
"/services/spams/{serviceId}/domains/{domain}",
|
|
3170
|
+
{"serviceId": service_id, "domain": domain},
|
|
3171
|
+
output=output,
|
|
3172
|
+
)
|
|
3173
|
+
|
|
3174
|
+
|
|
3175
|
+
@gen_spams.command("list")
|
|
3176
|
+
def gen_services_spams_index(
|
|
3177
|
+
ctx: typer.Context,
|
|
3178
|
+
sort: str | None = _SORT_OPT,
|
|
3179
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3180
|
+
) -> None:
|
|
3181
|
+
"""List spams."""
|
|
3182
|
+
|
|
3183
|
+
_generated_call(
|
|
3184
|
+
ctx,
|
|
3185
|
+
"GET",
|
|
3186
|
+
"/services/spams",
|
|
3187
|
+
{},
|
|
3188
|
+
sort=sort,
|
|
3189
|
+
output=output,
|
|
3190
|
+
)
|
|
3191
|
+
|
|
3192
|
+
|
|
3193
|
+
@gen_spams.command("show")
|
|
3194
|
+
def gen_services_spams_show(
|
|
3195
|
+
ctx: typer.Context,
|
|
3196
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3197
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3198
|
+
) -> None:
|
|
3199
|
+
"""Show spams details."""
|
|
3200
|
+
|
|
3201
|
+
_generated_call(
|
|
3202
|
+
ctx,
|
|
3203
|
+
"GET",
|
|
3204
|
+
"/services/spams/{serviceId}",
|
|
3205
|
+
{"serviceId": service_id},
|
|
3206
|
+
output=output,
|
|
3207
|
+
)
|
|
3208
|
+
|
|
3209
|
+
|
|
3210
|
+
@gen_spams_transports.command("create")
|
|
3211
|
+
def gen_services_spams_transports_create(
|
|
3212
|
+
ctx: typer.Context,
|
|
3213
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3214
|
+
domain: str | None = typer.Option(None),
|
|
3215
|
+
host: str | None = typer.Option(None),
|
|
3216
|
+
port: int | None = typer.Option(None),
|
|
3217
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3218
|
+
) -> None:
|
|
3219
|
+
"""Create spams transports."""
|
|
3220
|
+
|
|
3221
|
+
_generated_call(
|
|
3222
|
+
ctx,
|
|
3223
|
+
"POST",
|
|
3224
|
+
"/services/spams/{serviceId}/transports",
|
|
3225
|
+
{"serviceId": service_id},
|
|
3226
|
+
body={"domain": domain, "host": host, "port": port},
|
|
3227
|
+
output=output,
|
|
3228
|
+
)
|
|
3229
|
+
|
|
3230
|
+
|
|
3231
|
+
@gen_spams_transports.command("delete")
|
|
3232
|
+
def gen_services_spams_transports_delete(
|
|
3233
|
+
ctx: typer.Context,
|
|
3234
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3235
|
+
domain: str = typer.Argument(..., metavar="DOMAIN"),
|
|
3236
|
+
yes: bool = _YES_OPT,
|
|
3237
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3238
|
+
) -> None:
|
|
3239
|
+
"""Delete spams transports."""
|
|
3240
|
+
|
|
3241
|
+
_generated_call(
|
|
3242
|
+
ctx,
|
|
3243
|
+
"DELETE",
|
|
3244
|
+
"/services/spams/{serviceId}/transports/{domain}",
|
|
3245
|
+
{"serviceId": service_id, "domain": domain},
|
|
3246
|
+
confirm="Delete spams transports",
|
|
3247
|
+
yes=yes,
|
|
3248
|
+
output=output,
|
|
3249
|
+
)
|
|
3250
|
+
|
|
3251
|
+
|
|
3252
|
+
@gen_spams_transports.command("list")
|
|
3253
|
+
def gen_services_spams_transports_index(
|
|
3254
|
+
ctx: typer.Context,
|
|
3255
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3256
|
+
sort: str | None = _SORT_OPT,
|
|
3257
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3258
|
+
) -> None:
|
|
3259
|
+
"""List spams transports."""
|
|
3260
|
+
|
|
3261
|
+
_generated_call(
|
|
3262
|
+
ctx,
|
|
3263
|
+
"GET",
|
|
3264
|
+
"/services/spams/{serviceId}/transports",
|
|
3265
|
+
{"serviceId": service_id},
|
|
3266
|
+
sort=sort,
|
|
3267
|
+
output=output,
|
|
3268
|
+
)
|
|
3269
|
+
|
|
3270
|
+
|
|
3271
|
+
@gen_spams_transports.command("show")
|
|
3272
|
+
def gen_services_spams_transports_show(
|
|
3273
|
+
ctx: typer.Context,
|
|
3274
|
+
service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
|
|
3275
|
+
domain: str = typer.Argument(..., metavar="DOMAIN"),
|
|
3276
|
+
output: OutputFormat | None = _OUTPUT_OPT,
|
|
3277
|
+
) -> None:
|
|
3278
|
+
"""Show spams transports details."""
|
|
3279
|
+
|
|
3280
|
+
_generated_call(
|
|
3281
|
+
ctx,
|
|
3282
|
+
"GET",
|
|
3283
|
+
"/services/spams/{serviceId}/transports/{domain}",
|
|
3284
|
+
{"serviceId": service_id, "domain": domain},
|
|
3285
|
+
output=output,
|
|
3286
|
+
)
|
|
3287
|
+
|
|
3288
|
+
|
|
3289
|
+
# --- END GENERATED COMMANDS ---
|
|
3290
|
+
|
|
3291
|
+
|
|
3292
|
+
def run() -> None:
|
|
3293
|
+
app()
|
|
3294
|
+
|
|
3295
|
+
|
|
3296
|
+
if __name__ == "__main__":
|
|
3297
|
+
run()
|