golem-vm-provider 0.1.16__py3-none-any.whl → 0.1.18__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
5
5
  Keywords: golem,vm,provider,cloud,decentralized
6
6
  Author: Phillip Jensen
@@ -2,7 +2,7 @@ provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
2
2
  provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
3
3
  provider/api/models.py,sha256=JOzoNf1oE5N97UqTN5xuIrTkqn2tCHqPDaIzGA3jUyo,3513
4
4
  provider/api/routes.py,sha256=P27RQvNqFWn6PacRwr1PaVz-yv5KAWsp9KeORejkXSI,6452
5
- provider/config.py,sha256=tF51xZqu-7x_cmHS3lQaAZorjk7DTZOZuOlU-O8Gk9k,11204
5
+ provider/config.py,sha256=-Cu05ebOjUbhnh5iv3raQ7Z79HMhZ9EcRIRrZVW3Ino,14513
6
6
  provider/discovery/__init__.py,sha256=VR3NRoQtZRH5Vs8FG7jnGLR7p7wn7XeZdLaBb3t8e1g,123
7
7
  provider/discovery/advertiser.py,sha256=yv7RbRf1K43qOLAEa2Olj9hhN8etl2qsBuoHok0xoVs,6784
8
8
  provider/discovery/resource_tracker.py,sha256=8dYhJxoe_jLRwisHoA0jr575YhUKmLIqSXfW88KshcQ,6000
@@ -13,14 +13,15 @@ provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3
13
13
  provider/utils/logging.py,sha256=C_elr0sJROHKQgErYpHJQvfujgh0k4Zf2gg8ZKfrmVk,2590
14
14
  provider/utils/port_display.py,sha256=5d_604Eo-82dqx_yV2ZScq7bKQ8IsXacc-yXC_KAz3A,11031
15
15
  provider/utils/retry.py,sha256=ekP2ucaSJNN-lBcrIvyHa4QYPKNITMl1a5V1X6BBvsw,1560
16
+ provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
16
17
  provider/vm/__init__.py,sha256=JGs50tUmzOR1rQ_w4fMY_3XWylmiA1G7KKWZkVw51mY,501
17
- provider/vm/cloud_init.py,sha256=lgOlpiWTtdZi6jbYceA4XERITAfJZjWi12XI3aAIIF8,3267
18
+ provider/vm/cloud_init.py,sha256=E5dDH7dqStRcJNDfbarBBe83-c9N63W8B5ycIrHI8eU,4627
18
19
  provider/vm/models.py,sha256=zkfvP5Z50SPDNajwZTt9NTDIMRQIsZLvSOsuirHEcJM,6256
19
20
  provider/vm/multipass.py,sha256=FOcsfcJ-NrgBg_fvq_CKOKsQ0xOmk7Z34KXi3ag_Vl8,16603
20
21
  provider/vm/name_mapper.py,sha256=MrshNeJ4Dw-WBsyiIVcn9N5xyOxaBKX4Yqhyh_m5IFg,4103
21
22
  provider/vm/port_manager.py,sha256=d03uwU76vx6LgADMN8ffBT9t400XQ3vtYlXr6cLIFN0,9831
22
23
  provider/vm/proxy_manager.py,sha256=cu0FPPbeCc3CR6NRE_CnLjiRg7xVdSFUylVUOL1g1sI,10154
23
- golem_vm_provider-0.1.16.dist-info/METADATA,sha256=8pEYP425VQlfm1bpPWB5RiQ-1eaftoMAvox-T7VeW_w,10594
24
- golem_vm_provider-0.1.16.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
25
- golem_vm_provider-0.1.16.dist-info/entry_points.txt,sha256=E4rCWo_Do_2zCG_GewNuftfVlHF_8b_OvioZre0dfeA,54
26
- golem_vm_provider-0.1.16.dist-info/RECORD,,
24
+ golem_vm_provider-0.1.18.dist-info/METADATA,sha256=8YfU5Z2xOi0URBANotJQK27x2zJvUz5o2rzVl5hRHt0,10594
25
+ golem_vm_provider-0.1.18.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
26
+ golem_vm_provider-0.1.18.dist-info/entry_points.txt,sha256=E4rCWo_Do_2zCG_GewNuftfVlHF_8b_OvioZre0dfeA,54
27
+ golem_vm_provider-0.1.18.dist-info/RECORD,,
provider/config.py CHANGED
@@ -57,26 +57,90 @@ class Settings(BaseSettings):
57
57
  VM_DATA_DIR: str = ""
