request-vm-on-golem 0.1.24__py3-none-any.whl → 0.1.26__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.
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.26.dist-info}/METADATA +11 -9
- request_vm_on_golem-0.1.26.dist-info/RECORD +24 -0
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.26.dist-info}/WHEEL +1 -1
- requestor/api/main.py +59 -0
- requestor/cli/commands.py +213 -256
- requestor/config.py +36 -2
- requestor/db/sqlite.py +33 -41
- requestor/run.py +4 -2
- requestor/services/__init__.py +6 -0
- requestor/services/database_service.py +91 -0
- requestor/services/provider_service.py +265 -0
- requestor/services/ssh_service.py +121 -0
- requestor/services/vm_service.py +209 -0
- request_vm_on_golem-0.1.24.dist-info/RECORD +0 -18
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.26.dist-info}/entry_points.txt +0 -0
requestor/cli/commands.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
"""CLI interface for VM on Golem."""
|
1
2
|
import click
|
2
3
|
import asyncio
|
3
4
|
from typing import Optional
|
@@ -5,27 +6,30 @@ from pathlib import Path
|
|
5
6
|
import subprocess
|
6
7
|
import aiohttp
|
7
8
|
from tabulate import tabulate
|
9
|
+
import uvicorn
|
8
10
|
|
9
11
|
from ..config import config
|
10
12
|
from ..provider.client import ProviderClient
|
11
|
-
from ..ssh.manager import SSHKeyManager
|
12
|
-
from ..db.sqlite import Database
|
13
13
|
from ..errors import RequestorError
|
14
14
|
from ..utils.logging import setup_logger
|
15
|
-
from ..utils.spinner import step
|
15
|
+
from ..utils.spinner import step, Spinner
|
16
|
+
from ..services.vm_service import VMService
|
17
|
+
from ..services.provider_service import ProviderService
|
18
|
+
from ..services.ssh_service import SSHService
|
19
|
+
from ..services.database_service import DatabaseService
|
16
20
|
|
17
21
|
# Initialize logger
|
18
22
|
logger = setup_logger('golem.requestor')
|
19
23
|
|
20
|
-
# Initialize
|
21
|
-
|
24
|
+
# Initialize services
|
25
|
+
db_service = DatabaseService(config.db_path)
|
22
26
|
|
23
27
|
|
24
28
|
def async_command(f):
|
25
29
|
"""Decorator to run async commands."""
|
26
30
|
async def wrapper(*args, **kwargs):
|
27
31
|
# Initialize database
|
28
|
-
await
|
32
|
+
await db_service.init()
|
29
33
|
return await f(*args, **kwargs)
|
30
34
|
return lambda *args, **kwargs: asyncio.run(wrapper(*args, **kwargs))
|
31
35
|
|
@@ -47,8 +51,9 @@ def vm():
|
|
47
51
|
@click.option('--memory', type=int, help='Minimum memory (GB) required')
|
48
52
|
@click.option('--storage', type=int, help='Minimum storage (GB) required')
|
49
53
|
@click.option('--country', help='Preferred provider country')
|
54
|
+
@click.option('--driver', type=click.Choice(['central', 'golem-base']), default=None, help='Discovery driver to use')
|
50
55
|
@async_command
|
51
|
-
async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str]):
|
56
|
+
async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], driver: Optional[str]):
|
52
57
|
"""List available providers matching requirements."""
|
53
58
|
try:
|
54
59
|
# Log search criteria if any
|
@@ -65,66 +70,29 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
65
70
|
|
66
71
|
logger.process("Querying discovery service")
|
67
72
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
async with session.get(
|
79
|
-
f"{config.discovery_url}/api/v1/advertisements",
|
80
|
-
params=params
|
81
|
-
) as response:
|
82
|
-
if not response.ok:
|
83
|
-
raise RequestorError("Failed to query discovery service")
|
84
|
-
providers = await response.json()
|
73
|
+
# Initialize provider service
|
74
|
+
provider_service = ProviderService()
|
75
|
+
async with provider_service:
|
76
|
+
providers = await provider_service.find_providers(
|
77
|
+
cpu=cpu,
|
78
|
+
memory=memory,
|
79
|
+
storage=storage,
|
80
|
+
country=country,
|
81
|
+
driver=driver
|
82
|
+
)
|
85
83
|
|
86
84
|
if not providers:
|
87
85
|
logger.warning("No providers found matching criteria")
|
88
86
|
return
|
89
87
|
|
90
|
-
# Format provider information
|
91
|
-
headers =
|
92
|
-
|
93
|
-
rows = []
|
94
|
-
for p in providers:
|
95
|
-
# Get provider IP based on environment
|
96
|
-
provider_ip = 'localhost' if config.environment == "development" else p.get(
|
97
|
-
'ip_address')
|
98
|
-
if not provider_ip and config.environment == "production":
|
99
|
-
logger.warning(f"Provider {p['provider_id']} has no IP address")
|
100
|
-
provider_ip = 'N/A'
|
101
|
-
rows.append([
|
102
|
-
p['provider_id'],
|
103
|
-
provider_ip,
|
104
|
-
p['country'],
|
105
|
-
p['resources']['cpu'],
|
106
|
-
p['resources']['memory'],
|
107
|
-
p['resources']['storage'],
|
108
|
-
p['updated_at']
|
109
|
-
])
|
88
|
+
# Format provider information using service with colors
|
89
|
+
headers = provider_service.provider_headers
|
90
|
+
rows = await asyncio.gather(*(provider_service.format_provider_row(p, colorize=True) for p in providers))
|
110
91
|
|
111
92
|
# Show fancy header
|
112
|
-
click.echo("\n" + "─" *
|
93
|
+
click.echo("\n" + "─" * 80)
|
113
94
|
click.echo(click.style(f" 🌍 Available Providers ({len(providers)} total)", fg="blue", bold=True))
|
114
|
-
click.echo("─" *
|
115
|
-
|
116
|
-
# Add color and formatting to columns
|
117
|
-
for row in rows:
|
118
|
-
# Format Provider ID to be more readable
|
119
|
-
row[0] = click.style(row[0], fg="yellow") # Provider ID
|
120
|
-
|
121
|
-
# Format resources with icons and colors
|
122
|
-
row[3] = click.style(f"💻 {row[3]}", fg="cyan", bold=True) # CPU
|
123
|
-
row[4] = click.style(f"🧠 {row[4]}", fg="cyan", bold=True) # Memory
|
124
|
-
row[5] = click.style(f"💾 {row[5]}", fg="cyan", bold=True) # Storage
|
125
|
-
|
126
|
-
# Format location info
|
127
|
-
row[2] = click.style(f"🌍 {row[2]}", fg="green", bold=True) # Country
|
95
|
+
click.echo("─" * 80)
|
128
96
|
|
129
97
|
# Show table with colored headers
|
130
98
|
click.echo("\n" + tabulate(
|
@@ -132,67 +100,13 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
132
100
|
headers=[click.style(h, bold=True) for h in headers],
|
133
101
|
tablefmt="grid"
|
134
102
|
))
|
135
|
-
click.echo("\n" + "─" *
|
103
|
+
click.echo("\n" + "─" * 80)
|
136
104
|
|
137
105
|
except Exception as e:
|
138
106
|
logger.error(f"Failed to list providers: {str(e)}")
|
139
107
|
raise click.Abort()
|
140
108
|
|
141
109
|
|
142
|
-
# Helper functions for VM creation with spinners
|
143
|
-
@step("Checking VM name availability")
|
144
|
-
async def check_vm_name(db, name):
|
145
|
-
existing_vm = await db.get_vm(name)
|
146
|
-
if existing_vm:
|
147
|
-
raise RequestorError(f"VM with name '{name}' already exists")
|
148
|
-
|
149
|
-
@step("Locating provider")
|
150
|
-
async def find_provider(discovery_url, provider_id):
|
151
|
-
async with aiohttp.ClientSession() as session:
|
152
|
-
async with session.get(f"{discovery_url}/api/v1/advertisements") as response:
|
153
|
-
if not response.ok:
|
154
|
-
raise RequestorError("Failed to query discovery service")
|
155
|
-
providers = await response.json()
|
156
|
-
|
157
|
-
provider = next((p for p in providers if p['provider_id'] == provider_id), None)
|
158
|
-
if not provider:
|
159
|
-
raise RequestorError(f"Provider {provider_id} not found")
|
160
|
-
return provider
|
161
|
-
|
162
|
-
@step("Verifying resource availability")
|
163
|
-
async def verify_resources(provider, cpu, memory, storage):
|
164
|
-
if (cpu > provider['resources']['cpu'] or
|
165
|
-
memory > provider['resources']['memory'] or
|
166
|
-
storage > provider['resources']['storage']):
|
167
|
-
raise RequestorError("Requested resources exceed provider capacity")
|
168
|
-
|
169
|
-
@step("Setting up SSH access")
|
170
|
-
async def setup_ssh(ssh_manager):
|
171
|
-
return await ssh_manager.get_key_pair()
|
172
|
-
|
173
|
-
@step("Deploying VM on provider")
|
174
|
-
async def deploy_vm(client, name, cpu, memory, storage, ssh_key):
|
175
|
-
return await client.create_vm(
|
176
|
-
name=name,
|
177
|
-
cpu=cpu,
|
178
|
-
memory=memory,
|
179
|
-
storage=storage,
|
180
|
-
ssh_key=ssh_key
|
181
|
-
)
|
182
|
-
|
183
|
-
@step("Configuring VM access")
|
184
|
-
async def get_vm_access(client, vm_id):
|
185
|
-
return await client.get_vm_access(vm_id)
|
186
|
-
|
187
|
-
@step("Saving VM configuration")
|
188
|
-
async def save_vm_config(db, name, provider_ip, vm_id, config):
|
189
|
-
await db.save_vm(
|
190
|
-
name=name,
|
191
|
-
provider_ip=provider_ip,
|
192
|
-
vm_id=vm_id,
|
193
|
-
config=config
|
194
|
-
)
|
195
|
-
|
196
110
|
@vm.command(name='create')
|
197
111
|
@click.argument('name')
|
198
112
|
@click.option('--provider-id', required=True, help='Provider ID to use')
|
@@ -203,45 +117,50 @@ async def save_vm_config(db, name, provider_ip, vm_id, config):
|
|
203
117
|
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int):
|
204
118
|
"""Create a new VM on a specific provider."""
|
205
119
|
try:
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
# Find and verify provider
|
214
|
-
provider = await find_provider(config.discovery_url, provider_id)
|
215
|
-
await verify_resources(provider, cpu, memory, storage)
|
216
|
-
|
217
|
-
# Setup SSH
|
218
|
-
ssh_manager = SSHKeyManager(config.ssh_key_dir)
|
219
|
-
key_pair = await setup_ssh(ssh_manager)
|
220
|
-
|
221
|
-
# Get provider IP
|
222
|
-
provider_ip = 'localhost' if config.environment == "development" else provider.get('ip_address')
|
223
|
-
if not provider_ip and config.environment == "production":
|
224
|
-
raise RequestorError("Provider IP address not found in advertisement")
|
120
|
+
# Show configuration details
|
121
|
+
click.echo("\n" + "─" * 60)
|
122
|
+
click.echo(click.style(" VM Configuration", fg="blue", bold=True))
|
123
|
+
click.echo("─" * 60)
|
124
|
+
click.echo(f" Provider : {click.style(provider_id, fg='cyan')}")
|
125
|
+
click.echo(f" Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB Storage', fg='cyan')}")
|
126
|
+
click.echo("─" * 60 + "\n")
|
225
127
|
|
226
|
-
#
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
128
|
+
# Now start the deployment with spinner
|
129
|
+
with Spinner("Deploying VM..."):
|
130
|
+
# Initialize services
|
131
|
+
provider_service = ProviderService()
|
132
|
+
async with provider_service:
|
133
|
+
# Verify provider and resources
|
134
|
+
provider = await provider_service.verify_provider(provider_id)
|
135
|
+
if not await provider_service.check_resource_availability(provider_id, cpu, memory, storage):
|
136
|
+
raise RequestorError("Provider doesn't have enough resources available")
|
137
|
+
|
138
|
+
# Get provider IP
|
139
|
+
provider_ip = 'localhost' if config.environment == "development" else provider.get('ip_address')
|
140
|
+
if not provider_ip and config.environment == "production":
|
141
|
+
raise RequestorError("Provider IP address not found in advertisement")
|
142
|
+
|
143
|
+
# Setup SSH
|
144
|
+
ssh_service = SSHService(config.ssh_key_dir)
|
145
|
+
key_pair = await ssh_service.get_key_pair()
|
146
|
+
|
147
|
+
# Initialize VM service
|
148
|
+
provider_url = config.get_provider_url(provider_ip)
|
149
|
+
async with ProviderClient(provider_url) as client:
|
150
|
+
vm_service = VMService(db_service, ssh_service, client)
|
151
|
+
|
152
|
+
# Create VM
|
153
|
+
vm = await vm_service.create_vm(
|
154
|
+
name=name,
|
155
|
+
cpu=cpu,
|
156
|
+
memory=memory,
|
157
|
+
storage=storage,
|
158
|
+
provider_ip=provider_ip,
|
159
|
+
ssh_key=key_pair.public_key_content
|
160
|
+
)
|
161
|
+
|
162
|
+
# Get access info from config
|
163
|
+
ssh_port = vm['config']['ssh_port']
|
245
164
|
|
246
165
|
# Create a visually appealing success message
|
247
166
|
click.echo("\n" + "─" * 60)
|
@@ -259,13 +178,18 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
259
178
|
click.echo("\n" + click.style(" Connection Details", fg="blue", bold=True))
|
260
179
|
click.echo(" " + "┈" * 25)
|
261
180
|
click.echo(f" 🌐 IP Address : {click.style(provider_ip, fg='cyan')}")
|
262
|
-
click.echo(f" 🔌 Port : {click.style(str(
|
181
|
+
click.echo(f" 🔌 Port : {click.style(str(ssh_port), fg='cyan')}")
|
263
182
|
|
264
183
|
# Quick Connect Section
|
265
184
|
click.echo("\n" + click.style(" Quick Connect", fg="blue", bold=True))
|
266
185
|
click.echo(" " + "┈" * 25)
|
267
|
-
ssh_command =
|
268
|
-
|
186
|
+
ssh_command = ssh_service.format_ssh_command(
|
187
|
+
host=provider_ip,
|
188
|
+
port=ssh_port,
|
189
|
+
private_key_path=key_pair.private_key.absolute(),
|
190
|
+
colorize=True
|
191
|
+
)
|
192
|
+
click.echo(f" 🔑 SSH Command : {ssh_command}")
|
269
193
|
|
270
194
|
click.echo("\n" + "─" * 60)
|
271
195
|
|
@@ -289,34 +213,34 @@ async def ssh_vm(name: str):
|
|
289
213
|
try:
|
290
214
|
logger.command(f"🔌 Connecting to VM '{name}'")
|
291
215
|
|
292
|
-
#
|
216
|
+
# Initialize services
|
217
|
+
ssh_service = SSHService(config.ssh_key_dir)
|
218
|
+
|
219
|
+
# Get VM details using database service
|
293
220
|
logger.process("Retrieving VM details")
|
294
|
-
vm = await
|
221
|
+
vm = await db_service.get_vm(name)
|
295
222
|
if not vm:
|
296
223
|
raise click.BadParameter(f"VM '{name}' not found")
|
297
224
|
|
298
225
|
# Get SSH key
|
299
226
|
logger.process("Loading SSH credentials")
|
300
|
-
|
301
|
-
key_pair = await ssh_manager.get_key_pair()
|
227
|
+
key_pair = await ssh_service.get_key_pair()
|
302
228
|
|
303
|
-
# Get VM access info
|
229
|
+
# Get VM access info using service
|
304
230
|
logger.process("Fetching connection details")
|
305
231
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
306
232
|
async with ProviderClient(provider_url) as client:
|
307
|
-
|
233
|
+
vm_service = VMService(db_service, ssh_service, client)
|
234
|
+
vm = await vm_service.get_vm(name) # Get fresh VM info
|
235
|
+
ssh_port = vm['config']['ssh_port']
|
308
236
|
|
309
237
|
# Execute SSH command
|
310
|
-
logger.success(f"Connecting to {vm['provider_ip']}:{
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
"-o", "UserKnownHostsFile=/dev/null",
|
317
|
-
f"ubuntu@{vm['provider_ip']}"
|
318
|
-
]
|
319
|
-
subprocess.run(cmd)
|
238
|
+
logger.success(f"Connecting to {vm['provider_ip']}:{ssh_port}")
|
239
|
+
ssh_service.connect_to_vm(
|
240
|
+
host=vm['provider_ip'],
|
241
|
+
port=ssh_port,
|
242
|
+
private_key_path=key_pair.private_key.absolute()
|
243
|
+
)
|
320
244
|
|
321
245
|
except Exception as e:
|
322
246
|
error_msg = str(e)
|
@@ -338,29 +262,17 @@ async def destroy_vm(name: str):
|
|
338
262
|
try:
|
339
263
|
logger.command(f"💥 Destroying VM '{name}'")
|
340
264
|
|
341
|
-
# Get VM details
|
265
|
+
# Get VM details using database service
|
342
266
|
logger.process("Retrieving VM details")
|
343
|
-
vm = await
|
267
|
+
vm = await db_service.get_vm(name)
|
344
268
|
if not vm:
|
345
269
|
raise click.BadParameter(f"VM '{name}' not found")
|
346
270
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
await client.destroy_vm(vm['vm_id'])
|
353
|
-
logger.success("VM terminated on provider")
|
354
|
-
except Exception as e:
|
355
|
-
error_msg = str(e)
|
356
|
-
if "Not Found" in error_msg:
|
357
|
-
logger.warning("VM already removed from provider")
|
358
|
-
else:
|
359
|
-
raise
|
360
|
-
|
361
|
-
# Always remove from database
|
362
|
-
logger.process("Cleaning up VM records")
|
363
|
-
await db.delete_vm(name)
|
271
|
+
# Initialize VM service
|
272
|
+
provider_url = config.get_provider_url(vm['provider_ip'])
|
273
|
+
async with ProviderClient(provider_url) as client:
|
274
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
|
275
|
+
await vm_service.destroy_vm(name)
|
364
276
|
|
365
277
|
# Show fancy success message
|
366
278
|
click.echo("\n" + "─" * 60)
|
@@ -394,9 +306,9 @@ async def purge_vms(force: bool):
|
|
394
306
|
try:
|
395
307
|
logger.command("🌪️ Purging all VMs")
|
396
308
|
|
397
|
-
# Get all VMs
|
309
|
+
# Get all VMs using database service
|
398
310
|
logger.process("Retrieving all VM details")
|
399
|
-
vms = await
|
311
|
+
vms = await db_service.list_vms()
|
400
312
|
if not vms:
|
401
313
|
logger.warning("No VMs found to purge")
|
402
314
|
return
|
@@ -412,31 +324,23 @@ async def purge_vms(force: bool):
|
|
412
324
|
try:
|
413
325
|
logger.process(f"Purging VM '{vm['name']}'")
|
414
326
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
results['success'].append((vm['name'], 'Already removed from provider'))
|
433
|
-
else:
|
434
|
-
if not force:
|
435
|
-
raise
|
436
|
-
results['failed'].append((vm['name'], f"Provider error: {error_msg}"))
|
437
|
-
|
438
|
-
# Always remove from local database
|
439
|
-
await db.delete_vm(vm['name'])
|
327
|
+
# Initialize VM service
|
328
|
+
provider_url = config.get_provider_url(vm['provider_ip'])
|
329
|
+
async with ProviderClient(provider_url) as client:
|
330
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
|
331
|
+
try:
|
332
|
+
await vm_service.destroy_vm(vm['name'])
|
333
|
+
results['success'].append((vm['name'], 'Destroyed successfully'))
|
334
|
+
except Exception as e:
|
335
|
+
error_msg = str(e)
|
336
|
+
if "Not Found" in error_msg:
|
337
|
+
results['success'].append((vm['name'], 'Already removed from provider'))
|
338
|
+
# Still need to clean up database
|
339
|
+
await db_service.delete_vm(vm['name'])
|
340
|
+
else:
|
341
|
+
if not force:
|
342
|
+
raise
|
343
|
+
results['failed'].append((vm['name'], f"Provider error: {error_msg}"))
|
440
344
|
|
441
345
|
except Exception as e:
|
442
346
|
if not force:
|
@@ -493,18 +397,17 @@ async def start_vm(name: str):
|
|
493
397
|
try:
|
494
398
|
logger.command(f"🟢 Starting VM '{name}'")
|
495
399
|
|
496
|
-
# Get VM details
|
400
|
+
# Get VM details using database service
|
497
401
|
logger.process("Retrieving VM details")
|
498
|
-
vm = await
|
402
|
+
vm = await db_service.get_vm(name)
|
499
403
|
if not vm:
|
500
404
|
raise click.BadParameter(f"VM '{name}' not found")
|
501
405
|
|
502
|
-
#
|
503
|
-
logger.process("Powering up VM")
|
406
|
+
# Initialize VM service
|
504
407
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
505
408
|
async with ProviderClient(provider_url) as client:
|
506
|
-
|
507
|
-
await
|
409
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
|
410
|
+
await vm_service.start_vm(name)
|
508
411
|
|
509
412
|
# Show fancy success message
|
510
413
|
click.echo("\n" + "─" * 60)
|
@@ -540,30 +443,29 @@ async def stop_vm(name: str):
|
|
540
443
|
try:
|
541
444
|
logger.command(f"🔴 Stopping VM '{name}'")
|
542
445
|
|
543
|
-
# Get VM details
|
446
|
+
# Get VM details using database service
|
544
447
|
logger.process("Retrieving VM details")
|
545
|
-
vm = await
|
448
|
+
vm = await db_service.get_vm(name)
|
546
449
|
if not vm:
|
547
450
|
raise click.BadParameter(f"VM '{name}' not found")
|
548
451
|
|
549
|
-
#
|
550
|
-
logger.process("Shutting down VM")
|
452
|
+
# Initialize VM service
|
551
453
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
552
454
|
async with ProviderClient(provider_url) as client:
|
553
|
-
|
554
|
-
await
|
455
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
|
456
|
+
await vm_service.stop_vm(name)
|
555
457
|
|
556
458
|
# Show fancy success message
|
557
459
|
click.echo("\n" + "─" * 60)
|
558
460
|
click.echo(click.style(" 🔴 VM Stopped Successfully!", fg="yellow", bold=True))
|
559
461
|
click.echo("─" * 60 + "\n")
|
560
|
-
|
462
|
+
|
561
463
|
click.echo(click.style(" VM Status", fg="blue", bold=True))
|
562
464
|
click.echo(" " + "┈" * 25)
|
563
465
|
click.echo(f" 🏷️ Name : {click.style(name, fg='cyan')}")
|
564
466
|
click.echo(f" 💫 Status : {click.style('stopped', fg='yellow')}")
|
565
467
|
click.echo(f" 💾 Resources : {click.style('preserved', fg='cyan')}")
|
566
|
-
|
468
|
+
|
567
469
|
click.echo("\n" + "─" * 60)
|
568
470
|
|
569
471
|
except Exception as e:
|
@@ -574,6 +476,39 @@ async def stop_vm(name: str):
|
|
574
476
|
raise click.Abort()
|
575
477
|
|
576
478
|
|
479
|
+
@cli.group()
|
480
|
+
def server():
|
481
|
+
"""Server management commands"""
|
482
|
+
pass
|
483
|
+
|
484
|
+
|
485
|
+
@server.command(name='api')
|
486
|
+
@click.option('--host', default='127.0.0.1', help='Host to bind the API server to.')
|
487
|
+
@click.option('--port', default=8000, type=int, help='Port to run the API server on.')
|
488
|
+
@click.option('--reload', is_flag=True, help='Enable auto-reload for development.')
|
489
|
+
def run_api_server(host: str, port: int, reload: bool):
|
490
|
+
"""Run the Requestor API server."""
|
491
|
+
logger.command(f"🚀 Starting Requestor API server on {host}:{port}")
|
492
|
+
if reload:
|
493
|
+
logger.warning("Auto-reload enabled (for development)")
|
494
|
+
|
495
|
+
# Ensure the database directory exists before starting uvicorn
|
496
|
+
try:
|
497
|
+
config.db_path.parent.mkdir(parents=True, exist_ok=True)
|
498
|
+
logger.detail(f"Ensured database directory exists: {config.db_path.parent}")
|
499
|
+
except Exception as e:
|
500
|
+
logger.error(f"Failed to create database directory {config.db_path.parent}: {e}")
|
501
|
+
raise click.Abort()
|
502
|
+
|
503
|
+
uvicorn.run(
|
504
|
+
"requestor.api.main:app",
|
505
|
+
host=host,
|
506
|
+
port=port,
|
507
|
+
reload=reload,
|
508
|
+
log_level="info" # Or adjust as needed
|
509
|
+
)
|
510
|
+
|
511
|
+
|
577
512
|
@vm.command(name='list')
|
578
513
|
@async_command
|
579
514
|
async def list_vms():
|
@@ -582,41 +517,23 @@ async def list_vms():
|
|
582
517
|
logger.command("📋 Listing your VMs")
|
583
518
|
logger.process("Fetching VM details")
|
584
519
|
|
585
|
-
|
520
|
+
# Initialize VM service with temporary client (not needed for listing)
|
521
|
+
ssh_service = SSHService(config.ssh_key_dir)
|
522
|
+
vm_service = VMService(db_service, ssh_service, None)
|
523
|
+
vms = await vm_service.list_vms()
|
586
524
|
if not vms:
|
587
525
|
logger.warning("No VMs found")
|
588
526
|
return
|
589
527
|
|
590
|
-
|
591
|
-
|
592
|
-
rows = []
|
593
|
-
for vm in vms:
|
594
|
-
rows.append([
|
595
|
-
vm['name'],
|
596
|
-
vm['status'],
|
597
|
-
vm['provider_ip'],
|
598
|
-
vm['config'].get('ssh_port', 'N/A'),
|
599
|
-
vm['config']['cpu'],
|
600
|
-
vm['config']['memory'],
|
601
|
-
vm['config']['storage'],
|
602
|
-
vm['created_at']
|
603
|
-
])
|
528
|
+
# Format VM information using service
|
529
|
+
headers = vm_service.vm_headers
|
530
|
+
rows = [vm_service.format_vm_row(vm, colorize=True) for vm in vms]
|
604
531
|
|
605
532
|
# Show fancy header
|
606
533
|
click.echo("\n" + "─" * 60)
|
607
534
|
click.echo(click.style(f" 📋 Your VMs ({len(vms)} total)", fg="blue", bold=True))
|
608
535
|
click.echo("─" * 60)
|
609
536
|
|
610
|
-
# Add color to status column (index 1)
|
611
|
-
for i, row in enumerate(rows):
|
612
|
-
status = row[1]
|
613
|
-
if status == "running":
|
614
|
-
rows[i][1] = click.style("● " + status, fg="green", bold=True)
|
615
|
-
elif status == "stopped":
|
616
|
-
rows[i][1] = click.style("● " + status, fg="yellow", bold=True)
|
617
|
-
else:
|
618
|
-
rows[i][1] = click.style("● " + status, fg="red", bold=True)
|
619
|
-
|
620
537
|
# Show table with colored status
|
621
538
|
click.echo("\n" + tabulate(
|
622
539
|
rows,
|
@@ -640,3 +557,43 @@ def main():
|
|
640
557
|
|
641
558
|
if __name__ == '__main__':
|
642
559
|
main()
|
560
|
+
|
561
|
+
|
562
|
+
@vm.command(name='stats')
|
563
|
+
@click.argument('name')
|
564
|
+
@async_command
|
565
|
+
async def vm_stats(name: str):
|
566
|
+
"""Display live resource usage statistics for a VM."""
|
567
|
+
try:
|
568
|
+
# Initialize services
|
569
|
+
ssh_service = SSHService(config.ssh_key_dir)
|
570
|
+
vm_service = VMService(db_service, ssh_service)
|
571
|
+
|
572
|
+
# Get VM details
|
573
|
+
vm = await vm_service.get_vm(name)
|
574
|
+
if not vm:
|
575
|
+
raise click.BadParameter(f"VM '{name}' not found")
|
576
|
+
|
577
|
+
# Loop to fetch and display stats continuously
|
578
|
+
while True:
|
579
|
+
stats = await vm_service.get_vm_stats(name)
|
580
|
+
|
581
|
+
click.clear()
|
582
|
+
click.echo("\n" + "─" * 60)
|
583
|
+
click.echo(click.style(f" 📊 Live Stats for VM: {name} (Press Ctrl+C to exit)", fg="blue", bold=True))
|
584
|
+
click.echo("─" * 60)
|
585
|
+
|
586
|
+
if 'cpu' in stats and 'usage' in stats['cpu']:
|
587
|
+
click.echo(f" 💻 CPU Usage : {click.style(stats['cpu']['usage'], fg='cyan')}")
|
588
|
+
if 'memory' in stats and 'used' in stats['memory']:
|
589
|
+
click.echo(f" 🧠 Memory : {click.style(stats['memory']['used'], fg='cyan')} / {click.style(stats['memory']['total'], fg='cyan')}")
|
590
|
+
if 'disk' in stats and 'used' in stats['disk']:
|
591
|
+
click.echo(f" 💾 Disk : {click.style(stats['disk']['used'], fg='cyan')} / {click.style(stats['disk']['total'], fg='cyan')}")
|
592
|
+
|
593
|
+
click.echo("─" * 60)
|
594
|
+
|
595
|
+
await asyncio.sleep(2) # Update every 2 seconds
|
596
|
+
|
597
|
+
except Exception as e:
|
598
|
+
logger.error(f"Failed to get VM stats: {str(e)}")
|
599
|
+
raise click.Abort()
|