impreza-cli 0.3.2__tar.gz → 0.4.0__tar.gz

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.
Files changed (63) hide show
  1. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/PKG-INFO +1 -1
  2. impreza_cli-0.4.0/impreza_cli/commands/dedicated.py +466 -0
  3. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/main.py +2 -0
  4. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/PKG-INFO +1 -1
  5. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/SOURCES.txt +1 -0
  6. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/README.md +0 -0
  7. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/__init__.py +0 -0
  8. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/__init__.py +0 -0
  9. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/_helpers.py +0 -0
  10. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/account.py +0 -0
  11. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/catalog.py +0 -0
  12. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/context.py +0 -0
  13. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/doctor.py +0 -0
  14. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/domain.py +0 -0
  15. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/invoice.py +0 -0
  16. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/key.py +0 -0
  17. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/orders.py +0 -0
  18. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/services.py +0 -0
  19. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/vps.py +0 -0
  20. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/vps_cloud.py +0 -0
  21. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/vps_proxmox.py +0 -0
  22. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/webhooks.py +0 -0
  23. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/config.py +0 -0
  24. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/output.py +0 -0
  25. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/sdk.py +0 -0
  26. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/state.py +0 -0
  27. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/dependency_links.txt +0 -0
  28. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/entry_points.txt +0 -0
  29. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/requires.txt +0 -0
  30. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/top_level.txt +0 -0
  31. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/pyproject.toml +0 -0
  32. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/setup.cfg +0 -0
  33. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/.gitkeep +0 -0
  34. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/__init__.py +0 -0
  35. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/conftest.py +0 -0
  36. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_account_commands.py +0 -0
  37. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_account_topup_commands.py +0 -0
  38. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_catalog_commands.py +0 -0
  39. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_config.py +0 -0
  40. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_context_commands.py +0 -0
  41. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_doctor_commands.py +0 -0
  42. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_domain_commands.py +0 -0
  43. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_invoice_commands.py +0 -0
  44. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_key_commands.py +0 -0
  45. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_orders_commands.py +0 -0
  46. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_2_smoke.py +0 -0
  47. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_3_smoke.py +0 -0
  48. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_4_smoke.py +0 -0
  49. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_5_smoke.py +0 -0
  50. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_6_smoke.py +0 -0
  51. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_1_smoke.py +0 -0
  52. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_2_smoke.py +0 -0
  53. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_3_smoke.py +0 -0
  54. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_4_smoke.py +0 -0
  55. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_5_smoke.py +0 -0
  56. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_6_smoke.py +0 -0
  57. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_7_smoke.py +0 -0
  58. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_services_commands.py +0 -0
  59. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_vps_cloud_commands.py +0 -0
  60. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_vps_commands.py +0 -0
  61. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_vps_proxmox_commands.py +0 -0
  62. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_webhooks_commands.py +0 -0
  63. {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_yaml_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: impreza-cli
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Official command-line interface for the Impreza Host public REST API
5
5
  Author-email: Impreza Host <support@imprezahost.com>
6
6
  License: MIT
@@ -0,0 +1,466 @@
1
+ """``impreza dedicated`` subcommand surface — Phase 8.
2
+
3
+ Twenty verbs over the :class:`~impreza.DedicatedResource` shipped in
4
+ the SDK alongside this CLI. The public ``/dedicated/*`` namespace is
5
+ vendor-agnostic: operations are gated by per-service capabilities,
6
+ and capability-gated endpoints return ``NOT_SUPPORTED`` when the
7
+ underlying service doesn't advertise the feature. Always inspect
8
+ ``dedicated capabilities <id>`` first when scripting.
9
+
10
+ Discovery & state:
11
+ * ``list`` / ``show <id>`` / ``capabilities <id>`` / ``status <id>``
12
+ * ``ips <id>`` — IPs with current PTR
13
+ * ``os-images <id>`` — OS images available for reinstall
14
+
15
+ Power:
16
+ * ``start <id>`` / ``shutdown <id>`` / ``reboot <id>``
17
+
18
+ rDNS:
19
+ * ``set-rdns <id> --ip --hostname``
20
+ * ``reset-rdns <id>``
21
+
22
+ KVM / IPMI:
23
+ * ``kvm <id>`` — current access info
24
+ * ``enable-kvm <id>`` — the SDK / server inject the caller's public
25
+ IP as the session-binding IP when the service needs it.
26
+ * ``disable-kvm <id>``
27
+
28
+ Firewall (requires ``firewall`` capability):
29
+ * ``firewall <id>`` / ``ddos-logs <id>``
30
+ * ``set-firewall <id> --ip --state --sensitivity``
31
+
32
+ Bandwidth (requires ``bandwidth`` capability):
33
+ * ``bandwidth <id> --type --scale``
34
+
35
+ VPN credentials (requires ``vpn`` capability):
36
+ * ``vpn <id>``
37
+
38
+ Reinstall (destructive — wipes ALL data):
39
+ * ``reinstall <id> --os-id --password --confirm [--yes]``
40
+ Requires both the SDK's ``confirm=True`` flag and the
41
+ ``X-Impreza-Confirm: WIPE`` header (the SDK injects the header
42
+ when ``confirm=True``). The CLI also prompts on a TTY unless
43
+ ``--yes`` is passed.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from typing import Any
49
+
50
+ import typer
51
+ from impreza.exceptions import ApiError
52
+
53
+ from ..output import OutputFormat, error, info, print_dict, print_table, success
54
+ from ..sdk import make_client_or_exit
55
+ from ..state import confirm_or_exit, from_typer_context, resolve_output
56
+ from ._helpers import exit_on_api_error
57
+
58
+ app = typer.Typer(
59
+ name="dedicated",
60
+ help="Manage dedicated servers. Operations gated by per-service capabilities.",
61
+ no_args_is_help=True,
62
+ )
63
+
64
+
65
+ # ── helpers ─────────────────────────────────────────────────────────
66
+
67
+
68
+ def _emit(value: Any, *, ctx: typer.Context) -> None:
69
+ """Render a heterogeneous payload (dict, list[dict], or scalar).
70
+
71
+ The /dedicated/* payloads vary by service — there's no single
72
+ typed projection that fits every endpoint. Tables turn into flat
73
+ key=value dumps; JSON / YAML pass the structure through.
74
+ """
75
+ state = from_typer_context(ctx)
76
+ fmt = resolve_output(state, override=None)
77
+ if fmt is OutputFormat.TABLE and isinstance(value, dict):
78
+ print_dict(value)
79
+ elif fmt is OutputFormat.TABLE and isinstance(value, list) and value and isinstance(value[0], dict):
80
+ # Use the first row's keys as the column order so the table
81
+ # is stable across runs.
82
+ columns = list(value[0].keys())
83
+ print_table(columns, value)
84
+ else:
85
+ print_dict(value) if isinstance(value, dict) else typer.echo(_serialize(value, fmt))
86
+
87
+
88
+ def _serialize(value: Any, fmt: OutputFormat) -> str:
89
+ """Format a non-dict value for JSON / YAML output."""
90
+ import json
91
+
92
+ if fmt is OutputFormat.YAML:
93
+ try:
94
+ import yaml # type: ignore[import-not-found]
95
+
96
+ return yaml.safe_dump(value, sort_keys=False).rstrip()
97
+ except ImportError: # pragma: no cover
98
+ return json.dumps(value, indent=2, ensure_ascii=False)
99
+ return json.dumps(value, indent=2, ensure_ascii=False)
100
+
101
+
102
+ # ── discovery ──────────────────────────────────────────────────────
103
+
104
+
105
+ @app.command("list")
106
+ def cmd_list(ctx: typer.Context) -> None:
107
+ """List every dedicated server on the account."""
108
+ client = make_client_or_exit(ctx)
109
+ try:
110
+ items = client.dedicated.list()
111
+ except ApiError as exc:
112
+ exit_on_api_error(exc)
113
+ return
114
+ if not items:
115
+ info("No dedicated servers on this account.")
116
+ return
117
+ fmt = resolve_output(from_typer_context(ctx), override=None)
118
+ if fmt is OutputFormat.TABLE:
119
+ columns = ["service_id", "domain", "ip", "status", "capabilities"]
120
+ rows = [
121
+ {
122
+ "service_id": item.get("service_id"),
123
+ "domain": item.get("domain", ""),
124
+ "ip": item.get("ip", ""),
125
+ "status": item.get("status", ""),
126
+ "capabilities": ",".join(item.get("capabilities") or []),
127
+ }
128
+ for item in items
129
+ ]
130
+ print_table(columns, rows)
131
+ else:
132
+ _emit(items, ctx=ctx)
133
+
134
+
135
+ @app.command("show")
136
+ def cmd_show(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
137
+ """Show full details for a dedicated server."""
138
+ client = make_client_or_exit(ctx)
139
+ try:
140
+ data = client.dedicated.info(service_id)
141
+ except ApiError as exc:
142
+ exit_on_api_error(exc)
143
+ return
144
+ _emit(data, ctx=ctx)
145
+
146
+
147
+ @app.command("capabilities")
148
+ def cmd_capabilities(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
149
+ """Show the capability list advertised by a dedicated server.
150
+
151
+ Call this before any capability-gated sub-command — firewall,
152
+ bandwidth, vpn, kvm, reinstall, power — so you don't waste a
153
+ round-trip on a NOT_SUPPORTED response.
154
+ """
155
+ client = make_client_or_exit(ctx)
156
+ try:
157
+ data = client.dedicated.capabilities(service_id)
158
+ except ApiError as exc:
159
+ exit_on_api_error(exc)
160
+ return
161
+ _emit(data, ctx=ctx)
162
+
163
+
164
+ @app.command("status")
165
+ def cmd_status(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
166
+ """Show current power / provisioning state."""
167
+ client = make_client_or_exit(ctx)
168
+ try:
169
+ data = client.dedicated.status(service_id)
170
+ except ApiError as exc:
171
+ exit_on_api_error(exc)
172
+ return
173
+ _emit(data, ctx=ctx)
174
+
175
+
176
+ @app.command("ips")
177
+ def cmd_ips(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
178
+ """List the IPs assigned to the server with current PTR."""
179
+ client = make_client_or_exit(ctx)
180
+ try:
181
+ data = client.dedicated.ips(service_id)
182
+ except ApiError as exc:
183
+ exit_on_api_error(exc)
184
+ return
185
+ _emit(data, ctx=ctx)
186
+
187
+
188
+ @app.command("os-images")
189
+ def cmd_os_images(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
190
+ """List OS images available for reinstall."""
191
+ client = make_client_or_exit(ctx)
192
+ try:
193
+ data = client.dedicated.os_images(service_id)
194
+ except ApiError as exc:
195
+ exit_on_api_error(exc)
196
+ return
197
+ _emit(data, ctx=ctx)
198
+
199
+
200
+ # ── power ──────────────────────────────────────────────────────────
201
+
202
+
203
+ @app.command("start")
204
+ def cmd_start(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
205
+ """Power on a dedicated server."""
206
+ client = make_client_or_exit(ctx)
207
+ try:
208
+ client.dedicated.start(service_id)
209
+ except ApiError as exc:
210
+ exit_on_api_error(exc)
211
+ return
212
+ success(f"Dedicated server {service_id} start signal sent.")
213
+
214
+
215
+ @app.command("shutdown")
216
+ def cmd_shutdown(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
217
+ """Graceful shutdown."""
218
+ client = make_client_or_exit(ctx)
219
+ try:
220
+ client.dedicated.shutdown(service_id)
221
+ except ApiError as exc:
222
+ exit_on_api_error(exc)
223
+ return
224
+ success(f"Dedicated server {service_id} shutdown signal sent.")
225
+
226
+
227
+ @app.command("reboot")
228
+ def cmd_reboot(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
229
+ """Reboot."""
230
+ client = make_client_or_exit(ctx)
231
+ try:
232
+ client.dedicated.reboot(service_id)
233
+ except ApiError as exc:
234
+ exit_on_api_error(exc)
235
+ return
236
+ success(f"Dedicated server {service_id} reboot signal sent.")
237
+
238
+
239
+ # ── rDNS ───────────────────────────────────────────────────────────
240
+
241
+
242
+ @app.command("set-rdns")
243
+ def cmd_set_rdns(
244
+ ctx: typer.Context,
245
+ service_id: int = typer.Argument(...),
246
+ ip: str = typer.Option(..., "--ip", help="IP whose PTR you want to set."),
247
+ hostname: str = typer.Option(..., "--hostname", help="New PTR value."),
248
+ ) -> None:
249
+ """Set the PTR for a single IP on a dedicated server.
250
+
251
+ Applied synchronously on most services. On services without an
252
+ automated rDNS path the response is ``{status: queued, ...}`` and
253
+ an operator on our side completes it within a few hours.
254
+ """
255
+ client = make_client_or_exit(ctx)
256
+ try:
257
+ data = client.dedicated.set_rdns(service_id, ip, hostname)
258
+ except ApiError as exc:
259
+ exit_on_api_error(exc)
260
+ return
261
+ _emit(data, ctx=ctx)
262
+
263
+
264
+ @app.command("reset-rdns")
265
+ def cmd_reset_rdns(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
266
+ """Reset every PTR back to the Impreza default (impreza.host pattern)."""
267
+ client = make_client_or_exit(ctx)
268
+ try:
269
+ data = client.dedicated.reset_rdns(service_id)
270
+ except ApiError as exc:
271
+ exit_on_api_error(exc)
272
+ return
273
+ _emit(data, ctx=ctx)
274
+
275
+
276
+ # ── KVM ────────────────────────────────────────────────────────────
277
+
278
+
279
+ @app.command("kvm")
280
+ def cmd_kvm(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
281
+ """Show current KVM / IPMI access info."""
282
+ client = make_client_or_exit(ctx)
283
+ try:
284
+ data = client.dedicated.kvm(service_id)
285
+ except ApiError as exc:
286
+ exit_on_api_error(exc)
287
+ return
288
+ _emit(data, ctx=ctx)
289
+
290
+
291
+ @app.command("enable-kvm")
292
+ def cmd_enable_kvm(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
293
+ """Open a KVM/IPMI session.
294
+
295
+ The caller's public IP is injected as the session-binding IP
296
+ automatically when the service needs it — nothing for you to
297
+ pass.
298
+ """
299
+ client = make_client_or_exit(ctx)
300
+ try:
301
+ data = client.dedicated.enable_kvm(service_id)
302
+ except ApiError as exc:
303
+ exit_on_api_error(exc)
304
+ return
305
+ _emit(data, ctx=ctx)
306
+
307
+
308
+ @app.command("disable-kvm")
309
+ def cmd_disable_kvm(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
310
+ """Close the active KVM/IPMI session."""
311
+ client = make_client_or_exit(ctx)
312
+ try:
313
+ client.dedicated.disable_kvm(service_id)
314
+ except ApiError as exc:
315
+ exit_on_api_error(exc)
316
+ return
317
+ success(f"KVM session for service {service_id} disabled.")
318
+
319
+
320
+ # ── Firewall (capability-gated) ────────────────────────────────────
321
+
322
+
323
+ @app.command("firewall")
324
+ def cmd_firewall(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
325
+ """Show DDoS firewall state. Requires the ``firewall`` capability."""
326
+ client = make_client_or_exit(ctx)
327
+ try:
328
+ data = client.dedicated.firewall(service_id)
329
+ except ApiError as exc:
330
+ exit_on_api_error(exc)
331
+ return
332
+ _emit(data, ctx=ctx)
333
+
334
+
335
+ @app.command("ddos-logs")
336
+ def cmd_ddos_logs(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
337
+ """Show DDoS attack logs. Requires the ``firewall`` capability."""
338
+ client = make_client_or_exit(ctx)
339
+ try:
340
+ data = client.dedicated.ddos_logs(service_id)
341
+ except ApiError as exc:
342
+ exit_on_api_error(exc)
343
+ return
344
+ _emit(data, ctx=ctx)
345
+
346
+
347
+ @app.command("set-firewall")
348
+ def cmd_set_firewall(
349
+ ctx: typer.Context,
350
+ service_id: int = typer.Argument(...),
351
+ ip: str = typer.Option(..., "--ip", help="IP to update."),
352
+ state: str | None = typer.Option(
353
+ None,
354
+ "--state",
355
+ help="always_on | redirect_on_attack (omit to leave unchanged).",
356
+ ),
357
+ sensitivity: str | None = typer.Option(
358
+ None,
359
+ "--sensitivity",
360
+ help="low | normal | medium | high (omit to leave unchanged).",
361
+ ),
362
+ ) -> None:
363
+ """Update DDoS firewall state/sensitivity for an IP. Requires the ``firewall`` capability."""
364
+ client = make_client_or_exit(ctx)
365
+ try:
366
+ data = client.dedicated.set_firewall(
367
+ service_id, ip=ip, state=state, sensitivity=sensitivity
368
+ )
369
+ except ApiError as exc:
370
+ exit_on_api_error(exc)
371
+ return
372
+ _emit(data, ctx=ctx)
373
+
374
+
375
+ # ── Bandwidth (capability-gated) ───────────────────────────────────
376
+
377
+
378
+ @app.command("bandwidth")
379
+ def cmd_bandwidth(
380
+ ctx: typer.Context,
381
+ service_id: int = typer.Argument(...),
382
+ type: str = typer.Option(
383
+ "port_bits",
384
+ "--type",
385
+ help="port_bits | port_upkts | port_percent | port_errors | port_pktsize | port_discards",
386
+ ),
387
+ scale: str = typer.Option("month", "--scale", help="day | week | month"),
388
+ ) -> None:
389
+ """Bandwidth graph (PNG base64). Requires the ``bandwidth`` capability."""
390
+ client = make_client_or_exit(ctx)
391
+ try:
392
+ data = client.dedicated.bandwidth(service_id, type=type, scale=scale)
393
+ except ApiError as exc:
394
+ exit_on_api_error(exc)
395
+ return
396
+ _emit(data, ctx=ctx)
397
+
398
+
399
+ # ── VPN (capability-gated) ─────────────────────────────────────────
400
+
401
+
402
+ @app.command("vpn")
403
+ def cmd_vpn(ctx: typer.Context, service_id: int = typer.Argument(...)) -> None:
404
+ """Show rotating VPN credentials. Requires the ``vpn`` capability."""
405
+ client = make_client_or_exit(ctx)
406
+ try:
407
+ data = client.dedicated.vpn(service_id)
408
+ except ApiError as exc:
409
+ exit_on_api_error(exc)
410
+ return
411
+ _emit(data, ctx=ctx)
412
+
413
+
414
+ # ── Reinstall (destructive) ────────────────────────────────────────
415
+
416
+
417
+ @app.command("reinstall")
418
+ def cmd_reinstall(
419
+ ctx: typer.Context,
420
+ service_id: int = typer.Argument(...),
421
+ os_id: str = typer.Option(..., "--os-id", help="OS id from `dedicated os-images <id>`."),
422
+ password: str = typer.Option(
423
+ ..., "--password", help="New root password (min 8 chars).", prompt=True, hide_input=True
424
+ ),
425
+ os_label: str | None = typer.Option(
426
+ None, "--os-label", help="Optional OS label — auto-resolved from os-id if absent."
427
+ ),
428
+ confirm: bool = typer.Option(
429
+ False,
430
+ "--confirm",
431
+ help="Acknowledge that ALL DATA will be wiped.",
432
+ ),
433
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the interactive prompt."),
434
+ ) -> None:
435
+ """Reinstall the OS. Destructive — wipes ALL data.
436
+
437
+ On services that support a synchronous reinstall path, the result
438
+ returns immediately and includes the new root password. On services
439
+ where the reinstall has to be applied manually, the response is
440
+ ``{status: queued, message: ...}`` and our team executes the
441
+ reinstall within a few hours.
442
+
443
+ Both ``--confirm`` AND the prompt (or ``--yes``) must pass. The
444
+ required ``X-Impreza-Confirm: WIPE`` header is injected by the SDK
445
+ automatically.
446
+ """
447
+ if not confirm:
448
+ error("Reinstall is destructive — pass --confirm to acknowledge data loss.")
449
+ raise typer.Exit(code=1)
450
+ confirm_or_exit(
451
+ f"Reinstall service {service_id} to os-id={os_id}? ALL DATA WILL BE LOST.",
452
+ yes=yes,
453
+ )
454
+ client = make_client_or_exit(ctx)
455
+ try:
456
+ data = client.dedicated.reinstall(
457
+ service_id,
458
+ os_id=os_id,
459
+ password=password,
460
+ confirm=True,
461
+ os_label=os_label,
462
+ )
463
+ except ApiError as exc:
464
+ exit_on_api_error(exc)
465
+ return
466
+ _emit(data, ctx=ctx)
@@ -19,6 +19,7 @@ from . import __version__
19
19
  from .commands import account as account_cmd
20
20
  from .commands import catalog as catalog_cmd
21
21
  from .commands import context as context_cmd
22
+ from .commands import dedicated as dedicated_cmd
22
23
  from .commands import doctor as doctor_cmd
23
24
  from .commands import domain as domain_cmd
24
25
  from .commands import invoice as invoice_cmd
@@ -86,6 +87,7 @@ def _root(
86
87
  app.add_typer(account_cmd.app, name="account")
87
88
  app.add_typer(catalog_cmd.app, name="catalog")
88
89
  app.add_typer(context_cmd.app, name="context")
90
+ app.add_typer(dedicated_cmd.app, name="dedicated")
89
91
  app.add_typer(doctor_cmd.app, name="doctor")
90
92
  app.add_typer(domain_cmd.app, name="domain")
91
93
  app.add_typer(invoice_cmd.app, name="invoice")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: impreza-cli
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Official command-line interface for the Impreza Host public REST API
5
5
  Author-email: Impreza Host <support@imprezahost.com>
6
6
  License: MIT
@@ -17,6 +17,7 @@ impreza_cli/commands/_helpers.py
17
17
  impreza_cli/commands/account.py
18
18
  impreza_cli/commands/catalog.py
19
19
  impreza_cli/commands/context.py
20
+ impreza_cli/commands/dedicated.py
20
21
  impreza_cli/commands/doctor.py
21
22
  impreza_cli/commands/domain.py
22
23
  impreza_cli/commands/invoice.py
File without changes
File without changes
File without changes
File without changes