58
58
  SSH_KEY_DIR: str = ""
59
59
  CLOUD_INIT_DIR: str = ""
60
+ CLOUD_INIT_FALLBACK_DIR: str = "" # Will be set to a temp directory if needed
60
61
 
61
62
  @validator("CLOUD_INIT_DIR", pre=True)
62
63
  def resolve_cloud_init_dir(cls, v: str) -> str:
63
64
  """Resolve and create cloud-init directory path."""
64
- if not v:
65
- path = Path.home() / ".golem" / "provider" / "cloud-init"
66
- else:
65
+ import platform
66
+ import tempfile
67
+ from .utils.setup import setup_cloud_init_dir, check_setup_needed, mark_setup_complete
68
+
69
+ def verify_dir_permissions(path: Path) -> bool:
70
+ """Verify directory has correct permissions and is accessible."""
71
+ try:
72
+ # Create test file
73
+ test_file = path / "permission_test"
74
+ test_file.write_text("test")
75
+ test_file.unlink()
76
+ return True
77
+ except Exception:
78
+ return False
79
+
80
+ if v:
67
81
  path = Path(v)
68
82
  if not path.is_absolute():
69
83
  path = Path.home() / path
70
-
84
+ else:
85
+ system = platform.system().lower()
86
+ # Try OS-specific paths first
87
+ if system == "linux" and Path("/snap/bin/multipass").exists():
88
+ # Linux with snap
89
+ path = Path("/var/snap/multipass/common/cloud-init")
90
+
91
+ # Check if we need to set up permissions
92
+ if check_setup_needed():
93
+ logger.info("First run detected, setting up cloud-init directory...")
94
+ success, error = setup_cloud_init_dir(path)
95
+ if success:
96
+ logger.info("✓ Cloud-init directory setup complete")
97
+ mark_setup_complete()
98
+ else:
99
+ logger.error(f"Failed to set up cloud-init directory: {error}")
100
+ logger.error("\nTo fix this manually, run these commands:")
101
+ logger.error(" sudo mkdir -p /var/snap/multipass/common/cloud-init")
102
+ logger.error(" sudo chown -R $USER:$USER /var/snap/multipass/common/cloud-init")
103
+ logger.error(" sudo chmod -R 755 /var/snap/multipass/common/cloud-init\n")
104
+ # Fall back to user's home directory
105
+ path = Path.home() / ".local" / "share" / "golem" / "provider" / "cloud-init"
106
+
107
+ elif system == "linux":
108
+ # Linux without snap
109
+ path = Path.home() / ".local" / "share" / "golem" / "provider" / "cloud-init"
110
+ elif system == "darwin":
111
+ # macOS
112
+ path = Path.home() / "Library" / "Application Support" / "golem" / "provider" / "cloud-init"
113
+ elif system == "windows":
114
+ # Windows
115
+ path = Path(os.path.expandvars("%LOCALAPPDATA%")) / "golem" / "provider" / "cloud-init"
116
+ else:
117
+ path = Path.home() / ".golem" / "provider" / "cloud-init"
118
+
71
119
  try:
120
+ # Try to create and verify the directory
72
121
  path.mkdir(parents=True, exist_ok=True)
