request-vm-on-golem 0.1.35__tar.gz → 0.1.37__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.35 → request_vm_on_golem-0.1.37}/PKG-INFO +1 -1
  2. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/pyproject.toml +1 -1
  3. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/cli/commands.py +105 -37
  4. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/config.py +44 -11
  5. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/run.py +21 -13
  6. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/README.md +0 -0
  7. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/__init__.py +0 -0
  8. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/api/main.py +0 -0
  9. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/cli/__init__.py +0 -0
  10. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/db/__init__.py +0 -0
  11. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/db/sqlite.py +0 -0
  12. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/errors.py +0 -0
  13. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/provider/__init__.py +0 -0
  14. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/provider/client.py +0 -0
  15. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/services/__init__.py +0 -0
  16. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/services/database_service.py +0 -0
  17. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/services/provider_service.py +0 -0
  18. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/services/ssh_service.py +0 -0
  19. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/services/vm_service.py +0 -0
  20. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/ssh/__init__.py +0 -0
  21. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/ssh/manager.py +0 -0
  22. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/requestor/utils/logging.py +0 -0
  23. {request_vm_on_golem-0.1.35 → request_vm_on_golem-0.1.37}/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.35
3
+ Version: 0.1.37
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.35"
3
+ version = "0.1.37"
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"
@@ -1,6 +1,7 @@
1
1
  """CLI interface for VM on Golem."""
2
2
  import click
3
3
  import asyncio
4
+ import json
4
5
  from typing import Optional
5
6
  from pathlib import Path
6
7
  import subprocess
@@ -13,7 +14,7 @@ except ImportError:
13
14
  # Python < 3.8
14
15
  import importlib_metadata as metadata
15
16
 
16
- from ..config import config
17
+ from ..config import config, ensure_config
17
18
  from ..provider.client import ProviderClient
18
19
  from ..errors import RequestorError
19
20
  from ..utils.logging import setup_logger
@@ -55,6 +56,7 @@ def print_version(ctx, param, value):
55
56
  expose_value=False, is_eager=True, help="Show the version and exit.")
56
57
  def cli():
57
58
  """VM on Golem management CLI"""
59
+ ensure_config()
58
60
  pass
59
61
 
60
62
 
@@ -70,8 +72,9 @@ def vm():
70
72
  @click.option('--storage', type=int, help='Minimum storage (GB) required')
71
73
  @click.option('--country', help='Preferred provider country')
72
74
  @click.option('--driver', type=click.Choice(['central', 'golem-base']), default=None, help='Discovery driver to use')
75
+ @click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
73
76
  @async_command
74
- async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], driver: Optional[str]):
77
+ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], driver: Optional[str], as_json: bool):
75
78
  """List available providers matching requirements."""
76
79
  try:
77
80
  # Log search criteria if any
@@ -85,11 +88,11 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
85
88
  logger.detail(f"Storage: {storage}GB+")
86
89
  if country:
87
90
  logger.detail(f"Country: {country}")
88
-
91
+
89
92
  # Determine the discovery driver being used
90
93
  discovery_driver = driver or config.discovery_driver
91
94
  logger.process(f"Querying discovery service via {discovery_driver}")
92
-
95
+
93
96
  # Initialize provider service
94
97
  provider_service = ProviderService()
95
98
  async with provider_service:
@@ -103,24 +106,31 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
103
106
 
104
107
  if not providers:
105
108
  logger.warning("No providers found matching criteria")
106
- return
109
+ return {"providers": []}
107
110
 
108
- # Format provider information using service with colors
109
- headers = provider_service.provider_headers
110
- rows = await asyncio.gather(*(provider_service.format_provider_row(p, colorize=True) for p in providers))
111
+ result = {"providers": providers}
111
112
 
