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.
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.27.dist-info}/METADATA +11 -9
- request_vm_on_golem-0.1.27.dist-info/RECORD +24 -0
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.27.dist-info}/WHEEL +1 -1
- requestor/api/main.py +59 -0
- requestor/cli/commands.py +220 -273
- 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 +128 -0
- requestor/services/vm_service.py +218 -0
- requestor/ssh/manager.py +62 -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.27.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)
|
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
|
-
#
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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)
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
416
|
-
|
417
|
-
|
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
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
#
|
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
|
-
|
507
|
-
await
|
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
|
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
|
-
#
|
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
|
-
|
554
|
-
await
|
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
|
-
|
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
|
-
|
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
|
-
])
|
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()
|