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.
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/PKG-INFO +1 -1
- impreza_cli-0.4.0/impreza_cli/commands/dedicated.py +466 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/main.py +2 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/PKG-INFO +1 -1
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/SOURCES.txt +1 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/README.md +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/__init__.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/__init__.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/_helpers.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/account.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/catalog.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/context.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/doctor.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/domain.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/invoice.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/key.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/orders.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/services.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/vps.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/vps_cloud.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/vps_proxmox.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/commands/webhooks.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/config.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/output.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/sdk.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli/state.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/dependency_links.txt +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/entry_points.txt +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/requires.txt +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/impreza_cli.egg-info/top_level.txt +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/pyproject.toml +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/setup.cfg +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/.gitkeep +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/__init__.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/conftest.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_account_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_account_topup_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_catalog_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_config.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_context_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_doctor_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_domain_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_invoice_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_key_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_orders_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_2_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_3_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_4_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_5_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_2_6_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_1_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_2_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_3_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_4_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_5_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_6_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_phase_3_7_smoke.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_services_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_vps_cloud_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_vps_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_vps_proxmox_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_webhooks_commands.py +0 -0
- {impreza_cli-0.3.2 → impreza_cli-0.4.0}/tests/test_yaml_output.py +0 -0
|
@@ -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")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|