112
- # Show fancy header
113
- click.echo("\n" + "─" * 80)
114
- click.echo(click.style(f" 🌍 Available Providers ({len(providers)} total)", fg="blue", bold=True))
115
- click.echo("─" * 80)
113
+ if as_json:
114
+ click.echo(json.dumps(result, indent=2))
115
+ else:
116
+ # Format provider information using service with colors
117
+ headers = provider_service.provider_headers
118
+ rows = await asyncio.gather(*(provider_service.format_provider_row(p, colorize=True) for p in providers))
116
119
 
117
- # Show table with colored headers
118
- click.echo("\n" + tabulate(
119
- rows,
120
- headers=[click.style(h, bold=True) for h in headers],
121
- tablefmt="grid"
122
- ))
123
- click.echo("\n" + "─" * 80)
120
+ # Show fancy header
121
+ click.echo("\n" + "─" * 80)
122
+ click.echo(click.style(f" 🌍 Available Providers ({len(providers)} total)", fg="blue", bold=True))
123
+ click.echo("─" * 80)
124
+
125
+ # Show table with colored headers
126
+ click.echo("\n" + tabulate(
127
+ rows,
128
+ headers=[click.style(h, bold=True) for h in headers],
129
+ tablefmt="grid"
130
+ ))
131
+ click.echo("\n" + "─" * 80)
132
+
133
+ return result
124
134
 
125
135
  except Exception as e:
126
136
  logger.error(f"Failed to list providers: {str(e)}")
@@ -274,6 +284,56 @@ async def ssh_vm(name: str):
274
284
  raise click.Abort()
275
285
 
276
286
 
287
+ @vm.command(name='info')
288
+ @click.argument('name')
289
+ @click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
290
+ @async_command
291
+ async def info_vm(name: str, as_json: bool):
292
+ """Show information about a VM."""
293
+ try:
294
+ logger.command(f"ℹ️ Getting info for VM '{name}'")
295
+
296
+ # Initialize VM service
297
+ ssh_service = SSHService(config.ssh_key_dir)
298
+ vm_service = VMService(db_service, ssh_service)
299
+
300
+ # Retrieve VM details
301
+ vm = await vm_service.get_vm(name)
302
+ if not vm:
303
+ raise click.BadParameter(f"VM '{name}' not found")
304
+
305
+ result = vm
306
+
307
+ if as_json:
308
+ click.echo(json.dumps(result, indent=2))
309
+ else:
310
+ headers = [
311
+ "Status",
312
+ "IP Address",
313
+ "SSH Port",
314
+ "CPU",
315
+ "Memory (GB)",
316
+ "Storage (GB)",
317
+ ]
318
+
319
+ row = [
320
+ vm.get("status", "unknown"),
321
+ vm["provider_ip"],
322
+ vm["config"].get("ssh_port", "N/A"),
323
+ vm["config"]["cpu"],
324
+ vm["config"]["memory"],
325
+ vm["config"]["storage"],
326
+ ]
327
+
328
+ click.echo("\n" + tabulate([row], headers=headers, tablefmt="grid"))
329
+
330
+ return result
331
+
332
+ except Exception as e:
333
+ logger.error(f"Failed to get VM info: {str(e)}")
334
+ raise click.Abort()
335
+
336
+
277
337
  @vm.command(name='destroy')
278
338
  @click.argument('name')
279
339
  @async_command
@@ -520,37 +580,45 @@ def run_api_server(host: str, port: int, reload: bool):
520
580
 
521
581
 
522
582
  @vm.command(name='list')
583
+ @click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
523
584
  @async_command
524
- async def list_vms():
585
+ async def list_vms(as_json: bool):
525
586
  """List all VMs."""
526
587
  try:
527
588
  logger.command("📋 Listing your VMs")
528
589
  logger.process("Fetching VM details")
529
-
590
+
530
591
  # Initialize VM service with temporary client (not needed for listing)
531
592
  ssh_service = SSHService(config.ssh_key_dir)
532
593
  vm_service = VMService(db_service, ssh_service, None)
533
594
  vms = await vm_service.list_vms()
