sovaria-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apps/cli/README.md +147 -0
- apps/cli/__init__.py +0 -0
- apps/cli/src/__init__.py +3 -0
- apps/cli/src/commands/__init__.py +1 -0
- apps/cli/src/commands/admin.py +160 -0
- apps/cli/src/commands/api.py +48 -0
- apps/cli/src/commands/audit.py +181 -0
- apps/cli/src/commands/auth_product.py +187 -0
- apps/cli/src/commands/billing.py +513 -0
- apps/cli/src/commands/chrono.py +248 -0
- apps/cli/src/commands/context.py +99 -0
- apps/cli/src/commands/data.py +272 -0
- apps/cli/src/commands/db.py +528 -0
- apps/cli/src/commands/deploy.py +1099 -0
- apps/cli/src/commands/governance.py +491 -0
- apps/cli/src/commands/iam.py +436 -0
- apps/cli/src/commands/init_cmd.py +177 -0
- apps/cli/src/commands/keys.py +120 -0
- apps/cli/src/commands/login.py +842 -0
- apps/cli/src/commands/org.py +157 -0
- apps/cli/src/commands/policy.py +317 -0
- apps/cli/src/commands/project.py +130 -0
- apps/cli/src/commands/realtime.py +364 -0
- apps/cli/src/commands/scope.py +42 -0
- apps/cli/src/commands/shell.py +415 -0
- apps/cli/src/commands/sim.py +178 -0
- apps/cli/src/commands/storage.py +405 -0
- apps/cli/src/commands/vector.py +204 -0
- apps/cli/src/main.py +169 -0
- apps/cli/src/utils/__init__.py +1 -0
- apps/cli/src/utils/api_client.py +329 -0
- apps/cli/src/utils/auth_store.py +299 -0
- apps/cli/src/utils/config.py +185 -0
- apps/cli/src/utils/display.py +62 -0
- apps/cli/src/utils/migrations.py +69 -0
- apps/cli/src/utils/project_context.py +124 -0
- apps/cli/tests/test_auth_store_profiles.py +103 -0
- apps/cli/tests/test_cli_security.py +110 -0
- apps/cli/tests/test_cli_smoke_paths.py +279 -0
- apps/cli/tests/test_config_profiles.py +55 -0
- apps/cli/tests/test_main_startup.py +29 -0
- apps/cli/tests/test_project_and_commands.py +196 -0
- sovaria_cli-0.2.0.dist-info/METADATA +170 -0
- sovaria_cli-0.2.0.dist-info/RECORD +46 -0
- sovaria_cli-0.2.0.dist-info/WHEEL +4 -0
- sovaria_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Nexus governance plane commands (regions, tenants, databases, metrics)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
|
|
10
|
+
from apps.cli.src.utils.display import print_json
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Platform governance: regions, tenants, databases, metrics")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _gov_path(*parts: str) -> str:
|
|
16
|
+
return "/api/v1/governance/" + "/".join(quote(part, safe="") for part in parts)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("regions")
|
|
20
|
+
def list_regions(
|
|
21
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""List configured governance regions."""
|
|
24
|
+
try:
|
|
25
|
+
result = call_api(
|
|
26
|
+
method="GET",
|
|
27
|
+
path="/api/v1/governance/regions",
|
|
28
|
+
profile=profile,
|
|
29
|
+
require_scope=False,
|
|
30
|
+
)
|
|
31
|
+
except CLIRequestError as exc:
|
|
32
|
+
typer.echo(str(exc), err=True)
|
|
33
|
+
raise typer.Exit(1) from exc
|
|
34
|
+
print_json(result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("regions-available")
|
|
38
|
+
def regions_available(
|
|
39
|
+
size: str = typer.Option("micro", "--size", help="Compute size filter"),
|
|
40
|
+
plan: str | None = typer.Option(None, "--plan", help="Optional plan filter"),
|
|
41
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""List regions with available capacity for provisioning."""
|
|
44
|
+
params: dict[str, str] = {"size": size}
|
|
45
|
+
if plan:
|
|
46
|
+
params["plan"] = plan
|
|
47
|
+
try:
|
|
48
|
+
result = call_api(
|
|
49
|
+
method="GET",
|
|
50
|
+
path="/api/v1/governance/regions/available",
|
|
51
|
+
profile=profile,
|
|
52
|
+
require_scope=False,
|
|
53
|
+
params=params,
|
|
54
|
+
)
|
|
55
|
+
except CLIRequestError as exc:
|
|
56
|
+
typer.echo(str(exc), err=True)
|
|
57
|
+
raise typer.Exit(1) from exc
|
|
58
|
+
print_json(result)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("region-patch")
|
|
62
|
+
def region_patch(
|
|
63
|
+
region_id: str = typer.Argument(..., help="Region ID"),
|
|
64
|
+
label: str | None = typer.Option(None, "--label"),
|
|
65
|
+
status: str | None = typer.Option(None, "--status"),
|
|
66
|
+
is_default: bool | None = typer.Option(None, "--default/--no-default"),
|
|
67
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Patch governance region metadata."""
|
|
70
|
+
body: dict[str, object] = {}
|
|
71
|
+
if label is not None:
|
|
72
|
+
body["label"] = label
|
|
73
|
+
if status is not None:
|
|
74
|
+
body["status"] = status
|
|
75
|
+
if is_default is not None:
|
|
76
|
+
body["is_default"] = is_default
|
|
77
|
+
if not body:
|
|
78
|
+
raise typer.BadParameter("Provide at least one of --label, --status, or --default")
|
|
79
|
+
try:
|
|
80
|
+
result = call_api(
|
|
81
|
+
method="PATCH",
|
|
82
|
+
path=_gov_path("regions", region_id),
|
|
83
|
+
profile=profile,
|
|
84
|
+
require_scope=False,
|
|
85
|
+
body=body,
|
|
86
|
+
)
|
|
87
|
+
except CLIRequestError as exc:
|
|
88
|
+
typer.echo(str(exc), err=True)
|
|
89
|
+
raise typer.Exit(1) from exc
|
|
90
|
+
print_json(result)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command("tenants")
|
|
94
|
+
def list_tenants(
|
|
95
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
96
|
+
) -> None:
|
|
97
|
+
"""List registered tenants."""
|
|
98
|
+
try:
|
|
99
|
+
result = call_api(
|
|
100
|
+
method="GET",
|
|
101
|
+
path="/api/v1/governance/tenants",
|
|
102
|
+
profile=profile,
|
|
103
|
+
require_scope=False,
|
|
104
|
+
)
|
|
105
|
+
except CLIRequestError as exc:
|
|
106
|
+
typer.echo(str(exc), err=True)
|
|
107
|
+
raise typer.Exit(1) from exc
|
|
108
|
+
print_json(result)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("tenant-get")
|
|
112
|
+
def tenant_get(
|
|
113
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
114
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Fetch one tenant record."""
|
|
117
|
+
try:
|
|
118
|
+
result = call_api(
|
|
119
|
+
method="GET",
|
|
120
|
+
path=_gov_path("tenants", org_id),
|
|
121
|
+
profile=profile,
|
|
122
|
+
require_scope=False,
|
|
123
|
+
)
|
|
124
|
+
except CLIRequestError as exc:
|
|
125
|
+
typer.echo(str(exc), err=True)
|
|
126
|
+
raise typer.Exit(1) from exc
|
|
127
|
+
print_json(result)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command("tenant-status")
|
|
131
|
+
def tenant_status(
|
|
132
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
133
|
+
status: str = typer.Option(..., "--status", help="active | suspended | deleted"),
|
|
134
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Update tenant lifecycle status."""
|
|
137
|
+
try:
|
|
138
|
+
result = call_api(
|
|
139
|
+
method="POST",
|
|
140
|
+
path=_gov_path("tenants", org_id, "status"),
|
|
141
|
+
profile=profile,
|
|
142
|
+
require_scope=False,
|
|
143
|
+
body={"status": status},
|
|
144
|
+
)
|
|
145
|
+
except CLIRequestError as exc:
|
|
146
|
+
typer.echo(str(exc), err=True)
|
|
147
|
+
raise typer.Exit(1) from exc
|
|
148
|
+
print_json(result)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.command("tenant-databases")
|
|
152
|
+
def tenant_databases(
|
|
153
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
154
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
155
|
+
) -> None:
|
|
156
|
+
"""List databases provisioned for a tenant."""
|
|
157
|
+
try:
|
|
158
|
+
result = call_api(
|
|
159
|
+
method="GET",
|
|
160
|
+
path=_gov_path("tenants", org_id, "databases"),
|
|
161
|
+
profile=profile,
|
|
162
|
+
require_scope=False,
|
|
163
|
+
)
|
|
164
|
+
except CLIRequestError as exc:
|
|
165
|
+
typer.echo(str(exc), err=True)
|
|
166
|
+
raise typer.Exit(1) from exc
|
|
167
|
+
print_json(result)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.command("db-provision")
|
|
171
|
+
def db_provision(
|
|
172
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
173
|
+
project_id: str = typer.Option(..., "--project", help="Project ID to provision"),
|
|
174
|
+
tier: str = typer.Option("dedicated", "--tier"),
|
|
175
|
+
compute_size: str | None = typer.Option(None, "--compute-size"),
|
|
176
|
+
region: str | None = typer.Option(None, "--region"),
|
|
177
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Provision a dedicated database for a tenant project."""
|
|
180
|
+
body: dict[str, object] = {"project_id": project_id, "tier": tier}
|
|
181
|
+
if compute_size:
|
|
182
|
+
body["compute_size"] = compute_size
|
|
183
|
+
if region:
|
|
184
|
+
body["region"] = region
|
|
185
|
+
try:
|
|
186
|
+
result = call_api(
|
|
187
|
+
method="POST",
|
|
188
|
+
path=_gov_path("tenants", org_id, "databases", "provision"),
|
|
189
|
+
profile=profile,
|
|
190
|
+
require_scope=False,
|
|
191
|
+
body=body,
|
|
192
|
+
)
|
|
193
|
+
except CLIRequestError as exc:
|
|
194
|
+
typer.echo(str(exc), err=True)
|
|
195
|
+
raise typer.Exit(1) from exc
|
|
196
|
+
print_json(result)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command("db-connection")
|
|
200
|
+
def db_connection(
|
|
201
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
202
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
203
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Fetch database connection metadata (password redacted)."""
|
|
206
|
+
try:
|
|
207
|
+
result = call_api(
|
|
208
|
+
method="GET",
|
|
209
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "connection"),
|
|
210
|
+
profile=profile,
|
|
211
|
+
require_scope=False,
|
|
212
|
+
)
|
|
213
|
+
except CLIRequestError as exc:
|
|
214
|
+
typer.echo(str(exc), err=True)
|
|
215
|
+
raise typer.Exit(1) from exc
|
|
216
|
+
print_json(result)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@app.command("db-connection-reveal")
|
|
220
|
+
def db_connection_reveal(
|
|
221
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
222
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
223
|
+
grant_id: str = typer.Option(..., "--grant-id", help="Active JIT grant ID"),
|
|
224
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Reveal database password using an active JIT grant."""
|
|
227
|
+
try:
|
|
228
|
+
result = call_api(
|
|
229
|
+
method="POST",
|
|
230
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "connection", "reveal"),
|
|
231
|
+
profile=profile,
|
|
232
|
+
require_scope=False,
|
|
233
|
+
body={"grant_id": grant_id},
|
|
234
|
+
)
|
|
235
|
+
except CLIRequestError as exc:
|
|
236
|
+
typer.echo(str(exc), err=True)
|
|
237
|
+
raise typer.Exit(1) from exc
|
|
238
|
+
print_json(result)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.command("db-pause")
|
|
242
|
+
def db_pause(
|
|
243
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
244
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
245
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Pause a tenant database."""
|
|
248
|
+
try:
|
|
249
|
+
result = call_api(
|
|
250
|
+
method="POST",
|
|
251
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "pause"),
|
|
252
|
+
profile=profile,
|
|
253
|
+
require_scope=False,
|
|
254
|
+
body={},
|
|
255
|
+
)
|
|
256
|
+
except CLIRequestError as exc:
|
|
257
|
+
typer.echo(str(exc), err=True)
|
|
258
|
+
raise typer.Exit(1) from exc
|
|
259
|
+
print_json(result)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@app.command("db-resume")
|
|
263
|
+
def db_resume(
|
|
264
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
265
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
266
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Resume a paused tenant database."""
|
|
269
|
+
try:
|
|
270
|
+
result = call_api(
|
|
271
|
+
method="POST",
|
|
272
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "resume"),
|
|
273
|
+
profile=profile,
|
|
274
|
+
require_scope=False,
|
|
275
|
+
body={},
|
|
276
|
+
)
|
|
277
|
+
except CLIRequestError as exc:
|
|
278
|
+
typer.echo(str(exc), err=True)
|
|
279
|
+
raise typer.Exit(1) from exc
|
|
280
|
+
print_json(result)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@app.command("db-resize")
|
|
284
|
+
def db_resize(
|
|
285
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
286
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
287
|
+
compute_size: str = typer.Option(..., "--compute-size", help="Target compute size"),
|
|
288
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Resize a tenant database compute profile."""
|
|
291
|
+
try:
|
|
292
|
+
result = call_api(
|
|
293
|
+
method="POST",
|
|
294
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "resize"),
|
|
295
|
+
profile=profile,
|
|
296
|
+
require_scope=False,
|
|
297
|
+
body={"compute_size": compute_size},
|
|
298
|
+
)
|
|
299
|
+
except CLIRequestError as exc:
|
|
300
|
+
typer.echo(str(exc), err=True)
|
|
301
|
+
raise typer.Exit(1) from exc
|
|
302
|
+
print_json(result)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@app.command("db-backups")
|
|
306
|
+
def db_backups(
|
|
307
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
308
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
309
|
+
limit: int = typer.Option(5, "--limit", min=1, max=100),
|
|
310
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
311
|
+
) -> None:
|
|
312
|
+
"""List database backups for a tenant project."""
|
|
313
|
+
try:
|
|
314
|
+
result = call_api(
|
|
315
|
+
method="GET",
|
|
316
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "backups"),
|
|
317
|
+
profile=profile,
|
|
318
|
+
require_scope=False,
|
|
319
|
+
params={"limit": str(limit)},
|
|
320
|
+
)
|
|
321
|
+
except CLIRequestError as exc:
|
|
322
|
+
typer.echo(str(exc), err=True)
|
|
323
|
+
raise typer.Exit(1) from exc
|
|
324
|
+
print_json(result)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command("db-backup-trigger")
|
|
328
|
+
def db_backup_trigger(
|
|
329
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
330
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
331
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Trigger a manual database backup."""
|
|
334
|
+
try:
|
|
335
|
+
result = call_api(
|
|
336
|
+
method="POST",
|
|
337
|
+
path=_gov_path("tenants", org_id, "databases", project_id, "backups"),
|
|
338
|
+
profile=profile,
|
|
339
|
+
require_scope=False,
|
|
340
|
+
body={},
|
|
341
|
+
)
|
|
342
|
+
except CLIRequestError as exc:
|
|
343
|
+
typer.echo(str(exc), err=True)
|
|
344
|
+
raise typer.Exit(1) from exc
|
|
345
|
+
print_json(result)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@app.command("db-delete")
|
|
349
|
+
def db_delete(
|
|
350
|
+
org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
|
|
351
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
352
|
+
snapshot: bool = typer.Option(True, "--snapshot/--no-snapshot"),
|
|
353
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
354
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Delete a tenant database (optionally after snapshot)."""
|
|
357
|
+
if not yes and not typer.confirm(f"Delete database for project '{project_id}'?"):
|
|
358
|
+
raise typer.Exit(0)
|
|
359
|
+
params = {"snapshot": "true" if snapshot else "false"}
|
|
360
|
+
try:
|
|
361
|
+
result = call_api(
|
|
362
|
+
method="DELETE",
|
|
363
|
+
path=_gov_path("tenants", org_id, "databases", project_id),
|
|
364
|
+
profile=profile,
|
|
365
|
+
require_scope=False,
|
|
366
|
+
params=params,
|
|
367
|
+
)
|
|
368
|
+
except CLIRequestError as exc:
|
|
369
|
+
typer.echo(str(exc), err=True)
|
|
370
|
+
raise typer.Exit(1) from exc
|
|
371
|
+
print_json(result)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@app.command("admin-databases")
|
|
375
|
+
def admin_databases(
|
|
376
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
377
|
+
) -> None:
|
|
378
|
+
"""List all tenant databases (platform admin)."""
|
|
379
|
+
try:
|
|
380
|
+
result = call_api(
|
|
381
|
+
method="GET",
|
|
382
|
+
path="/api/v1/governance/admin/databases",
|
|
383
|
+
profile=profile,
|
|
384
|
+
require_scope=False,
|
|
385
|
+
)
|
|
386
|
+
except CLIRequestError as exc:
|
|
387
|
+
typer.echo(str(exc), err=True)
|
|
388
|
+
raise typer.Exit(1) from exc
|
|
389
|
+
print_json(result)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@app.command("hosts")
|
|
393
|
+
def list_hosts(
|
|
394
|
+
region_id: str | None = typer.Option(None, "--region-id"),
|
|
395
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
396
|
+
) -> None:
|
|
397
|
+
"""List governance hosts."""
|
|
398
|
+
params = {"region_id": region_id} if region_id else None
|
|
399
|
+
try:
|
|
400
|
+
result = call_api(
|
|
401
|
+
method="GET",
|
|
402
|
+
path="/api/v1/governance/hosts",
|
|
403
|
+
profile=profile,
|
|
404
|
+
require_scope=False,
|
|
405
|
+
params=params,
|
|
406
|
+
)
|
|
407
|
+
except CLIRequestError as exc:
|
|
408
|
+
typer.echo(str(exc), err=True)
|
|
409
|
+
raise typer.Exit(1) from exc
|
|
410
|
+
print_json(result)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@app.command("host-patch")
|
|
414
|
+
def host_patch(
|
|
415
|
+
host_id: str = typer.Argument(..., help="Host ID"),
|
|
416
|
+
status: str | None = typer.Option(None, "--status"),
|
|
417
|
+
address: str | None = typer.Option(None, "--address"),
|
|
418
|
+
total_vcpu: float | None = typer.Option(None, "--total-vcpu"),
|
|
419
|
+
total_memory_mb: int | None = typer.Option(None, "--total-memory-mb"),
|
|
420
|
+
total_storage_gb: int | None = typer.Option(None, "--total-storage-gb"),
|
|
421
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Patch governance host metadata."""
|
|
424
|
+
body: dict[str, object] = {}
|
|
425
|
+
if status is not None:
|
|
426
|
+
body["status"] = status
|
|
427
|
+
if address is not None:
|
|
428
|
+
body["address"] = address
|
|
429
|
+
if total_vcpu is not None:
|
|
430
|
+
body["total_vcpu"] = total_vcpu
|
|
431
|
+
if total_memory_mb is not None:
|
|
432
|
+
body["total_memory_mb"] = total_memory_mb
|
|
433
|
+
if total_storage_gb is not None:
|
|
434
|
+
body["total_storage_gb"] = total_storage_gb
|
|
435
|
+
if not body:
|
|
436
|
+
raise typer.BadParameter("Provide at least one patch field")
|
|
437
|
+
try:
|
|
438
|
+
result = call_api(
|
|
439
|
+
method="PATCH",
|
|
440
|
+
path=_gov_path("hosts", host_id),
|
|
441
|
+
profile=profile,
|
|
442
|
+
require_scope=False,
|
|
443
|
+
body=body,
|
|
444
|
+
)
|
|
445
|
+
except CLIRequestError as exc:
|
|
446
|
+
typer.echo(str(exc), err=True)
|
|
447
|
+
raise typer.Exit(1) from exc
|
|
448
|
+
print_json(result)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@app.command("metrics-overview")
|
|
452
|
+
def metrics_overview(
|
|
453
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
454
|
+
) -> None:
|
|
455
|
+
"""Fetch platform metrics overview."""
|
|
456
|
+
try:
|
|
457
|
+
result = call_api(
|
|
458
|
+
method="GET",
|
|
459
|
+
path="/api/v1/governance/metrics/overview",
|
|
460
|
+
profile=profile,
|
|
461
|
+
require_scope=False,
|
|
462
|
+
)
|
|
463
|
+
except CLIRequestError as exc:
|
|
464
|
+
typer.echo(str(exc), err=True)
|
|
465
|
+
raise typer.Exit(1) from exc
|
|
466
|
+
print_json(result)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.command("project-metrics")
|
|
470
|
+
def project_metrics(
|
|
471
|
+
project_id: str = typer.Argument(..., help="Project ID"),
|
|
472
|
+
since: str = typer.Option("24", "--since", help="Hours or ISO timestamp"),
|
|
473
|
+
limit: int = typer.Option(500, "--limit", min=1, max=5000),
|
|
474
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Fetch governance metrics for one project."""
|
|
477
|
+
params: dict[str, str] = {"since": since}
|
|
478
|
+
if limit != 500:
|
|
479
|
+
params["limit"] = str(limit)
|
|
480
|
+
try:
|
|
481
|
+
result = call_api(
|
|
482
|
+
method="GET",
|
|
483
|
+
path=_gov_path("metrics", "projects", project_id),
|
|
484
|
+
profile=profile,
|
|
485
|
+
require_scope=False,
|
|
486
|
+
params=params,
|
|
487
|
+
)
|
|
488
|
+
except CLIRequestError as exc:
|
|
489
|
+
typer.echo(str(exc), err=True)
|
|
490
|
+
raise typer.Exit(1) from exc
|
|
491
|
+
print_json(result)
|