voltagegpu-cli 1.0.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.
- volt/__init__.py +6 -0
- volt/cli/__init__.py +3 -0
- volt/cli/cli.py +417 -0
- volt/sdk/__init__.py +7 -0
- volt/sdk/client.py +270 -0
- volt/sdk/config.py +62 -0
- volt/sdk/decorators.py +200 -0
- volt/sdk/exceptions.py +29 -0
- volt/sdk/models.py +132 -0
- volt/sdk/utils.py +94 -0
- voltagegpu_cli-1.0.0.dist-info/METADATA +288 -0
- voltagegpu_cli-1.0.0.dist-info/RECORD +14 -0
- voltagegpu_cli-1.0.0.dist-info/WHEEL +4 -0
- voltagegpu_cli-1.0.0.dist-info/entry_points.txt +2 -0
volt/__init__.py
ADDED
volt/cli/__init__.py
ADDED
volt/cli/cli.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""VoltageGPU CLI - Command-line interface for managing GPU cloud resources."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_client():
|
|
10
|
+
"""Get an authenticated VoltageGPU client."""
|
|
11
|
+
try:
|
|
12
|
+
from volt.sdk import VoltageGPUClient
|
|
13
|
+
return VoltageGPUClient()
|
|
14
|
+
except ValueError as e:
|
|
15
|
+
click.echo(f"Error: {e}", err=True)
|
|
16
|
+
click.echo("Set your API key: export VOLT_API_KEY=your_key", err=True)
|
|
17
|
+
click.echo("Or create ~/.volt/config.ini with [api] api_key=your_key", err=True)
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
click.echo(f"Error initializing client: {e}", err=True)
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_table(headers, rows):
|
|
25
|
+
"""Format data as a simple aligned table."""
|
|
26
|
+
if not rows:
|
|
27
|
+
click.echo("No results found.")
|
|
28
|
+
return
|
|
29
|
+
col_widths = [len(h) for h in headers]
|
|
30
|
+
for row in rows:
|
|
31
|
+
for i, cell in enumerate(row):
|
|
32
|
+
col_widths[i] = max(col_widths[i], len(str(cell)))
|
|
33
|
+
header_line = " ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers))
|
|
34
|
+
separator = " ".join("-" * w for w in col_widths)
|
|
35
|
+
click.echo(header_line)
|
|
36
|
+
click.echo(separator)
|
|
37
|
+
for row in rows:
|
|
38
|
+
click.echo(" ".join(str(cell).ljust(col_widths[i]) for i, cell in enumerate(row)))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group(invoke_without_command=True)
|
|
42
|
+
@click.version_option(version="1.0.0", prog_name="volt")
|
|
43
|
+
@click.pass_context
|
|
44
|
+
def cli(ctx):
|
|
45
|
+
"""VoltageGPU CLI - Manage GPU cloud resources from your terminal.
|
|
46
|
+
|
|
47
|
+
Deploy, manage, and scale GPU pods with ease.
|
|
48
|
+
Get started: https://docs.voltagegpu.com/cli
|
|
49
|
+
"""
|
|
50
|
+
if ctx.invoked_subcommand is None:
|
|
51
|
+
click.echo(ctx.get_help())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ==================== PODS ====================
|
|
55
|
+
|
|
56
|
+
@cli.group()
|
|
57
|
+
def pods():
|
|
58
|
+
"""Manage GPU pods."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pods.command("list")
|
|
63
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
64
|
+
def pods_list(as_json):
|
|
65
|
+
"""List all your pods."""
|
|
66
|
+
client = get_client()
|
|
67
|
+
try:
|
|
68
|
+
pods_data = client.list_pods()
|
|
69
|
+
if as_json:
|
|
70
|
+
click.echo(json.dumps([{
|
|
71
|
+
"id": p.id, "name": p.name, "status": p.status,
|
|
72
|
+
"gpu_type": p.gpu_type, "gpu_count": p.gpu_count,
|
|
73
|
+
"hourly_price": p.hourly_price
|
|
74
|
+
} for p in pods_data], indent=2))
|
|
75
|
+
else:
|
|
76
|
+
rows = [(p.id[:12] + "...", p.name, p.status, p.gpu_type,
|
|
77
|
+
str(p.gpu_count), f"${p.hourly_price:.2f}/hr") for p in pods_data]
|
|
78
|
+
format_table(["ID", "NAME", "STATUS", "GPU", "COUNT", "PRICE"], rows)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
click.echo(f"Error: {e}", err=True)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
finally:
|
|
83
|
+
client.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pods.command("get")
|
|
87
|
+
@click.argument("pod_id")
|
|
88
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
89
|
+
def pods_get(pod_id, as_json):
|
|
90
|
+
"""Get details of a specific pod."""
|
|
91
|
+
client = get_client()
|
|
92
|
+
try:
|
|
93
|
+
pod = client.get_pod(pod_id)
|
|
94
|
+
if as_json:
|
|
95
|
+
click.echo(json.dumps({
|
|
96
|
+
"id": pod.id, "name": pod.name, "status": pod.status,
|
|
97
|
+
"gpu_type": pod.gpu_type, "gpu_count": pod.gpu_count,
|
|
98
|
+
"hourly_price": pod.hourly_price, "ssh_host": pod.ssh_host,
|
|
99
|
+
"ssh_port": pod.ssh_port, "template_id": pod.template_id,
|
|
100
|
+
"created_at": pod.created_at
|
|
101
|
+
}, indent=2))
|
|
102
|
+
else:
|
|
103
|
+
click.echo(f"Pod: {pod.name}")
|
|
104
|
+
click.echo(f" ID: {pod.id}")
|
|
105
|
+
click.echo(f" Status: {pod.status}")
|
|
106
|
+
click.echo(f" GPU: {pod.gpu_count}x {pod.gpu_type}")
|
|
107
|
+
click.echo(f" Price: ${pod.hourly_price:.2f}/hr")
|
|
108
|
+
if pod.ssh_host:
|
|
109
|
+
click.echo(f" SSH: ssh root@{pod.ssh_host} -p {pod.ssh_port or 22}")
|
|
110
|
+
if pod.template_id:
|
|
111
|
+
click.echo(f" Template: {pod.template_id}")
|
|
112
|
+
if pod.created_at:
|
|
113
|
+
click.echo(f" Created: {pod.created_at}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
click.echo(f"Error: {e}", err=True)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
finally:
|
|
118
|
+
client.close()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pods.command("create")
|
|
122
|
+
@click.option("--template", "template_id", required=True, help="Template ID to use")
|
|
123
|
+
@click.option("--name", required=True, help="Name for the pod")
|
|
124
|
+
@click.option("--gpu-count", default=1, type=int, help="Number of GPUs (default: 1)")
|
|
125
|
+
@click.option("--ssh-key", "ssh_key_ids", multiple=True, help="SSH key ID(s) to attach")
|
|
126
|
+
def pods_create(template_id, name, gpu_count, ssh_key_ids):
|
|
127
|
+
"""Deploy a new GPU pod."""
|
|
128
|
+
client = get_client()
|
|
129
|
+
try:
|
|
130
|
+
pod = client.create_pod(
|
|
131
|
+
template_id=template_id,
|
|
132
|
+
name=name,
|
|
133
|
+
gpu_count=gpu_count,
|
|
134
|
+
ssh_key_ids=list(ssh_key_ids) if ssh_key_ids else None
|
|
135
|
+
)
|
|
136
|
+
click.echo(f"Pod created successfully!")
|
|
137
|
+
click.echo(f" ID: {pod.id}")
|
|
138
|
+
click.echo(f" Name: {pod.name}")
|
|
139
|
+
click.echo(f" Status: {pod.status}")
|
|
140
|
+
click.echo(f" GPU: {pod.gpu_count}x {pod.gpu_type}")
|
|
141
|
+
click.echo(f" Price: ${pod.hourly_price:.2f}/hr")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
click.echo(f"Error: {e}", err=True)
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
finally:
|
|
146
|
+
client.close()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pods.command("start")
|
|
150
|
+
@click.argument("pod_id")
|
|
151
|
+
def pods_start(pod_id):
|
|
152
|
+
"""Start a stopped pod."""
|
|
153
|
+
client = get_client()
|
|
154
|
+
try:
|
|
155
|
+
pod = client.start_pod(pod_id)
|
|
156
|
+
click.echo(f"Pod {pod.name} started. Status: {pod.status}")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
click.echo(f"Error: {e}", err=True)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
finally:
|
|
161
|
+
client.close()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pods.command("stop")
|
|
165
|
+
@click.argument("pod_id")
|
|
166
|
+
def pods_stop(pod_id):
|
|
167
|
+
"""Stop a running pod."""
|
|
168
|
+
client = get_client()
|
|
169
|
+
try:
|
|
170
|
+
pod = client.stop_pod(pod_id)
|
|
171
|
+
click.echo(f"Pod {pod.name} stopped. Status: {pod.status}")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
click.echo(f"Error: {e}", err=True)
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
finally:
|
|
176
|
+
client.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pods.command("delete")
|
|
180
|
+
@click.argument("pod_id")
|
|
181
|
+
@click.option("--yes", is_flag=True, help="Skip confirmation")
|
|
182
|
+
def pods_delete(pod_id, yes):
|
|
183
|
+
"""Terminate and delete a pod permanently."""
|
|
184
|
+
if not yes:
|
|
185
|
+
click.confirm(f"Are you sure you want to delete pod {pod_id}?", abort=True)
|
|
186
|
+
client = get_client()
|
|
187
|
+
try:
|
|
188
|
+
client.delete_pod(pod_id)
|
|
189
|
+
click.echo(f"Pod {pod_id} deleted.")
|
|
190
|
+
except Exception as e:
|
|
191
|
+
click.echo(f"Error: {e}", err=True)
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
finally:
|
|
194
|
+
client.close()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pods.command("ssh")
|
|
198
|
+
@click.argument("pod_id")
|
|
199
|
+
def pods_ssh(pod_id):
|
|
200
|
+
"""Show SSH command to connect to a pod."""
|
|
201
|
+
client = get_client()
|
|
202
|
+
try:
|
|
203
|
+
pod = client.get_pod(pod_id)
|
|
204
|
+
if pod.ssh_host:
|
|
205
|
+
port = pod.ssh_port or 22
|
|
206
|
+
cmd = f"ssh root@{pod.ssh_host} -p {port}"
|
|
207
|
+
click.echo(cmd)
|
|
208
|
+
else:
|
|
209
|
+
click.echo("SSH not available for this pod.", err=True)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
click.echo(f"Error: {e}", err=True)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
finally:
|
|
215
|
+
client.close()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ==================== TEMPLATES ====================
|
|
219
|
+
|
|
220
|
+
@cli.group()
|
|
221
|
+
def templates():
|
|
222
|
+
"""Browse available pod templates."""
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@templates.command("list")
|
|
227
|
+
@click.option("--category", help="Filter by category (e.g., llm, ml, diffusion)")
|
|
228
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
229
|
+
def templates_list(category, as_json):
|
|
230
|
+
"""List available templates."""
|
|
231
|
+
client = get_client()
|
|
232
|
+
try:
|
|
233
|
+
templates_data = client.list_templates(category=category)
|
|
234
|
+
if as_json:
|
|
235
|
+
click.echo(json.dumps([{
|
|
236
|
+
"id": t.id, "name": t.name, "category": t.category,
|
|
237
|
+
"docker_image": t.docker_image
|
|
238
|
+
} for t in templates_data], indent=2))
|
|
239
|
+
else:
|
|
240
|
+
rows = [(t.id[:12] + "...", t.name, t.category or "-",
|
|
241
|
+
t.docker_image[:40] + "..." if len(t.docker_image) > 40 else t.docker_image)
|
|
242
|
+
for t in templates_data]
|
|
243
|
+
format_table(["ID", "NAME", "CATEGORY", "DOCKER IMAGE"], rows)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
click.echo(f"Error: {e}", err=True)
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
finally:
|
|
248
|
+
client.close()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@templates.command("get")
|
|
252
|
+
@click.argument("template_id")
|
|
253
|
+
def templates_get(template_id):
|
|
254
|
+
"""Get details of a specific template."""
|
|
255
|
+
client = get_client()
|
|
256
|
+
try:
|
|
257
|
+
t = client.get_template(template_id)
|
|
258
|
+
click.echo(f"Template: {t.name}")
|
|
259
|
+
click.echo(f" ID: {t.id}")
|
|
260
|
+
click.echo(f" Description: {t.description}")
|
|
261
|
+
click.echo(f" Docker Image: {t.docker_image}")
|
|
262
|
+
click.echo(f" Category: {t.category or '-'}")
|
|
263
|
+
except Exception as e:
|
|
264
|
+
click.echo(f"Error: {e}", err=True)
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
finally:
|
|
267
|
+
client.close()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ==================== SSH KEYS ====================
|
|
271
|
+
|
|
272
|
+
@cli.group("ssh-keys")
|
|
273
|
+
def ssh_keys():
|
|
274
|
+
"""Manage SSH keys."""
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@ssh_keys.command("list")
|
|
279
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
280
|
+
def ssh_keys_list(as_json):
|
|
281
|
+
"""List your SSH keys."""
|
|
282
|
+
client = get_client()
|
|
283
|
+
try:
|
|
284
|
+
keys = client.list_ssh_keys()
|
|
285
|
+
if as_json:
|
|
286
|
+
click.echo(json.dumps([{
|
|
287
|
+
"id": k.id, "name": k.name, "fingerprint": k.fingerprint,
|
|
288
|
+
"created_at": k.created_at
|
|
289
|
+
} for k in keys], indent=2))
|
|
290
|
+
else:
|
|
291
|
+
rows = [(k.id[:12] + "...", k.name, k.fingerprint or "-",
|
|
292
|
+
k.created_at or "-") for k in keys]
|
|
293
|
+
format_table(["ID", "NAME", "FINGERPRINT", "CREATED"], rows)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
click.echo(f"Error: {e}", err=True)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
finally:
|
|
298
|
+
client.close()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@ssh_keys.command("add")
|
|
302
|
+
@click.option("--name", required=True, help="Label for this key")
|
|
303
|
+
@click.option("--file", "key_file", type=click.Path(exists=True), help="Path to public key file")
|
|
304
|
+
@click.option("--key", "key_str", help="Public key string directly")
|
|
305
|
+
def ssh_keys_add(name, key_file, key_str):
|
|
306
|
+
"""Add a new SSH key."""
|
|
307
|
+
if not key_file and not key_str:
|
|
308
|
+
click.echo("Error: provide --file or --key", err=True)
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
if key_file:
|
|
311
|
+
with open(key_file) as f:
|
|
312
|
+
public_key = f.read().strip()
|
|
313
|
+
else:
|
|
314
|
+
public_key = key_str
|
|
315
|
+
client = get_client()
|
|
316
|
+
try:
|
|
317
|
+
key = client.add_ssh_key(name=name, public_key=public_key)
|
|
318
|
+
click.echo(f"SSH key added: {key.name} ({key.id})")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
click.echo(f"Error: {e}", err=True)
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
finally:
|
|
323
|
+
client.close()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@ssh_keys.command("delete")
|
|
327
|
+
@click.argument("key_id")
|
|
328
|
+
def ssh_keys_delete(key_id):
|
|
329
|
+
"""Remove an SSH key."""
|
|
330
|
+
client = get_client()
|
|
331
|
+
try:
|
|
332
|
+
client.delete_ssh_key(key_id)
|
|
333
|
+
click.echo(f"SSH key {key_id} deleted.")
|
|
334
|
+
except Exception as e:
|
|
335
|
+
click.echo(f"Error: {e}", err=True)
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
finally:
|
|
338
|
+
client.close()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ==================== MACHINES ====================
|
|
342
|
+
|
|
343
|
+
@cli.group()
|
|
344
|
+
def machines():
|
|
345
|
+
"""View available GPU hardware."""
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@machines.command("list")
|
|
350
|
+
@click.option("--gpu", help="Filter by GPU type (e.g., RTX4090, A100, H100)")
|
|
351
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
352
|
+
def machines_list(gpu, as_json):
|
|
353
|
+
"""List available GPU machines with pricing."""
|
|
354
|
+
client = get_client()
|
|
355
|
+
try:
|
|
356
|
+
machines_data = client.list_machines(gpu_type=gpu)
|
|
357
|
+
if as_json:
|
|
358
|
+
click.echo(json.dumps([{
|
|
359
|
+
"name": m.name, "hourly_price": m.hourly_price,
|
|
360
|
+
"total_gpu_count": m.total_gpu_count, "available": m.available
|
|
361
|
+
} for m in machines_data], indent=2))
|
|
362
|
+
else:
|
|
363
|
+
rows = [(m.name, str(m.total_gpu_count),
|
|
364
|
+
f"${m.hourly_price:.2f}/hr",
|
|
365
|
+
"Yes" if m.available else "No") for m in machines_data]
|
|
366
|
+
format_table(["GPU", "AVAILABLE COUNT", "PRICE", "IN STOCK"], rows)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
click.echo(f"Error: {e}", err=True)
|
|
369
|
+
sys.exit(1)
|
|
370
|
+
finally:
|
|
371
|
+
client.close()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ==================== ACCOUNT ====================
|
|
375
|
+
|
|
376
|
+
@cli.group()
|
|
377
|
+
def account():
|
|
378
|
+
"""View account details and balance."""
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@account.command("balance")
|
|
383
|
+
def account_balance():
|
|
384
|
+
"""Check your account credit balance."""
|
|
385
|
+
client = get_client()
|
|
386
|
+
try:
|
|
387
|
+
balance = client.get_balance()
|
|
388
|
+
click.echo(f"Balance: ${balance:.2f}")
|
|
389
|
+
except Exception as e:
|
|
390
|
+
click.echo(f"Error: {e}", err=True)
|
|
391
|
+
sys.exit(1)
|
|
392
|
+
finally:
|
|
393
|
+
client.close()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@account.command("info")
|
|
397
|
+
def account_info():
|
|
398
|
+
"""Show your account details."""
|
|
399
|
+
client = get_client()
|
|
400
|
+
try:
|
|
401
|
+
info = client.get_account_info()
|
|
402
|
+
for key, value in info.items():
|
|
403
|
+
click.echo(f" {key}: {value}")
|
|
404
|
+
except Exception as e:
|
|
405
|
+
click.echo(f"Error: {e}", err=True)
|
|
406
|
+
sys.exit(1)
|
|
407
|
+
finally:
|
|
408
|
+
client.close()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def main():
|
|
412
|
+
"""Main entry point for the CLI."""
|
|
413
|
+
cli()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
main()
|