impreza-cli 0.3.1__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.1 → impreza_cli-0.4.0}/PKG-INFO +3 -3
  2. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/README.md +2 -2
  3. impreza_cli-0.4.0/impreza_cli/commands/dedicated.py +466 -0
  4. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/vps_cloud.py +30 -123
  5. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/main.py +2 -0
  6. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli.egg-info/PKG-INFO +3 -3
  7. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli.egg-info/SOURCES.txt +1 -0
  8. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_5_smoke.py +18 -68
  9. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_vps_cloud_commands.py +14 -57
  10. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/__init__.py +0 -0
  11. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/__init__.py +0 -0
  12. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/_helpers.py +0 -0
  13. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/account.py +0 -0
  14. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/catalog.py +0 -0
  15. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/context.py +0 -0
  16. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/doctor.py +0 -0
  17. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/domain.py +0 -0
  18. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/invoice.py +0 -0
  19. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/key.py +0 -0
  20. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/orders.py +0 -0
  21. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/services.py +0 -0
  22. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/vps.py +0 -0
  23. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/vps_proxmox.py +0 -0
  24. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/commands/webhooks.py +0 -0
  25. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/config.py +0 -0
  26. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/output.py +0 -0
  27. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/sdk.py +0 -0
  28. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli/state.py +0 -0
  29. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli.egg-info/dependency_links.txt +0 -0
  30. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli.egg-info/entry_points.txt +0 -0
  31. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli.egg-info/requires.txt +0 -0
  32. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/impreza_cli.egg-info/top_level.txt +0 -0
  33. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/pyproject.toml +0 -0
  34. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/setup.cfg +0 -0
  35. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/.gitkeep +0 -0
  36. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/__init__.py +0 -0
  37. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/conftest.py +0 -0
  38. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_account_commands.py +0 -0
  39. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_account_topup_commands.py +0 -0
  40. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_catalog_commands.py +0 -0
  41. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_config.py +0 -0
  42. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_context_commands.py +0 -0
  43. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_doctor_commands.py +0 -0
  44. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_domain_commands.py +0 -0
  45. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_invoice_commands.py +0 -0
  46. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_key_commands.py +0 -0
  47. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_orders_commands.py +0 -0
  48. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_2_2_smoke.py +0 -0
  49. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_2_3_smoke.py +0 -0
  50. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_2_4_smoke.py +0 -0
  51. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_2_5_smoke.py +0 -0
  52. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_2_6_smoke.py +0 -0
  53. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_1_smoke.py +0 -0
  54. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_2_smoke.py +0 -0
  55. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_3_smoke.py +0 -0
  56. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_4_smoke.py +0 -0
  57. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_6_smoke.py +0 -0
  58. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_phase_3_7_smoke.py +0 -0
  59. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_services_commands.py +0 -0
  60. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_vps_commands.py +0 -0
  61. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_vps_proxmox_commands.py +0 -0
  62. {impreza_cli-0.3.1 → impreza_cli-0.4.0}/tests/test_webhooks_commands.py +0 -0
  63. {impreza_cli-0.3.1 → 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.1
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
@@ -142,10 +142,10 @@ to see the full subcommand list, or `impreza <group> <command>
142
142
  | `account` | `info / balance / services / topup / topup-status` | Profile + balance + services + crypto top-up |
143
143
  | `catalog` | `products / product / product-groups / tlds` | Pre-purchase discovery |
144
144
  | `domain` | `show / check / pricing / register / transfer / set-nameservers / lock / unlock / id-protection / raa-verify / gdpr-auth / transfer-approval` + `domain dns list / add / update / delete / activate` | Domain registrations + full DNS CRUD |
145
- | `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / rdns / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
145
+ | `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
146
146
  | `order` | `list / show / create / upgrade` | Submit / browse product orders |
147
147
  | `service` | `cancel` | Submit cancellation request (any service) |
148
- | `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history |
148
+ | `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history. Event-payload contract documented in [`../openapi/asyncapi.yaml`](../openapi/asyncapi.yaml). |
149
149
  | `invoice` | `list / show` | Invoices with line items + transactions |
150
150
  | `key` | `whoami` | Active API key identity + IP whitelist |
151
151
 
@@ -101,10 +101,10 @@ to see the full subcommand list, or `impreza <group> <command>
101
101
  | `account` | `info / balance / services / topup / topup-status` | Profile + balance + services + crypto top-up |
102
102
  | `catalog` | `products / product / product-groups / tlds` | Pre-purchase discovery |
103
103
  | `domain` | `show / check / pricing / register / transfer / set-nameservers / lock / unlock / id-protection / raa-verify / gdpr-auth / transfer-approval` + `domain dns list / add / update / delete / activate` | Domain registrations + full DNS CRUD |
104
- | `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / rdns / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
104
+ | `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
105
105
  | `order` | `list / show / create / upgrade` | Submit / browse product orders |
106
106
  | `service` | `cancel` | Submit cancellation request (any service) |
107
- | `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history |
107
+ | `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history. Event-payload contract documented in [`../openapi/asyncapi.yaml`](../openapi/asyncapi.yaml). |
108
108
  | `invoice` | `list / show` | Invoices with line items + transactions |
109
109
  | `key` | `whoami` | Active API key identity + IP whitelist |
110
110
 
@@ -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)
@@ -3,7 +3,7 @@
3
3
  Cloud sub-resources mounted on the main ``vps`` Typer
