golem-vm-provider 0.1.13__py3-none-any.whl → 0.1.16__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.13
3
+ Version: 0.1.16
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,11 +2,11 @@ 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=41FhWAzPJrbyu1VJC4YOMLl79RzYmzFpOjEuS2MZSm4,9128
5
+ provider/config.py,sha256=tF51xZqu-7x_cmHS3lQaAZorjk7DTZOZuOlU-O8Gk9k,11204
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
9
- provider/main.py,sha256=_kZTDqOlkI7cuFCKhB_WvHN3vlxPpCWqEausdUMzYpM,10232
9
+ provider/main.py,sha256=nVuMxq6npioif4-bFXGjQWJCKdy6O5ZbtX3zxhCM3zI,9206
10
10
  provider/network/port_verifier.py,sha256=AUtBGuZdfq9Jt4BRDuYesh5YEmwneEzYUgIw-uajZhA,12977
11
11
  provider/security/ethereum.py,sha256=SDRDbcjynbVy44kNnxlDcYLL0BZ3Qnc0DvmneQ-WKLE,1383
12
12
  provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
@@ -14,13 +14,13 @@ provider/utils/logging.py,sha256=C_elr0sJROHKQgErYpHJQvfujgh0k4Zf2gg8ZKfrmVk,259
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
16
  provider/vm/__init__.py,sha256=JGs50tUmzOR1rQ_w4fMY_3XWylmiA1G7KKWZkVw51mY,501
17
- provider/vm/cloud_init.py,sha256=o2CWLjl1ZN9fSEFHAWO-glh7BW-DxAMSe0MbqhzKNTg,1709
17
+ provider/vm/cloud_init.py,sha256=lgOlpiWTtdZi6jbYceA4XERITAfJZjWi12XI3aAIIF8,3267
18
18
  provider/vm/models.py,sha256=zkfvP5Z50SPDNajwZTt9NTDIMRQIsZLvSOsuirHEcJM,6256
19
- provider/vm/multipass.py,sha256=RLUqCeoYz4PG8RL7dBu_TzjNEAmgIz9NonBtSuYc4kw,16431
19
+ provider/vm/multipass.py,sha256=FOcsfcJ-NrgBg_fvq_CKOKsQ0xOmk7Z34KXi3ag_Vl8,16603
20
20
  provider/vm/name_mapper.py,sha256=MrshNeJ4Dw-WBsyiIVcn9N5xyOxaBKX4Yqhyh_m5IFg,4103
21
21
  provider/vm/port_manager.py,sha256=d03uwU76vx6LgADMN8ffBT9t400XQ3vtYlXr6cLIFN0,9831
22
22
  provider/vm/proxy_manager.py,sha256=cu0FPPbeCc3CR6NRE_CnLjiRg7xVdSFUylVUOL1g1sI,10154
23
- golem_vm_provider-0.1.13.dist-info/METADATA,sha256=zaGnsTHOW6OEZAOOpl-a3gchR9ESFbDNNfOR35JJUSQ,10594
24
- golem_vm_provider-0.1.13.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
25
- golem_vm_provider-0.1.13.dist-info/entry_points.txt,sha256=E4rCWo_Do_2zCG_GewNuftfVlHF_8b_OvioZre0dfeA,54
26
- golem_vm_provider-0.1.13.dist-info/RECORD,,
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,,
provider/config.py CHANGED
@@ -56,25 +56,65 @@ class Settings(BaseSettings):
56
56
  DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
57
57
  VM_DATA_DIR: str = ""
58
58
  SSH_KEY_DIR: str = ""
59
+ CLOUD_INIT_DIR: str = ""
60
+
61
+ @validator("CLOUD_INIT_DIR", pre=True)
62
+ def resolve_cloud_init_dir(cls, v: str) -> str:
63
+ """Resolve and create cloud-init directory path."""
64
+ if not v:
65
+ path = Path.home() / ".golem" / "provider" / "cloud-init"
66
+ else:
67
+ path = Path(v)
68
+ if not path.is_absolute():
69
+ path = Path.home() / path
70
+
71
+ try:
72
+ 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}")
75
+ except Exception as e:
76
+ logger.error(f"Failed to create cloud-init directory at {path}: {e}")
77
+ raise ValueError(f"Failed to create cloud-init directory: {e}")
78
+
79
+ return str(path)
59
80
 
