request-vm-on-golem 0.1.24__py3-none-any.whl → 0.1.27__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.
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 components
21
- db = Database(config.db_path)
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 db.init()
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
- params = {
69
- k: v for k, v in {
70
- 'cpu': cpu,
71
- 'memory': memory,
72
- 'storage': storage,
73
- 'country': country
74
- }.items() if v is not None
75
- }
76
-
77
- async with aiohttp.ClientSession() as session:
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 = ["Provider ID", "IP Address", "Country",
92
- "CPU", "Memory (GB)", "Storage (GB)", "Updated"]
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" + "─" * 60)
93
+ click.echo("\n" + "─" * 80)
113
94
  click.echo(click.style(f" 🌍 Available Providers ({len(providers)} total)", fg="blue", bold=True))
114
- click.echo("─" * 60)
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" + "─" * 60)
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
- logger.command(f"🚀 Creating VM '{name}'")
207
- logger.detail(f"Provider: {provider_id}")
208
- logger.detail(f"Resources: {cpu} CPU, {memory}GB RAM, {storage}GB Storage")
209
-
210
- # Check VM name
211
- await check_vm_name(db, name)
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)
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")
220
127
 
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")
225
-
226
- # Create and configure VM
227
- provider_url = config.get_provider_url(provider_ip)
228
- async with ProviderClient(provider_url) as client:
229
- # Create VM and get full VM ID
230
- vm = await deploy_vm(client, name, cpu, memory, storage, key_pair.public_key_content)
231
- access_info = await get_vm_access(client, vm['id'])
232
-
233
- # Store the full VM ID from access info
234
- full_vm_id = access_info['vm_id']
235
-
236
- await save_vm_config(
237
- db, name, provider_ip, full_vm_id,
238
- {
239
- 'cpu': cpu,
240
- 'memory': memory,
241
- 'storage': storage,
242
- 'ssh_port': access_info['ssh_port']
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(access_info['ssh_port']), fg='cyan')}")
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 = f"ssh -i {key_pair.private_key.absolute()} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p {access_info['ssh_port']} ubuntu@{provider_ip}"
268
- click.echo(f" 🔑 SSH Command : {click.style(ssh_command, fg='yellow')}")
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
- # Get VM details
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 db.get_vm(name)
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
- ssh_manager = SSHKeyManager(config.ssh_key_dir)
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
- access_info = await client.get_vm_access(vm['vm_id'])
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']}:{access_info['ssh_port']}")
311
- cmd = [
312
- "ssh",
313
- "-i", str(key_pair.private_key.absolute()),
314
- "-p", str(access_info['ssh_port']),
315
- "-o", "StrictHostKeyChecking=no",
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 db.get_vm(name)
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
- try:
348
- # Connect to provider using full VM ID
349
- logger.process("Requesting VM termination")
350
- provider_url = config.get_provider_url(vm['provider_ip'])
351
- async with ProviderClient(provider_url) as client:
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)
@@ -386,7 +298,7 @@ async def destroy_vm(name: str):
386
298
 
387
299
 
388
300
  @vm.command(name='purge')
389
- @click.option('--force', is_flag=True, help='Force purge even if errors occur')
301
+ @click.option('--force', is_flag=True, help='Force purge even if other errors occur')
390
302
  @click.confirmation_option(prompt='Are you sure you want to purge all VMs?')
391
303
  @async_command
392
304
  async def purge_vms(force: bool):
@@ -394,61 +306,48 @@ async def purge_vms(force: bool):
394
306
  try:
395
307
  logger.command("🌪️ Purging all VMs")
396
308
 
397
- # Get all VMs
398
- logger.process("Retrieving all VM details")
399
- vms = await db.list_vms()
309
+ vms = await db_service.list_vms()
400
310
  if not vms:
401
311
  logger.warning("No VMs found to purge")
402
312
  return
403
313
 
404
- # Track results
405
- results = {
406
- 'success': [],
407
- 'failed': []
408
- }
314
+ results = {'success': [], 'failed': []}
409
315
 
410
- # Process each VM
411
316
  for vm in vms:
412
317
  try:
413
318
  logger.process(f"Purging VM '{vm['name']}'")
319
+ provider_url = config.get_provider_url(vm['provider_ip'])
414
320
 
415
- try:
416
- # Try to destroy on provider using full VM ID
417
- provider_url = config.get_provider_url(vm['provider_ip'])
418
- async with ProviderClient(provider_url) as client:
419
- # Get latest VM access info to ensure we have the correct ID
420
- try:
421
- access_info = await client.get_vm_access(vm['vm_id'])
422
- full_vm_id = access_info['vm_id']
423
- except Exception:
424
- # If we can't get access info, use stored VM ID
425
- full_vm_id = vm['vm_id']
426
-
427
- await client.destroy_vm(full_vm_id)
321
+ async with ProviderClient(provider_url) as client:
322
+ vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
323
+ await vm_service.destroy_vm(vm['name'])
428
324
  results['success'].append((vm['name'], 'Destroyed successfully'))
429
- except Exception as e:
430
- error_msg = str(e)
431
- if "Not Found" in error_msg:
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'])
440
-
325
+
326
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
327
+ await db_service.delete_vm(vm['name'])
328
+ msg = f"Could not connect to provider ({e}). Removed from local DB. Please destroy manually."
329
+ results['failed'].append((vm['name'], msg))
330
+
441
331
  except Exception as e:
442
- if not force:
332
+ if "Cannot connect to host" in str(e):
333
+ await db_service.delete_vm(vm['name'])
334
+ msg = f"Could not connect to provider ({e}). Removed from local DB. Please destroy manually."
335
+ results['failed'].append((vm['name'], msg))
336
+ elif "not found in multipass" in str(e).lower():
337
+ await db_service.delete_vm(vm['name'])
338
+ msg = "VM not found on provider. Removed from local DB."
339
+ results['success'].append((vm['name'], msg))
340
+ elif not force:
341
+ logger.error(f"Failed to purge VM '{vm['name']}'. Use --force to ignore errors and continue.")
443
342
  raise
444
- results['failed'].append((vm['name'], str(e)))
343
+ else:
344
+ results['failed'].append((vm['name'], str(e)))
445
345
 
446
346
  # Show results
447
347
  click.echo("\n" + "─" * 60)
448
348
  click.echo(click.style(" 🌪️ VM Purge Complete", fg="blue", bold=True))
449
349
  click.echo("─" * 60 + "\n")
450
350
 
451
- # Success section
452
351
  if results['success']:
453
352
  click.echo(click.style(" ✅ Successfully Purged", fg="green", bold=True))
454
353
  click.echo(" " + "┈" * 25)
@@ -456,7 +355,6 @@ async def purge_vms(force: bool):
456
355
  click.echo(f" • {click.style(name, fg='cyan')}: {click.style(msg, fg='green')}")
457
356
  click.echo()
458
357
 
459
- # Failures section
460
358
  if results['failed']:
461
359
  click.echo(click.style(" ❌ Failed to Purge", fg="red", bold=True))
462
360
  click.echo(" " + "┈" * 25)
@@ -464,7 +362,6 @@ async def purge_vms(force: bool):
464
362
  click.echo(f" • {click.style(name, fg='cyan')}: {click.style(error, fg='red')}")
465
363
  click.echo()
466
364
 
467
- # Summary
468
365
  total = len(results['success']) + len(results['failed'])
469
366
  success_rate = (len(results['success']) / total) * 100 if total > 0 else 0
470
367
 
@@ -478,10 +375,7 @@ async def purge_vms(force: bool):
478
375
  click.echo("\n" + "─" * 60)
479
376
 
480
377
  except Exception as e:
481
- error_msg = str(e)
482
- if "database" in error_msg.lower():
483
- error_msg = "Failed to access local database"
484
- logger.error(f"Purge operation failed: {error_msg}")
378
+ logger.error(f"Purge operation failed: {str(e)}")
485
379
  raise click.Abort()
486
380
 
487
381
 
@@ -493,18 +387,17 @@ async def start_vm(name: str):
493
387
  try:
494
388
  logger.command(f"🟢 Starting VM '{name}'")
495
389
 
496
- # Get VM details
390
+ # Get VM details using database service
497
391
  logger.process("Retrieving VM details")
498
- vm = await db.get_vm(name)
392
+ vm = await db_service.get_vm(name)
499
393
  if not vm:
500
394
  raise click.BadParameter(f"VM '{name}' not found")
501
395
 
502
- # Connect to provider
503
- logger.process("Powering up VM")
396
+ # Initialize VM service
504
397
  provider_url = config.get_provider_url(vm['provider_ip'])
505
398
  async with ProviderClient(provider_url) as client:
506
- await client.start_vm(vm['vm_id'])
507
- await db.update_vm_status(name, "running")
399
+ vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
400
+ await vm_service.start_vm(name)
508
401
 
509
402
  # Show fancy success message
510
403
  click.echo("\n" + "─" * 60)
@@ -540,30 +433,29 @@ async def stop_vm(name: str):
540
433
  try:
541
434
  logger.command(f"🔴 Stopping VM '{name}'")
542
435
 
543
- # Get VM details
436
+ # Get VM details using database service
544
437
  logger.process("Retrieving VM details")
545
- vm = await db.get_vm(name)
438
+ vm = await db_service.get_vm(name)
546
439
  if not vm:
547
440
  raise click.BadParameter(f"VM '{name}' not found")
548
441
 
549
- # Connect to provider
550
- logger.process("Shutting down VM")
442
+ # Initialize VM service
551
443
  provider_url = config.get_provider_url(vm['provider_ip'])
552
444
  async with ProviderClient(provider_url) as client:
553
- await client.stop_vm(vm['vm_id'])
554
- await db.update_vm_status(name, "stopped")
445
+ vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
446
+ await vm_service.stop_vm(name)
555
447
 
556
448
  # Show fancy success message
557
449
  click.echo("\n" + "─" * 60)
558
450
  click.echo(click.style(" 🔴 VM Stopped Successfully!", fg="yellow", bold=True))
559
451
  click.echo("─" * 60 + "\n")
560
-
452
+
561
453
  click.echo(click.style(" VM Status", fg="blue", bold=True))
562
454
  click.echo(" " + "┈" * 25)
563
455
  click.echo(f" 🏷️ Name : {click.style(name, fg='cyan')}")
564
456
  click.echo(f" 💫 Status : {click.style('stopped', fg='yellow')}")
565
457
  click.echo(f" 💾 Resources : {click.style('preserved', fg='cyan')}")
566
-
458
+
567
459
  click.echo("\n" + "─" * 60)
568
460
 
569
461
  except Exception as e:
@@ -574,6 +466,39 @@ async def stop_vm(name: str):
574
466
  raise click.Abort()
575
467
 
576
468
 
469
+ @cli.group()
470
+ def server():
471
+ """Server management commands"""
472
+ pass
473
+
474
+
475
+ @server.command(name='api')
476
+ @click.option('--host', default='127.0.0.1', help='Host to bind the API server to.')
477
+ @click.option('--port', default=8000, type=int, help='Port to run the API server on.')
478
+ @click.option('--reload', is_flag=True, help='Enable auto-reload for development.')
479
+ def run_api_server(host: str, port: int, reload: bool):
480
+ """Run the Requestor API server."""
481
+ logger.command(f"🚀 Starting Requestor API server on {host}:{port}")
482
+ if reload:
483
+ logger.warning("Auto-reload enabled (for development)")
484
+
485
+ # Ensure the database directory exists before starting uvicorn
486
+ try:
487
+ config.db_path.parent.mkdir(parents=True, exist_ok=True)
488
+ logger.detail(f"Ensured database directory exists: {config.db_path.parent}")
489
+ except Exception as e:
490
+ logger.error(f"Failed to create database directory {config.db_path.parent}: {e}")
491
+ raise click.Abort()
492
+
493
+ uvicorn.run(
494
+ "requestor.api.main:app",
495
+ host=host,
496
+ port=port,
497
+ reload=reload,
498
+ log_level="info" # Or adjust as needed
499
+ )
500
+
501
+
577
502
  @vm.command(name='list')
578
503
  @async_command
579
504
  async def list_vms():
@@ -582,41 +507,23 @@ async def list_vms():
582
507
  logger.command("📋 Listing your VMs")
583
508
  logger.process("Fetching VM details")
584
509
 
585
- vms = await db.list_vms()
510
+ # Initialize VM service with temporary client (not needed for listing)
511
+ ssh_service = SSHService(config.ssh_key_dir)
512
+ vm_service = VMService(db_service, ssh_service, None)
513
+ vms = await vm_service.list_vms()
586
514
  if not vms:
587
515
  logger.warning("No VMs found")
588
516
  return
589
517
 
590
- headers = ["Name", "Status", "IP Address", "SSH Port",
591
- "CPU", "Memory (GB)", "Storage (GB)", "Created"]
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
- ])
518
+ # Format VM information using service
519
+ headers = vm_service.vm_headers
520
+ rows = [vm_service.format_vm_row(vm, colorize=True) for vm in vms]
604
521
 
605
522
  # Show fancy header
606
523
  click.echo("\n" + "─" * 60)
607
524
  click.echo(click.style(f" 📋 Your VMs ({len(vms)} total)", fg="blue", bold=True))
608
525
  click.echo("─" * 60)
609
526
 
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
527
  # Show table with colored status
621
528
  click.echo("\n" + tabulate(
622
529
  rows,
@@ -640,3 +547,43 @@ def main():
640
547
 
641
548
  if __name__ == '__main__':
642
549
  main()
550
+
551
+
552
+ @vm.command(name='stats')
553
+ @click.argument('name')
554
+ @async_command
555
+ async def vm_stats(name: str):
556
+ """Display live resource usage statistics for a VM."""
557
+ try:
558
+ # Initialize services
559
+ ssh_service = SSHService(config.ssh_key_dir)
560
+ vm_service = VMService(db_service, ssh_service)
561
+
562
+ # Get VM details
563
+ vm = await vm_service.get_vm(name)
564
+ if not vm:
565
+ raise click.BadParameter(f"VM '{name}' not found")
566
+
567
+ # Loop to fetch and display stats continuously
568
+ while True:
569
+ stats = await vm_service.get_vm_stats(name)
570
+
571
+ click.clear()
572
+ click.echo("\n" + "─" * 60)
573
+ click.echo(click.style(f" 📊 Live Stats for VM: {name} (Press Ctrl+C to exit)", fg="blue", bold=True))
574
+ click.echo("─" * 60)
575
+
576
+ if 'cpu' in stats and 'usage' in stats['cpu']:
577
+ click.echo(f" 💻 CPU Usage : {click.style(stats['cpu']['usage'], fg='cyan')}")
578
+ if 'memory' in stats and 'used' in stats['memory']:
579
+ click.echo(f" 🧠 Memory : {click.style(stats['memory']['used'], fg='cyan')} / {click.style(stats['memory']['total'], fg='cyan')}")
580
+ if 'disk' in stats and 'used' in stats['disk']:
581
+ click.echo(f" 💾 Disk : {click.style(stats['disk']['used'], fg='cyan')} / {click.style(stats['disk']['total'], fg='cyan')}")
582
+
583
+ click.echo("─" * 60)
584
+
585
+ await asyncio.sleep(2) # Update every 2 seconds
586
+
587
+ except Exception as e:
588
+ logger.error(f"Failed to get VM stats: {str(e)}")
589
+ raise click.Abort()