4
4
  app as the ``cloud`` namespace. Mirror of the 3.4 Proxmox layout.
5
5
 
6
- Five nested sub-apps + five inline verbs at the cloud root:
6
+ Four nested sub-apps + five inline verbs at the cloud root:
7
7
 
8
8
  * ``vps cloud images`` — ``list / create / restore / delete``
9
9
  Account-scoped on the wire (the Cloud image catalog is
@@ -15,15 +15,22 @@ Five nested sub-apps + five inline verbs at the cloud root:
15
15
 
16
16
  * ``vps cloud iso`` — ``mount / unmount``
17
17
 
18
- * ``vps cloud rdns`` — ``get / set / delete``
19
- Account-scoped on the API side (``/vps/cloud/rdns/{ip}``); the
20
- SDK exposes it via the bound VPS for ergonomics. ``<ip>`` selects
21
- the record within the account.
22
-
23
18
  * ``vps cloud ssh-keys`` — ``list / assign``
24
19
  ``list`` returns every key on the Cloud account; ``assign``
25
20
  attaches one or more existing keys to the bound VPS.
26
21
 
22
+ * ``vps cloud rdns`` (``get / set / delete``) was previously here
23
+ but is **removed from the CLI surface in 7.7**. The
24
+ ``/vps/cloud/rdns/{ip}`` endpoint hits a WAF/mod_security rule
25
+ on the public edge (dotted-IPv4 path segments are flagged as
26
+ suspicious) and returns the WAF maintenance HTML page instead
27
+ of a JSON response. Server-side fix is owned by whoever
28
+ configures the WAF rules; until that lands, the CLI hides the
29
+ broken verbs rather than ship a known-broken surface. The SDK
30
+ resource (``vps.rdns.get/set/delete``) stays intact so library
31
+ consumers can still call it and handle the WAF response
32
+ themselves.
33
+
27
34
  Inline verbs (no sub-app):
28
35
 
29
36
  * ``vps cloud vnc <id>`` — read the VNC client triple (host, port,
@@ -65,7 +72,7 @@ from ._helpers import exit_on_api_error, resolve_vps_or_exit
65
72
 
66
73
  app = typer.Typer(
67
74
  name="cloud",
68
- help="Cloud-only sub-resources: images, rescue, iso, rdns, ssh-keys, etc.",
75
+ help="Cloud-only sub-resources: images, rescue, iso, ssh-keys, etc.",
69
76
  no_args_is_help=True,
70
77
  )
71
78
 
@@ -84,11 +91,6 @@ iso_app = typer.Typer(
84
91
  help="Mount / unmount an ISO on a Cloud VPS.",
85
92
  no_args_is_help=True,
86
93
  )
87
- rdns_app = typer.Typer(
88
- name="rdns",
89
- help="Reverse-DNS records for Cloud account IPs.",
90
- no_args_is_help=True,
91
- )
92
94
  ssh_keys_app = typer.Typer(
93
95
  name="ssh-keys",
94
96
  help="List account-level SSH keys and assign to a Cloud VPS.",
@@ -100,10 +102,11 @@ ipv6_app = typer.Typer(
100
102
  no_args_is_help=True,
101
103
  )
102
104
 
105
+ # rdns sub-app removed in 7.7 (WAF-blocked endpoint). SDK resource
106
+ # `vps.rdns.get/set/delete` stays available for library consumers.
103
107
  app.add_typer(images_app)
104
108
  app.add_typer(rescue_app)
105
109
  app.add_typer(iso_app)
106
- app.add_typer(rdns_app)
107
110
  app.add_typer(ssh_keys_app)
108
111
  app.add_typer(ipv6_app)
109
112
 
@@ -448,117 +451,21 @@ def iso_unmount(
448
451
 
449
452
 
450
453
  # ══════════════════════════════════════════════════════════════════════
451
- # rDNS
454
+ # rDNS — removed from CLI surface in 7.7
452
455
  # ══════════════════════════════════════════════════════════════════════
453
-
454
-
455
- @rdns_app.command("get")
456
- def rdns_get(
457
- typer_ctx: typer.Context,
458
- service_id: int = typer.Argument(
459
- ...,
460
- help=(
461
- "Service id of any Cloud VPS on the account (rDNS is "
462
- "account-scoped; this id is only used to confirm the account)."
463
- ),
464
- ),
465
- ip: str = typer.Argument(..., help="IP address whose rDNS to read."),
466
- output: OutputFormat | None = typer.Option(
467
- None, "--output", "-o",
468
- help="Output format. Overrides the global --output flag.",
469
- case_sensitive=False,
470
- ),
471
- ) -> None:
472
- """Read the rDNS record for an IP.
473
-
474
- Wraps ``vps.rdns.get(ip)``. The API path is account-scoped
475
- (``GET /vps/cloud/rdns/{ip}``).
476
- """
477
- state = from_typer_context(typer_ctx)
478
- fmt = resolve_output(state, output)
479
- with make_client_or_exit(state) as client:
480
- vps = resolve_vps_or_exit(client, service_id)
481
- try:
482
- result = vps.rdns.get(ip)
483
- except BackendNotSupported:
484
- _cloud_only_exit("rdns")
485
- return
486
- except ApiError as exc:
487
- exit_on_api_error(exc)
488
- return
489
-
490
- if not result:
491
- typer.echo(f"No rDNS record for {ip}.")
492
- return
493
- print_dict(f"rDNS — {ip}", {str(k): v for k, v in result.items()}, fmt=fmt)
494
-
495
-
496
- @rdns_app.command("set")
497
- def rdns_set(
498
- typer_ctx: typer.Context,
499
- service_id: int = typer.Argument(
500
- ...,
501
- help="Service id of any Cloud VPS on the account (see `rdns get`).",
502
- ),
503
- ip: str = typer.Argument(..., help="IP address to set rDNS on."),
504
- hostname: str = typer.Argument(..., help="Hostname to point the rDNS PTR at."),
505
- ) -> None:
506
- """Set the rDNS PTR record for an IP.
507
-
508
- Wraps ``vps.rdns.set(ip, hostname)``. Some upstreams reject
509
- hostnames that don't resolve forward to the IP — those return
510
- a 4xx and the SDK surfaces it as :class:`InvalidRequest`.
511
- """
512
- state = from_typer_context(typer_ctx)
513
- with make_client_or_exit(state) as client:
514
- vps = resolve_vps_or_exit(client, service_id)
515
- try:
516
- vps.rdns.set(ip, hostname)
517
- except BackendNotSupported:
518
- _cloud_only_exit("rdns")
519
- return
520
- except ApiError as exc:
521
- exit_on_api_error(exc)
522
- return
523
- success(f"rDNS for {ip} set to {hostname!r}.")
524
-
525
-
526
- @rdns_app.command("delete")
527
- def rdns_delete(
528
- typer_ctx: typer.Context,
529
- service_id: int = typer.Argument(
530
- ...,
531
- help="Service id of any Cloud VPS on the account (see `rdns get`).",
532
- ),
533
- ip: str = typer.Argument(..., help="IP address whose rDNS to clear."),
534
- yes: bool = typer.Option(
535
- False, "--yes", "-y", help="Skip the deletion confirmation prompt."
536
- ),
537
- ) -> None:
538
- """Delete the rDNS record for an IP.
539
-
540
- Wraps ``vps.rdns.delete(ip)``. After deletion, reverse-DNS
541
- lookups on the IP return whatever default the upstream
542
- configures.
543
- """
544
- state = from_typer_context(typer_ctx)
545
- confirm_or_exit(
546
- f"Removing rDNS for {ip} reverts reverse-DNS lookups to the "
547
- "upstream default. Mail servers and rate-limiters relying on "
548
- "the PTR may behave differently.",
549
- yes=yes,
550
- )
551
- with make_client_or_exit(state) as client:
552
- vps = resolve_vps_or_exit(client, service_id)
553
- try:
554
- vps.rdns.delete(ip)
555
- except BackendNotSupported:
556
- _cloud_only_exit("rdns")
557
- return
558
- except ApiError as exc:
559
- exit_on_api_error(exc)
560
- return
561
- success(f"rDNS removed for {ip}.")
456
+ #
457
+ # `rdns_app` + the get/set/delete commands lived here through 0.3.1.
458
+ # The /vps/cloud/rdns/{ip} endpoint hits a WAF rule on the public
459
+ # edge (dotted-IPv4 path segments) and returns HTML instead of JSON,
460
+ # so the verbs are unusable end-to-end. SDK methods stay available
461
+ # (`client.vps.get(id).rdns.get/set/delete`) for library consumers
462
+ # who want to handle the WAF response themselves.
463
+ #
464
+ # Re-add the typer app + commands here when the server-side WAF rule
465
+ # is fixed. The shape was three commands matching `vps.rdns.{get,
466
+ # set, delete}` with a service-id positional + `<ip>` (+ `<hostname>`
467
+ # for set) — see git history at tag `cli-v0.3.1` for the original
468
+ # code if you need to restore it.
562
469
 
563
470
 
564
471
  # ══════════════════════════════════════════════════════════════════════
@@ -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.1
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
@@ -142,10 +142,10 @@ to see the full subcommand list, or `impreza <group> <command>
142
142
  | `account` | `info / balance / services / topup / topup-status` | Profile + balance + services + crypto top-up |
143
143
  | `catalog` | `products / product / product-groups / tlds` | Pre-purchase discovery |
144
144
  | `domain` | `show / check / pricing / register / transfer / set-nameservers / lock / unlock / id-protection / raa-verify / gdpr-auth / transfer-approval` + `domain dns list / add / update / delete / activate` | Domain registrations + full DNS CRUD |
145
- | `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / rdns / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
145
+ | `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
146
146
  | `order` | `list / show / create / upgrade` | Submit / browse product orders |
147
147
  | `service` | `cancel` | Submit cancellation request (any service) |
148
- | `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history |
148
+ | `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history. Event-payload contract documented in [`../openapi/asyncapi.yaml`](../openapi/asyncapi.yaml). |
149
149
  | `invoice` | `list / show` | Invoices with line items + transactions |
150
150
  | `key` | `whoami` | Active API key identity + IP whitelist |
151
151
 
@@ -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
@@ -8,24 +8,25 @@ Bucketed by reversibility, the same way 3.3 / 3.4 are:
8
8
  smokes so a casual ``pytest`` run never touches the live API.
9
9
  * **Catastrophic** (images create/restore/delete, resize,
10
10
  boot-order, vnc-password rotate, rescue enable, iso mount,
11
- rdns set/delete, ipv6 enable): real state changes — some
12
- reversible (boot-order, rescue, iso) and some that consume
13
- storage quota (images create) or have billing impact
14
- (resize). Mock-only by design.
11
+ ipv6 enable): real state changes — some reversible
12
+ (boot-order, rescue, iso) and some that consume storage quota
13
+ (images create) or have billing impact (resize). Mock-only by
14
+ design.
15
15
 
16
16
  The cheap round-trips that 3.3's set-hostname / 3.4's snapshots
17
- create+delete enabled don't have direct equivalents here:
18
-
19
- - rdns get is safe to read but requires knowing a valid IP
20
- (the VPS's dedicated_ip from `vps show`). Could be done as
21
- a round-trip (`set sentinel → get → delete-or-restore`)
22
- but the upstream may reject hostnames that don't resolve
23
- forward, so the test would need to construct a resolvable
24
- sentinel. Deferred for now.
17
+ create+delete enabled don't have a direct equivalent here:
18
+
25
19
  - vnc read returns short-lived credentials; testing them
26
20
  would require an actual VNC client. Just exercise the
27
21
  request-response.
28
22
 
23
+ The earlier ``vps cloud rdns`` smoke (read for the VPS's
24
+ dedicated_ip) was removed in 7.7 along with the CLI verbs —
25
+ the public-edge WAF rejects /vps/cloud/rdns/{ip} paths and
26
+ returns HTML instead of JSON. SDK-level unit tests for the
27
+ underlying ``vps.rdns`` resource still live in
28
+ sdk-python/tests/test_vps_specific_unit.py.
29
+
29
30
  If we add reversible round-trips later, gate them behind
30
31
  extra opt-in flags (``IMPREZA_TEST_ALLOW_RDNS_CHANGE=1`` etc.),
31
32
  following the 3.3 ``IMPREZA_TEST_ALLOW_PASSWORD_RESET`` pattern.
@@ -175,59 +176,8 @@ def test_smoke_vnc_read(live_seeded_config: Path) -> None:
175
176
  )
176
177
 
177
178
 
178
- # ── rdns: read for the VPS's own dedicated IP ───────────────────────
179
-
180
-
181
- @pytest.mark.skipif(
182
- os.environ.get("IMPREZA_DESTRUCTIVE_TESTS") != "1",
183
- reason="reads from a real account",
184
- )
185
- def test_smoke_rdns_get_dedicated_ip(live_seeded_config: Path) -> None:
186
- """Best-effort: look up the VPS's dedicated_ip via `vps show`,
187
- then read its rDNS. Skips cleanly if the VPS doesn't have a
188
- dedicated_ip exposed (some flavours don't).
189
- """
190
- vps_id = _test_cloud_vps_id()
191
-
192
- show_result = runner.invoke(
193
- app, ["vps", "show", str(vps_id), "--output", "json"]
194
- )
195
- if show_result.exit_code != 0:
196
- pytest.fail(f"vps show failed: {show_result.stderr.strip()!r}")
197
- info = json.loads(show_result.stdout)
198
- dedicated_ip = info.get("dedicated_ip")
199
- if not dedicated_ip:
200
- pytest.skip(
201
- f"VPS {vps_id} has no dedicated_ip in its Service record "
202
- "— rDNS read needs a specific IP target"
203
- )
204
-
205
- result = runner.invoke(
206
- app,
207
- ["vps", "cloud", "rdns", "get", str(vps_id), dedicated_ip,
208
- "--output", "json"],
209
- )
210
- if result.exit_code != 0:
211
- if "Proxmox backend" in result.stderr:
212
- pytest.skip(f"VPS {vps_id} is on Proxmox — rdns is Cloud-only")
213
- # rDNS-not-set is a legitimate state; the SDK returns an empty
214
- # dict and the CLI prints "No rDNS record" on stdout. Treat
215
- # any non-zero exit other than that as a failure.
216
- if "No rDNS record" in result.stdout:
217
- print(f"\n VPS {vps_id}: no rDNS record for {dedicated_ip}")
218
- return
219
- # the Cloud backend sometimes returns 502 instead of 404 when the
220
- # rDNS record isn't set or the IP isn't in their managed pool.
221
- # Treat as skip rather than failure — the integration plumbing
222
- # is covered by unit tests with respx mocks.
223
- if "502" in result.stderr or "UPSTREAM_ERROR" in result.stderr:
224
- pytest.skip(
225
- f"Cloud backend rejected rDNS read for {dedicated_ip} "
226
- f"(upstream 502): {result.stderr.strip()!r}"
227
- )
228
- pytest.fail(f"rdns get failed: {result.stderr.strip()!r}")
229
- if result.stdout.strip().startswith("{"):
230
- parsed = json.loads(result.stdout)
231
- print(f"\n VPS {vps_id}: rDNS for {dedicated_ip} = {parsed}")
232
- else:
233
- print(f"\n VPS {vps_id}: rDNS for {dedicated_ip} read OK (empty)")
179
+ # rdns smoke test removed in 7.7. The `vps cloud rdns get` verb is
180
+ # no longer exposed by the CLI (WAF-blocked endpoint — the public
181
+ # edge returns the maintenance HTML page for /vps/cloud/rdns/{ip}
182
+ # paths with dotted-IPv4 segments). Re-add when the server-side WAF
183
+ # rule is fixed and `rdns_app` returns to commands/vps_cloud.py.
@@ -1,17 +1,19 @@
1
- """Unit tests for ``impreza vps cloud`` (Phase 3.5).
1
+ """Unit tests for ``impreza vps cloud`` (Phase 3.5; 7.7 trimmed rdns).
2
2
 
3
- Mirrors the 3.4 Proxmox unit suite — covers the five nested
4
- sub-apps (images, rescue, iso, rdns, ssh-keys) plus the five
5
- inline verbs at the cloud root (vnc, vnc-password, resize,
6
- boot-order, ipv6 enable).
3
+ Mirrors the 3.4 Proxmox unit suite — covers the four nested
4
+ sub-apps (images, rescue, iso, ssh-keys) plus the five inline
5
+ verbs at the cloud root (vnc, vnc-password, resize, boot-order,
6
+ ipv6 enable). The fifth sub-app (rdns) was removed from the CLI
7
+ surface in 7.7 (WAF-blocked endpoint); SDK-level tests for
8
+ `vps.rdns.get/set/delete` stay in
9
+ sdk-python/tests/test_vps_specific_unit.py.
7
10
 
8
11
  For each verb:
9
12
 
10
13
  * happy-path test pins the URL + body
11
14
  * a Proxmox-backend test asserts the friendly "Cloud-only" stderr
12
15
  line (the inverse of 3.4's BackendNotSupported handling)
13
- * destructive verbs (delete, restore, rdns delete) get a
14
- decline-at-prompt test
16
+ * destructive verbs (delete, restore) get a decline-at-prompt test
15
17
  * boot-order has a client-side validation test (unknown --order
16
18
  rejected before any HTTP)
17
19
  """
@@ -299,56 +301,11 @@ def test_iso_unmount(seeded_config: Path) -> None:
299
301
  # ══════════════════════════════════════════════════════════════════════
300
302
 
301
303
 
302
- @respx.mock
303
- def test_rdns_get(seeded_config: Path) -> None:
304
- respx.get(f"{BASE}/account/services/17987").mock(
305
- return_value=httpx.Response(200, json=_service_envelope(_cloud_service()))
306
- )
307
- route = respx.get(f"{BASE}/vps/cloud/rdns/1.2.3.4").mock(
308
- return_value=httpx.Response(
309
- 200, json=_ok({"ip": "1.2.3.4", "hostname": "mail.example.com"})
310
- )
311
- )
312
- result = runner.invoke(
313
- app, ["vps", "cloud", "rdns", "get", "17987", "1.2.3.4"]
314
- )
315
- assert result.exit_code == 0, result.stderr
316
- assert route.called
317
- assert "mail.example.com" in result.stdout
318
-
319
-
320
- @respx.mock
321
- def test_rdns_set(seeded_config: Path) -> None:
322
- respx.get(f"{BASE}/account/services/17987").mock(
323
- return_value=httpx.Response(200, json=_service_envelope(_cloud_service()))
324
- )
325
- route = respx.put(f"{BASE}/vps/cloud/rdns/1.2.3.4").mock(
326
- return_value=httpx.Response(200, json=_ok())
327
- )
328
- result = runner.invoke(
329
- app,
330
- ["vps", "cloud", "rdns", "set", "17987", "1.2.3.4", "mail.example.com"],
331
- )
332
- assert result.exit_code == 0, result.stderr
333
- assert route.called
334
- body = json.loads(route.calls.last.request.content)
335
- assert body == {"hostname": "mail.example.com"}
336
-
337
-
338
- @respx.mock
339
- def test_rdns_delete_with_yes(seeded_config: Path) -> None:
340
- respx.get(f"{BASE}/account/services/17987").mock(
341
- return_value=httpx.Response(200, json=_service_envelope(_cloud_service()))
342
- )
343
- route = respx.delete(f"{BASE}/vps/cloud/rdns/1.2.3.4").mock(
344
- return_value=httpx.Response(200, json=_ok())
345
- )
346
- result = runner.invoke(
347
- app,
348
- ["vps", "cloud", "rdns", "delete", "17987", "1.2.3.4", "--yes"],
349
- )
350
- assert result.exit_code == 0, result.stderr
351
- assert route.called
304
+ # rdns CLI tests removed in 7.7. The `vps cloud rdns` sub-app is no
305
+ # longer registered (WAF-blocked endpoint); SDK-level tests for
306
+ # `vps.rdns.get/set/delete` still live in
307
+ # sdk-python/tests/test_vps_specific_unit.py for library consumers
308
+ # who route around the CLI surface.
352
309
 
353
310
 
354
311
  # ══════════════════════════════════════════════════════════════════════
File without changes
File without changes
File without changes