73
- path.chmod(0o755) # Readable and executable by owner and others, writable by owner
74
- logger.debug(f"Created cloud-init directory at {path}")
122
+ if platform.system().lower() != "windows":
123
+ path.chmod(0o755) # Readable and executable by owner and others, writable by owner
124
+
125
+ if verify_dir_permissions(path):
126
+ logger.debug(f"Created cloud-init directory at {path}")
127
+ return str(path)
128
+
129
+ # If verification fails, fall back to temp directory
130
+ fallback_path = Path(tempfile.gettempdir()) / "golem" / "cloud-init"
131
+ fallback_path.mkdir(parents=True, exist_ok=True)
132
+ if platform.system().lower() != "windows":
133
+ fallback_path.chmod(0o755)
134
+
135
+ if verify_dir_permissions(fallback_path):
136
+ logger.warning(f"Using fallback cloud-init directory at {fallback_path}")
137
+ return str(fallback_path)
138
+
139
+ raise ValueError("Could not create a writable cloud-init directory")
140
+
75
141
  except Exception as e:
76
142
  logger.error(f"Failed to create cloud-init directory at {path}: {e}")
77
143
  raise ValueError(f"Failed to create cloud-init directory: {e}")
78
-
79
- return str(path)
80
144
 
81
145
  @validator("VM_DATA_DIR", pre=True)
82
146
  def resolve_vm_data_dir(cls, v: str) -> str:
@@ -0,0 +1,100 @@
1
+ import os
2
+ import subprocess
3
+ import platform
4
+ from pathlib import Path
5
+ from typing import Tuple
6
+
7
+ from .logging import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+ def run_sudo_command(cmd: str) -> Tuple[bool, str]:
12
+ """Run a command with sudo.
13
+
14
+ Args:
15
+ cmd: Command to run
16
+
17
+ Returns:
18
+ Tuple of (success, error_message)
19
+ """
20
+ try:
21
+ # Try non-interactive sudo first
22
+ result = subprocess.run(
23
+ f"sudo -n {cmd}",
24
+ shell=True,
25
+ capture_output=True,
26
+ text=True
27
+ )
28
+ if result.returncode == 0:
29
+ return True, ""
30
+
31
+ # If that fails, try interactive sudo
32
+ logger.warning("Non-interactive sudo failed, will prompt for password")
33
+ result = subprocess.run(
34
+ f"sudo {cmd}",
35
+ shell=True,
36
+ capture_output=True,
37
+ text=True
38
+ )
39
+ if result.returncode == 0:
40
+ return True, ""
41
+
42
+ return False, result.stderr
43
+
44
+ except Exception as e:
45
+ return False, str(e)
46
+
47
+ def setup_cloud_init_dir(path: Path) -> Tuple[bool, str]:
48
+ """Set up cloud-init directory with correct permissions.
49
+
50
+ Args:
51
+ path: Path to cloud-init directory
52
+
53
+ Returns:
54
+ Tuple of (success, error_message)
55
+ """
56
+ if platform.system().lower() != "linux" or not Path("/snap/bin/multipass").exists():
57
+ # Only needed for Linux with snap
58
+ return True, ""
59
+
60
+ try:
61
+ # Create directory
62
+ success, error = run_sudo_command(f"mkdir -p {path}")
63
+ if not success:
64
+ return False, f"Failed to create directory: {error}"
65
+
66
+ # Set ownership
67
+ user = os.environ.get("USER", os.environ.get("USERNAME"))
68
+ success, error = run_sudo_command(f"chown -R {user}:{user} {path}")
69
+ if not success:
70
+ return False, f"Failed to set ownership: {error}"
71
+
72
+ # Set permissions
73
+ success, error = run_sudo_command(f"chmod -R 755 {path}")
74
+ if not success:
75
+ return False, f"Failed to set permissions: {error}"
76
+
77
+ return True, ""
78
+
79
+ except Exception as e:
80
+ return False, str(e)
81
+
82
+ def check_setup_needed() -> bool:
83
+ """Check if setup is needed.
84
+
85
+ Returns:
86
+ True if setup is needed, False otherwise
87
+ """
88
+ # Only needed for Linux with snap
89
+ if platform.system().lower() != "linux" or not Path("/snap/bin/multipass").exists():
90
+ return False
91
+
92
+ # Check if setup has already been completed
93
+ setup_flag = Path.home() / ".golem" / "provider" / ".setup-complete"
94
+ return not setup_flag.exists()
95
+
96
+ def mark_setup_complete() -> None:
97
+ """Mark setup as complete."""
98
+ setup_flag = Path.home() / ".golem" / "provider" / ".setup-complete"
99
+ setup_flag.parent.mkdir(parents=True, exist_ok=True)
100
+ setup_flag.touch()
provider/vm/cloud_init.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import yaml
2
2
  import os
