request-vm-on-golem 0.1.26__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.26.dist-info → request_vm_on_golem-0.1.27.dist-info}/METADATA +1 -1
- {request_vm_on_golem-0.1.26.dist-info → request_vm_on_golem-0.1.27.dist-info}/RECORD +9 -9
- requestor/cli/commands.py +24 -34
- requestor/services/provider_service.py +1 -1
- requestor/services/ssh_service.py +8 -1
- requestor/services/vm_service.py +9 -0
- requestor/ssh/manager.py +62 -0
- {request_vm_on_golem-0.1.26.dist-info → request_vm_on_golem-0.1.27.dist-info}/WHEEL +0 -0
- {request_vm_on_golem-0.1.26.dist-info → request_vm_on_golem-0.1.27.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
|
2
2
|
requestor/api/main.py,sha256=7utCzFNbh5Ol-vsBWeSwT4lXeHD7zdA-GFZuS3rHMWc,2180
|
3
3
|
requestor/cli/__init__.py,sha256=e3E4oEGxmGj-STPtFkQwg_qIWhR0JAiAQdw3G1hXciU,37
|
4
|
-
requestor/cli/commands.py,sha256=
|
4
|
+
requestor/cli/commands.py,sha256=1EtntPcyZqv9rpFAFsL94IG05o3v3xFVSzqGdyPBodI,23351
|
5
5
|
requestor/config.py,sha256=KfAvoAL6jktxTOaJyua5JWGHj7HBHFSrMYSLXl7mRE0,3371
|
6
6
|
requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
|
7
7
|
requestor/db/sqlite.py,sha256=l5pWbx2qlHuar1N_a0B9tVnmumLJY1w5rp3yZ7jmsC0,4146
|
@@ -11,14 +11,14 @@ requestor/provider/client.py,sha256=OUP7CoOCCtKD6DB9eqFkOXK6A2BLFdM4DWSkoulJQxg,
|
|
11
11
|
requestor/run.py,sha256=3w9oTitu8ZESwU5ss5rB3OldqZPfwiY-OPKY7EwIDBo,1609
|
12
12
|
requestor/services/__init__.py,sha256=1qSn_6RMn0KB0A7LCnY2IW6_tC3HBQsdfkFeV-h94eM,172
|
13
13
|
requestor/services/database_service.py,sha256=GlSrzzzd7PSYQJNup00sxkB-B2PMr1__04K8k5QSWvs,2996
|
14
|
-
requestor/services/provider_service.py,sha256=
|
15
|
-
requestor/services/ssh_service.py,sha256=
|
16
|
-
requestor/services/vm_service.py,sha256=
|
14
|
+
requestor/services/provider_service.py,sha256=iejw8Q-ziK3Ny0cEAD1EHejUsAqf9BwJTa7jFmei0_8,9773
|
15
|
+
requestor/services/ssh_service.py,sha256=tcOCtk2SlB9Uuv-P2ghR22e7BJ9kigQh5b4zSGdPFns,4280
|
16
|
+
requestor/services/vm_service.py,sha256=yHvGtfzoWw_wAC7MwgZEudbK8ElxB_2fCuG5Xa-F1KE,6820
|
17
17
|
requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
|
18
|
-
requestor/ssh/manager.py,sha256=
|
18
|
+
requestor/ssh/manager.py,sha256=XhZjz7_BRPnmpu-zxqnGHLCq0b2JZ8Xr8zc1OlMNDkc,9355
|
19
19
|
requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
|
20
20
|
requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
|
21
|
-
request_vm_on_golem-0.1.
|
22
|
-
request_vm_on_golem-0.1.
|
23
|
-
request_vm_on_golem-0.1.
|
24
|
-
request_vm_on_golem-0.1.
|
21
|
+
request_vm_on_golem-0.1.27.dist-info/METADATA,sha256=TyjqzhArp0Stqj6YtUV_Pv82TjV0fYQkUnrrqholjv4,9128
|
22
|
+
request_vm_on_golem-0.1.27.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
23
|
+
request_vm_on_golem-0.1.27.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
|
24
|
+
request_vm_on_golem-0.1.27.dist-info/RECORD,,
|
requestor/cli/commands.py
CHANGED
@@ -298,7 +298,7 @@ async def destroy_vm(name: str):
|
|
298
298
|
|
299
299
|
|
300
300
|
@vm.command(name='purge')
|
301
|
-
@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')
|
302
302
|
@click.confirmation_option(prompt='Are you sure you want to purge all VMs?')
|
303
303
|
@async_command
|
304
304
|
async def purge_vms(force: bool):
|
@@ -306,53 +306,48 @@ async def purge_vms(force: bool):
|
|
306
306
|
try:
|
307
307
|
logger.command("🌪️ Purging all VMs")
|
308
308
|
|
309
|
-
# Get all VMs using database service
|
310
|
-
logger.process("Retrieving all VM details")
|
311
309
|
vms = await db_service.list_vms()
|
312
310
|
if not vms:
|
313
311
|
logger.warning("No VMs found to purge")
|
314
312
|
return
|
315
313
|
|
316
|
-
|
317
|
-
results = {
|
318
|
-
'success': [],
|
319
|
-
'failed': []
|
320
|
-
}
|
314
|
+
results = {'success': [], 'failed': []}
|
321
315
|
|
322
|
-
# Process each VM
|
323
316
|
for vm in vms:
|
324
317
|
try:
|
325
318
|
logger.process(f"Purging VM '{vm['name']}'")
|
326
|
-
|
327
|
-
# Initialize VM service
|
328
319
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
320
|
+
|
329
321
|
async with ProviderClient(provider_url) as client:
|
330
322
|
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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}"))
|
344
|
-
|
323
|
+
await vm_service.destroy_vm(vm['name'])
|
324
|
+
results['success'].append((vm['name'], 'Destroyed successfully'))
|
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
|
+
|
345
331
|
except Exception as e:
|
346
|
-
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.")
|
347
342
|
raise
|
348
|
-
|
343
|
+
else:
|
344
|
+
results['failed'].append((vm['name'], str(e)))
|
349
345
|
|
350
346
|
# Show results
|
351
347
|
click.echo("\n" + "─" * 60)
|
352
348
|
click.echo(click.style(" 🌪️ VM Purge Complete", fg="blue", bold=True))
|
353
349
|
click.echo("─" * 60 + "\n")
|
354
350
|
|
355
|
-
# Success section
|
356
351
|
if results['success']:
|
357
352
|
click.echo(click.style(" ✅ Successfully Purged", fg="green", bold=True))
|
358
353
|
click.echo(" " + "┈" * 25)
|
@@ -360,7 +355,6 @@ async def purge_vms(force: bool):
|
|
360
355
|
click.echo(f" • {click.style(name, fg='cyan')}: {click.style(msg, fg='green')}")
|
361
356
|
click.echo()
|
362
357
|
|
363
|
-
# Failures section
|
364
358
|
if results['failed']:
|
365
359
|
click.echo(click.style(" ❌ Failed to Purge", fg="red", bold=True))
|
366
360
|
click.echo(" " + "┈" * 25)
|
@@ -368,7 +362,6 @@ async def purge_vms(force: bool):
|
|
368
362
|
click.echo(f" • {click.style(name, fg='cyan')}: {click.style(error, fg='red')}")
|
369
363
|
click.echo()
|
370
364
|
|
371
|
-
# Summary
|
372
365
|
total = len(results['success']) + len(results['failed'])
|
373
366
|
success_rate = (len(results['success']) / total) * 100 if total > 0 else 0
|
374
367
|
|
@@ -382,10 +375,7 @@ async def purge_vms(force: bool):
|
|
382
375
|
click.echo("\n" + "─" * 60)
|
383
376
|
|
384
377
|
except Exception as e:
|
385
|
-
|
386
|
-
if "database" in error_msg.lower():
|
387
|
-
error_msg = "Failed to access local database"
|
388
|
-
logger.error(f"Purge operation failed: {error_msg}")
|
378
|
+
logger.error(f"Purge operation failed: {str(e)}")
|
389
379
|
raise click.Abort()
|
390
380
|
|
391
381
|
|
@@ -84,7 +84,7 @@ class ProviderService:
|
|
84
84
|
provider = {
|
85
85
|
'provider_id': annotations.get('golem_provider_id'),
|
86
86
|
'provider_name': annotations.get('golem_provider_name'),
|
87
|
-
'ip_address':
|
87
|
+
'ip_address': annotations.get('golem_ip_address'),
|
88
88
|
'country': annotations.get('golem_country'),
|
89
89
|
'resources': {
|
90
90
|
'cpu': int(annotations.get('golem_cpu', 0)),
|
@@ -19,6 +19,13 @@ class SSHService:
|
|
19
19
|
except Exception as e:
|
20
20
|
raise SSHError(f"Failed to get SSH key pair: {str(e)}")
|
21
21
|
|
22
|
+
def get_key_pair_sync(self):
|
23
|
+
"""Get or create SSH key pair synchronously."""
|
24
|
+
try:
|
25
|
+
return self.ssh_manager.get_key_pair_sync()
|
26
|
+
except Exception as e:
|
27
|
+
raise SSHError(f"Failed to get SSH key pair: {str(e)}")
|
28
|
+
|
22
29
|
def connect_to_vm(
|
23
30
|
self,
|
24
31
|
host: str,
|
@@ -36,7 +43,7 @@ class SSHService:
|
|
36
43
|
"-o", "UserKnownHostsFile=/dev/null",
|
37
44
|
f"{username}@{host}"
|
38
45
|
]
|
39
|
-
subprocess.run(cmd)
|
46
|
+
subprocess.run(cmd, check=True)
|
40
47
|
except Exception as e:
|
41
48
|
raise SSHError(f"Failed to establish SSH connection: {str(e)}")
|
42
49
|
|
requestor/services/vm_service.py
CHANGED
@@ -149,6 +149,13 @@ class VMService:
|
|
149
149
|
"""Format VM information for display."""
|
150
150
|
from click import style
|
151
151
|
|
152
|
+
key_pair = self.ssh_service.get_key_pair_sync()
|
153
|
+
connect_command = self.ssh_service.format_ssh_command(
|
154
|
+
host=vm['provider_ip'],
|
155
|
+
port=vm['config'].get('ssh_port', 'N/A'),
|
156
|
+
private_key_path=key_pair.private_key.absolute()
|
157
|
+
)
|
158
|
+
|
152
159
|
row = [
|
153
160
|
vm['name'],
|
154
161
|
vm['status'],
|
@@ -157,6 +164,7 @@ class VMService:
|
|
157
164
|
vm['config']['cpu'],
|
158
165
|
vm['config']['memory'],
|
159
166
|
vm['config']['storage'],
|
167
|
+
connect_command,
|
160
168
|
vm['created_at']
|
161
169
|
]
|
162
170
|
|
@@ -188,6 +196,7 @@ class VMService:
|
|
188
196
|
"CPU",
|
189
197
|
"Memory (GB)",
|
190
198
|
"Storage (GB)",
|
199
|
+
"Connect Command",
|
191
200
|
"Created"
|
192
201
|
]
|
193
202
|
|
requestor/ssh/manager.py
CHANGED
@@ -93,6 +93,36 @@ class SSHKeyManager:
|
|
93
93
|
public_key_content=golem_pub_key.read_text().strip()
|
94
94
|
)
|
95
95
|
|
96
|
+
def get_key_pair_sync(self) -> KeyPair:
|
97
|
+
"""Get the SSH key pair to use (synchronous version)."""
|
98
|
+
logger.debug("Checking for system SSH key at %s", self.system_key_path)
|
99
|
+
system_pub_key = self.system_key_path.parent / 'id_rsa.pub'
|
100
|
+
|
101
|
+
if self.system_key_path.exists() and system_pub_key.exists():
|
102
|
+
logger.info("Using existing system SSH key")
|
103
|
+
try:
|
104
|
+
return KeyPair(
|
105
|
+
private_key=self.system_key_path,
|
106
|
+
public_key=system_pub_key,
|
107
|
+
private_key_content=self.system_key_path.read_text().strip(),
|
108
|
+
public_key_content=system_pub_key.read_text().strip()
|
109
|
+
)
|
110
|
+
except (PermissionError, OSError) as e:
|
111
|
+
logger.warning("Could not read system SSH key: %s", e)
|
112
|
+
|
113
|
+
logger.debug("Using Golem SSH key at %s", self.golem_key_path)
|
114
|
+
if not self.golem_key_path.exists():
|
115
|
+
logger.info("No existing Golem SSH key found, generating new key pair")
|
116
|
+
self._generate_key_pair_sync()
|
117
|
+
|
118
|
+
golem_pub_key = Path(str(self.golem_key_path) + '.pub')
|
119
|
+
return KeyPair(
|
120
|
+
private_key=self.golem_key_path,
|
121
|
+
public_key=golem_pub_key,
|
122
|
+
private_key_content=self.golem_key_path.read_text().strip(),
|
123
|
+
public_key_content=golem_pub_key.read_text().strip()
|
124
|
+
)
|
125
|
+
|
96
126
|
async def get_public_key_content(self) -> str:
|
97
127
|
"""Get the content of the public key file."""
|
98
128
|
key_pair = await self.get_key_pair()
|
@@ -155,6 +185,38 @@ class SSHKeyManager:
|
|
155
185
|
logger.error("Failed to generate key pair: %s", str(e))
|
156
186
|
raise
|
157
187
|
|
188
|
+
def _generate_key_pair_sync(self):
|
189
|
+
"""Generate a new RSA key pair for Golem VMs (synchronous version)."""
|
190
|
+
logger.debug("Generating new RSA key pair")
|
191
|
+
try:
|
192
|
+
private_key = rsa.generate_private_key(
|
193
|
+
public_exponent=65537,
|
194
|
+
key_size=2048,
|
195
|
+
backend=default_backend()
|
196
|
+
)
|
197
|
+
private_pem = private_key.private_bytes(
|
198
|
+
encoding=serialization.Encoding.PEM,
|
199
|
+
format=serialization.PrivateFormat.PKCS8,
|
200
|
+
encryption_algorithm=serialization.NoEncryption()
|
201
|
+
)
|
202
|
+
self.golem_key_path.write_bytes(private_pem)
|
203
|
+
if os.name == 'posix':
|
204
|
+
os.chmod(self.golem_key_path, 0o600)
|
205
|
+
|
206
|
+
public_key = private_key.public_key()
|
207
|
+
public_pem = public_key.public_bytes(
|
208
|
+
encoding=serialization.Encoding.OpenSSH,
|
209
|
+
format=serialization.PublicFormat.OpenSSH
|
210
|
+
)
|
211
|
+
pub_key_path = Path(str(self.golem_key_path) + '.pub')
|
212
|
+
pub_key_path.write_bytes(public_pem)
|
213
|
+
if os.name == 'posix':
|
214
|
+
os.chmod(pub_key_path, 0o644)
|
215
|
+
logger.info("Successfully generated and saved SSH key pair")
|
216
|
+
except Exception as e:
|
217
|
+
logger.error("Failed to generate key pair: %s", str(e))
|
218
|
+
raise
|
219
|
+
|
158
220
|
async def get_private_key_content(self, force_golem_key: bool = False) -> Optional[str]:
|
159
221
|
"""Get the content of the private key file."""
|
160
222
|
key_pair = await self.get_key_pair(force_golem_key)
|
File without changes
|
{request_vm_on_golem-0.1.26.dist-info → request_vm_on_golem-0.1.27.dist-info}/entry_points.txt
RENAMED
File without changes
|