534
595
  if not vms:
535
596
  logger.warning("No VMs found")
536
- return
597
+ return {"vms": []}
537
598
 
538
- # Format VM information using service
539
- headers = vm_service.vm_headers
540
- rows = [vm_service.format_vm_row(vm, colorize=True) for vm in vms]
599
+ result = {"vms": vms}
541
600
 
542
- # Show fancy header
543
- click.echo("\n" + "─" * 60)
544
- click.echo(click.style(f" 📋 Your VMs ({len(vms)} total)", fg="blue", bold=True))
545
- click.echo("─" * 60)
546
-
547
- # Show table with colored status
548
- click.echo("\n" + tabulate(
549
- rows,
550
- headers=[click.style(h, bold=True) for h in headers],
551
- tablefmt="grid"
552
- ))
553
- click.echo("\n" + "─" * 60)
601
+ if as_json:
602
+ click.echo(json.dumps(result, indent=2))
603
+ else:
604
+ # Format VM information using service
605
+ headers = vm_service.vm_headers
606
+ rows = [vm_service.format_vm_row(vm, colorize=True) for vm in vms]
607
+
608
+ # Show fancy header
609
+ click.echo("\n" + "─" * 60)
610
+ click.echo(click.style(f" 📋 Your VMs ({len(vms)} total)", fg="blue", bold=True))
611
+ click.echo("─" * 60)
612
+
613
+ # Show table with colored status
614
+ click.echo("\n" + tabulate(
615
+ rows,
616
+ headers=[click.style(h, bold=True) for h in headers],
617
+ tablefmt="grid"
618
+ ))
619
+ click.echo("\n" + "─" * 60)
620
+
621
+ return result
554
622
 
555
623
  except Exception as e:
556
624
  error_msg = str(e)
@@ -1,14 +1,46 @@
1
1
  from pathlib import Path
2
2
  from typing import Optional, Dict
3
3
  import os
4
- from pydantic_settings import BaseSettings
5
- from pydantic import Field, validator
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+ from pydantic import Field, field_validator, ValidationInfo
6
+
7
+
8
+ def ensure_config() -> None:
9
+ """Ensure the requestor configuration directory and defaults exist."""
10
+ base_dir = Path.home() / ".golem" / "requestor"
11
+ ssh_dir = base_dir / "ssh"
12
+ env_file = base_dir / ".env"
13
+ created = False
14
+
15
+ if not base_dir.exists():
16
+ base_dir.mkdir(parents=True, exist_ok=True)
17
+ created = True
18
+ if not ssh_dir.exists():
19
+ ssh_dir.mkdir(parents=True, exist_ok=True)
20
+ created = True
21
+
22
+ if not env_file.exists():
23
+ env_file.write_text("GOLEM_REQUESTOR_ENVIRONMENT=production\n")
24
+ created = True
25
+
26
+ private_key = ssh_dir / "id_rsa"
27
+ public_key = ssh_dir / "id_rsa.pub"
28
+ if not private_key.exists():
29
+ private_key.write_text("placeholder-private-key")
30
+ private_key.chmod(0o600)
31
+ public_key.write_text("placeholder-public-key")
32
+ created = True
33
+
34
+ if created:
35
+ print("Using default settings – run with --help to customize")
36
+
37
+
38
+ ensure_config()
6
39
 
7
40
  class RequestorConfig(BaseSettings):
8
41
  """Configuration settings for the requestor node."""
9
-
10
- class Config:
11
- env_prefix = "GOLEM_REQUESTOR_"
42
+
43
+ model_config = SettingsConfigDict(env_prefix="GOLEM_REQUESTOR_")
12
44
 
13
45
  # Environment
14
46
  environment: str = Field(
@@ -36,10 +68,11 @@ class RequestorConfig(BaseSettings):
36
68
  description="URL of the discovery service (for 'central' driver)"
37
69
  )
38
70
 