60
81
  @validator("VM_DATA_DIR", pre=True)
61
82
  def resolve_vm_data_dir(cls, v: str) -> str:
62
- """Resolve VM data directory path."""
83
+ """Resolve and create VM data directory path."""
63
84
  if not v:
64
- return str(Path.home() / ".golem" / "provider" / "vms")
65
- path = Path(v)
66
- if not path.is_absolute():
67
- path = Path.home() / path
85
+ path = Path.home() / ".golem" / "provider" / "vms"
86
+ else:
87
+ path = Path(v)
88
+ if not path.is_absolute():
89
+ path = Path.home() / path
90
+
91
+ try:
92
+ path.mkdir(parents=True, exist_ok=True)
93
+ logger.debug(f"Created VM data directory at {path}")
94
+ except Exception as e:
95
+ logger.error(f"Failed to create VM data directory at {path}: {e}")
96
+ raise ValueError(f"Failed to create VM data directory: {e}")
97
+
68
98
  return str(path)
69
99
 
70
100
  @validator("SSH_KEY_DIR", pre=True)
71
101
  def resolve_ssh_key_dir(cls, v: str) -> str:
72
- """Resolve SSH key directory path."""
102
+ """Resolve and create SSH key directory path with secure permissions."""
73
103
  if not v:
74
- return str(Path.home() / ".golem" / "provider" / "ssh")
75
- path = Path(v)
76
- if not path.is_absolute():
77
- path = Path.home() / path
104
+ path = Path.home() / ".golem" / "provider" / "ssh"
105
+ else:
106
+ path = Path(v)
107
+ if not path.is_absolute():
108
+ path = Path.home() / path
109
+
110
+ try:
111
+ path.mkdir(parents=True, exist_ok=True)
112
+ path.chmod(0o700) # Secure permissions for SSH keys
113
+ logger.debug(f"Created SSH key directory at {path} with secure permissions")
114
+ except Exception as e:
115
+ logger.error(f"Failed to create SSH key directory at {path}: {e}")
116
+ raise ValueError(f"Failed to create SSH key directory: {e}")
117
+
78
118
  return str(path)
79
119
 
80
120
  # Resource Settings
@@ -108,20 +148,22 @@ class Settings(BaseSettings):
108
148
 
109
149
  # If path provided via environment variable, ONLY validate that path
110
150
  if v:
111
- logger.debug(f"Using provided multipass path: {v}")
151
+ logger.info(f"Checking multipass binary at: {v}")
112
152
  if not validate_path(v):
113
- logger.error(f"Provided path {v} is invalid or not executable")
114
- raise ValueError(f"Invalid multipass binary path: {v}")
153
+ msg = f"Invalid multipass binary path: {v} (not found or not executable)"
154
+ logger.error(msg)
155
+ raise ValueError(msg)
156
+ logger.info(f"✓ Found valid multipass binary at: {v}")
115
157
  return v
116
158
 
117
- logger.debug("No multipass path provided, attempting auto-detection")
159
+ logger.info("No multipass path provided, attempting auto-detection...")
118
160
  system = platform.system().lower()
119
- logger.debug(f"Detected OS: {system}")
161
+ logger.info(f"Detected OS: {system}")
120
162
  binary_name = "multipass.exe" if system == "windows" else "multipass"
121
163
 
122
164
  # Try to find multipass based on OS
123
165
  if system == "linux":
124
- logger.debug("Checking for snap installation on Linux")
166
+ logger.info("Checking for snap installation...")
125
167
  # First try to find snap and check if multipass is installed
126
168
  try:
127
169
  # Check if snap exists
@@ -132,7 +174,7 @@ class Settings(BaseSettings):
132
174
  check=True
133
175
  )
134
176
  if snap_result.returncode == 0:
135
- logger.debug("Found snap, checking for multipass installation")
177
+ logger.info("Found snap, checking for multipass installation...")
136
178
  # Check if multipass is installed via snap
137
179
  try:
138
180
  snap_list = subprocess.run(
@@ -144,13 +186,13 @@ class Settings(BaseSettings):
144
186
  if snap_list.returncode == 0:
145
187
  snap_path = "/snap/bin/multipass"
146
188
  if validate_path(snap_path):
147
- logger.debug(f"Found multipass via snap at {snap_path}")
189
+ logger.info(f"Found multipass via snap at {snap_path}")
148
190
  return snap_path
149
191
  except subprocess.CalledProcessError:
150
- logger.debug("Multipass not installed via snap")
192
+ logger.info("Multipass not installed via snap")
151
193
  pass
152
194
  except subprocess.CalledProcessError:
153
- logger.debug("Snap not found")
195
+ logger.info("Snap not found")
154
196
  pass
155
197
 
156
198
  # Common Linux paths if snap installation not found
@@ -159,7 +201,7 @@ class Settings(BaseSettings):
159
201
  "/usr/bin",
160
202
  "/snap/bin"
161
203
  ]
162
- logger.debug(f"Checking common Linux paths: {search_paths}")
204
+ logger.info(f"Checking common Linux paths: {', '.join(search_paths)}")
163
205
 
164
206
  elif system == "darwin": # macOS
165
207
  search_paths = [
@@ -167,7 +209,7 @@ class Settings(BaseSettings):
167
209
  "/usr/local/bin", # Intel Mac
168
210
  "/opt/local/bin" # MacPorts
169
211
  ]
170
- logger.debug(f"Checking macOS paths: {search_paths}")
212
+ logger.info(f"Checking macOS paths: {', '.join(search_paths)}")
171
213
 
172
214
  elif system == "windows":
173
215
  search_paths = [
@@ -175,21 +217,18 @@ class Settings(BaseSettings):
175
217
  os.path.expandvars(r"%ProgramFiles(x86)%\Multipass"),
176
218
  os.path.expandvars(r"%LocalAppData%\Multipass")
177
219
  ]
178
- logger.debug(f"Checking Windows paths: {search_paths}")
220
+ logger.info(f"Checking Windows paths: {', '.join(search_paths)}")
179
221
 
180
222
  else:
181
223
  search_paths = ["/usr/local/bin", "/usr/bin"]
182
- logger.debug(f"Checking default paths: {search_paths}")
224
+ logger.info(f"Checking default paths: {', '.join(search_paths)}")
183
225
 
184
226
  # Search for multipass binary in OS-specific paths
185
227
  for directory in search_paths:
186
228
  path = os.path.join(directory, binary_name)
187
- logger.debug(f"Checking path: {path}")
188
229
  if validate_path(path):
189
- logger.debug(f"Found valid multipass binary at: {path}")
230
+ logger.info(f"Found valid multipass binary at: {path}")
190
231
  return path
191
- else:
192
- logger.debug(f"No valid multipass binary at: {path}")
193
232
 
194
233
  # OS-specific installation instructions
195
234
  if system == "linux":
@@ -224,12 +263,21 @@ class Settings(BaseSettings):
224
263
 
225
264
  @validator("PROXY_STATE_DIR", pre=True)
226
265
  def resolve_proxy_state_dir(cls, v: str) -> str:
227
- """Resolve proxy state directory path."""
266
+ """Resolve and create proxy state directory path."""
228
267
  if not v:
229
- return str(Path.home() / ".golem" / "provider" / "proxy")
230
- path = Path(v)
231
- if not path.is_absolute():
232
- path = Path.home() / path
268
+ path = Path.home() / ".golem" / "provider" / "proxy"
269
+ else:
270
+ path = Path(v)
271
+ if not path.is_absolute():
272
+ path = Path.home() / path
273
+
274
+ try:
275
+ path.mkdir(parents=True, exist_ok=True)
276
+ logger.debug(f"Created proxy state directory at {path}")
277
+ except Exception as e:
278
+ logger.error(f"Failed to create proxy state directory at {path}: {e}")
279
+ raise ValueError(f"Failed to create proxy state directory: {e}")
280
+
233
281
  return str(path)
234
282
 
235
283
  @validator("PUBLIC_IP", pre=True)
provider/main.py CHANGED
@@ -163,41 +163,13 @@ __all__ = ["app", "start"]
163
163
 
164
164
  def check_requirements():
165
165
  """Check if all requirements are met."""