3
+ import subprocess
3
4
  from datetime import datetime
4
5
  from pathlib import Path
5
6
  from typing import Dict, Optional, Tuple
@@ -9,6 +10,29 @@ from ..utils.logging import setup_logger
9
10
 
10
11
  logger = setup_logger(__name__)
11
12
 
13
+ def validate_cloud_init(content: str) -> bool:
14
+ """Validate cloud-init configuration content.
15
+
16
+ Args:
17
+ content: YAML content to validate
18
+
19
+ Returns:
20
+ True if valid, False otherwise
21
+ """
22
+ try:
23
+ # First validate YAML syntax
24
+ yaml.safe_load(content)
25
+
26
+ # Check for required #cloud-config header
27
+ if not content.startswith("#cloud-config\n"):
28
+ logger.error("Cloud-init config missing #cloud-config header")
29
+ return False
30
+
31
+ return True
32
+ except Exception as e:
33
+ logger.error(f"Cloud-init validation failed: {e}")
34
+ return False
35
+
12
36
  def generate_cloud_init(
13
37
  hostname: str,
14
38
  ssh_key: str,
@@ -32,10 +56,15 @@ def generate_cloud_init(
32
56
 
33
57
  logger.info(f"Generating cloud-init configuration {config_id}")
34
58
  try:
59
+ # Start with required #cloud-config header
60
+ yaml_content = "#cloud-config\n"
61
+
35
62
  config = {
63
+ "version": 1,
36
64
  "hostname": hostname,
37
65
  "package_update": True,
38
66
  "package_upgrade": True,
67
+ "preserve_hostname": False,
39
68
  "ssh_authorized_keys": [ssh_key],
40
69
  "users": [{
41
70
  "name": "root",
@@ -60,16 +89,21 @@ def generate_cloud_init(
60
89
  if runcmd:
61
90
  config["runcmd"].extend(runcmd)
62
91
 
63
- # Validate YAML before writing
64
- yaml_content = yaml.safe_dump(config)
65
- yaml.safe_load(yaml_content) # Validate by parsing
92
+ # Add config to YAML content with document markers
93
+ yaml_content += "---\n"
94
+ yaml_content += yaml.safe_dump(config, default_flow_style=False, sort_keys=False)
95
+
96
+ # Validate the configuration
97
+ if not validate_cloud_init(yaml_content):
98
+ raise Exception("Cloud-init configuration validation failed")
66
99
 
67
100
  # Write to file in our managed directory
68
101
  with open(config_path, 'w') as f:
69
102
  f.write(yaml_content)
70
103
 
71
104
  # Set proper permissions
72
- config_path.chmod(0o644) # World readable but only owner writable
105
+ if os.name != 'nt': # Skip on Windows
106
+ config_path.chmod(0o644) # World readable but only owner writable
73
107
 
74
108
  logger.debug(f"Cloud-init configuration written to {config_path}")
75
109
  logger.debug(f"Cloud-init configuration content:\n{yaml_content}")
@@ -82,6 +116,11 @@ def generate_cloud_init(
82
116
  # Don't cleanup on error - keep file for debugging
83
117
  if config_path.exists():
84
118
  logger.info(f"Failed config preserved at {config_path} for debugging")
119
+ # Log the file contents for debugging
120
+ try:
121
+ logger.debug(f"Failed config contents:\n{config_path.read_text()}")
122
+ except Exception as read_error:
123
+ logger.error(f"Could not read failed config: {read_error}")
85
124
  raise Exception(error_msg)
86
125
 
87
126
  def cleanup_cloud_init(path: str, config_id: str) -> None: