request-vm-on-golem 0.1.23__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.
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)
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
- # 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)
@@ -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 db.list_vms()
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
- 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)
428
- 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'])
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 db.get_vm(name)
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
- # Connect to provider
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
- await client.start_vm(vm['vm_id'])
507
- await db.update_vm_status(name, "running")
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 db.get_vm(name)
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
- # Connect to provider
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
- await client.stop_vm(vm['vm_id'])
554
- await db.update_vm_status(name, "stopped")
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
- vms = await db.list_vms()
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
- 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
- ])
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()