166
- import os
167
- from pathlib import Path
168
-
169
- # Check if multipass is installed
170
- multipass_path = os.environ.get('GOLEM_PROVIDER_MULTIPASS_BINARY_PATH', '/usr/local/bin/multipass')
171
- if not Path(multipass_path).exists():
172
- logger.error(f"Multipass binary not found at {multipass_path}")
173
- return False
174
-
175
- # Check required directories
176
- vm_data_dir = os.environ.get(
177
- 'GOLEM_PROVIDER_VM_DATA_DIR',
178
- str(Path.home() / '.golem' / 'provider' / 'vms')
179
- )
180
- ssh_key_dir = os.environ.get(
181
- 'GOLEM_PROVIDER_SSH_KEY_DIR',
182
- str(Path.home() / '.golem' / 'provider' / 'ssh')
183
- )
184
- proxy_state_dir = os.environ.get(
185
- 'GOLEM_PROVIDER_PROXY_STATE_DIR',
186
- str(Path.home() / '.golem' / 'provider' / 'proxy')
187
- )
188
-
189
166
  try:
190
- # Create and secure directories
191
- for directory in [vm_data_dir, ssh_key_dir, proxy_state_dir]:
192
- path = Path(directory)
193
- path.mkdir(parents=True, exist_ok=True)
194
- if directory == ssh_key_dir:
195
- path.chmod(0o700) # Secure permissions for SSH keys
167
+ # Import settings to trigger validation
168
+ from .config import settings
169
+ return True
196
170
  except Exception as e:
197
- logger.error(f"Failed to create required directories: {e}")
171
+ logger.error(f"Requirements check failed: {e}")
198
172
  return False
199
-
200
- return True
201
173
 
202
174
  async def verify_provider_port(port: int) -> bool:
203
175
  """Verify that the provider port is available for binding.
provider/vm/cloud_init.py CHANGED
@@ -1,14 +1,20 @@
1
1
  import yaml
2
- import tempfile
2
+ import os
3
+ from datetime import datetime
3
4
  from pathlib import Path
4
- from typing import Dict, Optional
5
+ from typing import Dict, Optional, Tuple
6
+
7
+ from ..config import settings
8
+ from ..utils.logging import setup_logger
9
+
10
+ logger = setup_logger(__name__)
5
11
 
6
12
  def generate_cloud_init(
7
13
  hostname: str,
8
14
  ssh_key: str,
9
15
  packages: Optional[list[str]] = None,
10
16
  runcmd: Optional[list[str]] = None
11
- ) -> str:
17
+ ) -> Tuple[str, str]:
12
18
  """Generate cloud-init configuration.
13
19
 
14
20
  Args:
