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