39
- @validator("discovery_url", always=True)
40
- def set_discovery_url(cls, v: str, values: dict) -> str:
71
+ @field_validator("discovery_url")
72
+ @classmethod
73
+ def set_discovery_url(cls, v: str, info: ValidationInfo) -> str:
41
74
  """Prefix discovery URL with DEVMODE if in development."""
42
- if values.get("environment") == "development":
75
+ if info.data.get("environment") == "development":
43
76
  return f"DEVMODE-{v}"
44
77
  return v
45
78
 
@@ -63,7 +96,7 @@ class RequestorConfig(BaseSettings):
63
96
 
64
97
  # Base Directory
65
98
  base_dir: Path = Field(
66
- default_factory=lambda: Path.home() / ".golem",
99
+ default_factory=lambda: Path.home() / ".golem" / "requestor",
67
100
  description="Base directory for all Golem requestor files"
68
101
  )
69
102
 
@@ -86,10 +119,10 @@ class RequestorConfig(BaseSettings):
86
119
 
87
120
  # Set dependent paths before validation
88
121
  if 'ssh_key_dir' not in kwargs:
89
- base_dir = kwargs.get('base_dir', Path.home() / ".golem")
122
+ base_dir = kwargs.get('base_dir', Path.home() / ".golem" / "requestor")
90
123
  kwargs['ssh_key_dir'] = base_dir / "ssh"
91
124
  if 'db_path' not in kwargs:
92
- base_dir = kwargs.get('base_dir', Path.home() / ".golem")
125
+ base_dir = kwargs.get('base_dir', Path.home() / ".golem" / "requestor")
93
126
  kwargs['db_path'] = base_dir / "vms.db"
94
127
  super().__init__(**kwargs)
95
128
 
@@ -5,30 +5,36 @@ from pathlib import Path
5
5
  from dotenv import load_dotenv
6
6
 
7
7
  from requestor.utils.logging import setup_logger
8
- from requestor.cli.commands import cli
9
8
 
10
9
  # Configure logging with debug mode from environment variable
11
10
  logger = setup_logger(__name__)
12
11
 
13
- def check_requirements():
14
- """Check if all requirements are met."""
15
- # Check required directories
16
- ssh_key_dir = os.environ.get(
17
- 'GOLEM_REQUESTOR_SSH_KEY_DIR',
18
- str(Path.home() / '.golem' / 'requestor' / 'ssh')
12
+
13
+ def get_ssh_key_dir() -> Path:
14
+ """Return the path to the SSH key directory."""
15
+ return Path(
16
+ os.environ.get(
17
+ "GOLEM_REQUESTOR_SSH_KEY_DIR",
18
+ str(Path.home() / ".golem" / "requestor" / "ssh"),
19
+ )
19
20
  )
20
-
21
+
22
+
23
+ def secure_directory(path: Path) -> bool:
24
+ """Create the directory if needed and set strict permissions."""
21
25
  try:
22
- # Create and secure directories
23
- path = Path(ssh_key_dir)
24
26
  path.mkdir(parents=True, exist_ok=True)
25
- path.chmod(0o700) # Secure permissions for SSH keys
26
- except Exception as e:
27
+ path.chmod(0o700)
28
+ except Exception as e: # pragma: no cover - OS-related failures
27
29
  logger.error(f"Failed to create required directories: {e}")
28
30
  return False
29
-
30
31
  return True
31
32
 
33
+
34
+ def check_requirements() -> bool:
35
+ """Check if all requirements are met."""
36
+ return secure_directory(get_ssh_key_dir())
37
+
32
38
  def main():
33
39
  """Run the requestor CLI."""
34
40
  try:
@@ -44,6 +50,8 @@ def main():
44
50
  sys.exit(1)
45
51
 
46
52
  # Run CLI
53
+ from requestor.cli.commands import cli
54
+
47
55
  cli()
48
56
  except Exception as e:
49
57
  logger.error(f"Failed to start requestor CLI: {e}")