@@ -18,50 +24,75 @@ def generate_cloud_init(
18
24
  runcmd: List of commands to run on first boot
19
25
 
20
26
  Returns:
21
- Path to cloud-init configuration file
27
+ Tuple of (path to cloud-init configuration file, config_id for debugging)
22
28
  """
23
- config = {
24
- "hostname": hostname,
25
- "package_update": True,
26
- "package_upgrade": True,
27
- "ssh_authorized_keys": [ssh_key],
28
- "users": [{
29
- "name": "root",
30
- "ssh_authorized_keys": [ssh_key]
31
- }],
32
- "write_files": [
33
- {
34
- "path": "/etc/ssh/sshd_config.d/allow_root.conf",
35
- "content": "PermitRootLogin prohibit-password\n",
36
- "owner": "root:root",
37
- "permissions": "0644"
38
- }
39
- ],
40
- "runcmd": [
41
- "systemctl restart ssh"
42
- ]
43
- }
29
+ # Generate unique config ID for this cloud-init file
30
+ config_id = f"{hostname}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
31
+ config_path = Path(settings.CLOUD_INIT_DIR) / f"{config_id}.yaml"
32
+
33
+ logger.info(f"Generating cloud-init configuration {config_id}")
34
+ try:
35
+ config = {
36
+ "hostname": hostname,
37
+ "package_update": True,
38
+ "package_upgrade": True,
39
+ "ssh_authorized_keys": [ssh_key],
40
+ "users": [{
41
+ "name": "root",
42
+ "ssh_authorized_keys": [ssh_key]
43
+ }],
44
+ "write_files": [
45
+ {
46
+ "path": "/etc/ssh/sshd_config.d/allow_root.conf",
47
+ "content": "PermitRootLogin prohibit-password\n",
48
+ "owner": "root:root",
49
+ "permissions": "0644"
50
+ }
51
+ ],
52
+ "runcmd": [
53
+ "systemctl restart ssh"
54
+ ]
55
+ }
56
+
57
+ if packages:
58
+ config["packages"] = packages
44
59
 
45
- if packages:
46
- config["packages"] = packages
60
+ if runcmd:
61
+ config["runcmd"].extend(runcmd)
47
62
 
48
- if runcmd:
49
- config["runcmd"].extend(runcmd)
63
+ # Validate YAML before writing
64
+ yaml_content = yaml.safe_dump(config)
65
+ yaml.safe_load(yaml_content) # Validate by parsing
50
66
 
51
- # Create temporary file
52
- temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False)
53
- yaml.safe_dump(config, temp_file)
54
- temp_file.close()
67
+ # Write to file in our managed directory
68
+ with open(config_path, 'w') as f:
69
+ f.write(yaml_content)
70
+
71
+ # Set proper permissions
72
+ config_path.chmod(0o644) # World readable but only owner writable
73
+
74
+ logger.debug(f"Cloud-init configuration written to {config_path}")
75
+ logger.debug(f"Cloud-init configuration content:\n{yaml_content}")
76
+
77
+ return str(config_path), config_id
55
78
 
56
- return temp_file.name
79
+ except Exception as e:
80
+ error_msg = f"Failed to generate cloud-init configuration: {str(e)}"
81
+ logger.error(f"{error_msg}\nConfig ID: {config_id}")
82
+ # Don't cleanup on error - keep file for debugging
83
+ if config_path.exists():
84
+ logger.info(f"Failed config preserved at {config_path} for debugging")
85
+ raise Exception(error_msg)
57
86
 
58
- def cleanup_cloud_init(path: str) -> None:
87
+ def cleanup_cloud_init(path: str, config_id: str) -> None:
59
88
  """Clean up cloud-init configuration file.
60
89
 
61
90
  Args:
62
91
  path: Path to cloud-init configuration file
92
+ config_id: Configuration ID for logging
63
93
  """
64
94
  try:
65
95
  Path(path).unlink()
66
- except Exception:
67
- pass
96
+ logger.debug(f"Cleaned up cloud-init configuration {config_id}")
97
+ except Exception as e:
98
+ logger.warning(f"Failed to cleanup cloud-init configuration {config_id}: {e}")
provider/vm/multipass.py CHANGED
@@ -158,20 +158,22 @@ class MultipassProvider(VMProvider):
158
158
  """
159
159
  multipass_name = f"golem-{config.name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
160
160
  await self.name_mapper.add_mapping(config.name, multipass_name)
161
+ cloud_init_path = None
162
+ config_id = None
161
163
 
162
164
  # Verify resources are properly allocated
163
165
  if not self.resource_tracker.can_accept_resources(config.resources):
164
166
  raise VMCreateError("Resources not properly allocated or insufficient")
165
167
 
166
- # Generate cloud-init config with requestor's public key
167
- cloud_init_path = generate_cloud_init(
168
- hostname=config.name,
169
- ssh_key=config.ssh_key
170
- )
171
-
172
168
  try:
169
+ # Generate cloud-init config with requestor's public key
170
+ cloud_init_path, config_id = generate_cloud_init(
171
+ hostname=config.name,
172
+ ssh_key=config.ssh_key
173
+ )
174
+
173
175
  # Launch VM
174
- logger.process(f"🚀 Launching VM {multipass_name}")
176
+ logger.process(f"🚀 Launching VM {multipass_name} with config {config_id}")
175
177
  launch_cmd = [
176
178
  "launch",
177
179
  config.image,
@@ -237,7 +239,8 @@ class MultipassProvider(VMProvider):
237
239
 
238
240
  finally:
239
241
  # Cleanup cloud-init file
240
- cleanup_cloud_init(cloud_init_path)
242
+ if cloud_init_path and config_id:
243
+ cleanup_cloud_init(cloud_init_path, config_id)
241
244
 
242
245
  def _verify_vm_exists(self, vm_id: str) -> bool:
243
246
  """Check if VM exists in multipass.