request-vm-on-golem 0.1.26__tar.gz → 0.1.27__tar.gz

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.
Files changed (23) hide show
  1. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/PKG-INFO +1 -1
  2. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/pyproject.toml +1 -1
  3. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/cli/commands.py +24 -34
  4. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/services/provider_service.py +1 -1
  5. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/services/ssh_service.py +8 -1
  6. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/services/vm_service.py +9 -0
  7. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/ssh/manager.py +62 -0
  8. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/README.md +0 -0
  9. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/__init__.py +0 -0
  10. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/api/main.py +0 -0
  11. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/cli/__init__.py +0 -0
  12. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/config.py +0 -0
  13. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/db/__init__.py +0 -0
  14. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/db/sqlite.py +0 -0
  15. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/errors.py +0 -0
  16. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/provider/__init__.py +0 -0
  17. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/provider/client.py +0 -0
  18. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/run.py +0 -0
  19. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/services/__init__.py +0 -0
  20. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/services/database_service.py +0 -0
  21. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/ssh/__init__.py +0 -0
  22. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/utils/logging.py +0 -0
  23. {request_vm_on_golem-0.1.26 → request_vm_on_golem-0.1.27}/requestor/utils/spinner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: request-vm-on-golem
3
- Version: 0.1.26
3
+ Version: 0.1.27
4
4
  Summary: VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network
5
5
  Keywords: golem,vm,cloud,decentralized,cli
6
6
  Author: Phillip Jensen
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "request-vm-on-golem"
3
- version = "0.1.26"
3
+ version = "0.1.27"
4
4
  description = "VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network"
5
5
  authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
6
6
  readme = "README.md"
@@ -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
- # Track results
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
- 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}"))
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 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.")
347
342
  raise
348
- results['failed'].append((vm['name'], str(e)))
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
- error_msg = str(e)
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': '127.0.0.1' if 'DEVMODE' in annotations.get('golem_provider_name', '') else annotations.get('golem_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
 
@@ -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
 
@@ -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)