golem-vm-provider 0.1.14__py3-none-any.whl → 0.1.17__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.14
3
+ Version: 0.1.17
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=tq23K-FHjqQM0cu9GIjKi6_a96YHCEyqpprMwzK36oI,10344
5
+ provider/config.py,sha256=btSyU6cB6z8o9nHWpNEOBDxKqsEha1yStM-6of3Np0s,13237
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
@@ -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=E5dDH7dqStRcJNDfbarBBe83-c9N63W8B5ycIrHI8eU,4627
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.14.dist-info/METADATA,sha256=a5GRzycDL9296fLDQnI1XHKA-gDOq10WVgNifEve5Ts,10594
24
- golem_vm_provider-0.1.14.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
25
- golem_vm_provider-0.1.14.dist-info/entry_points.txt,sha256=E4rCWo_Do_2zCG_GewNuftfVlHF_8b_OvioZre0dfeA,54
26
- golem_vm_provider-0.1.14.dist-info/RECORD,,
23
+ golem_vm_provider-0.1.17.dist-info/METADATA,sha256=Lu9EpHlgFmOQP56l89vLyowyM0j48TI5BTGfT_EQxtQ,10594
24
+ golem_vm_provider-0.1.17.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
25
+ golem_vm_provider-0.1.17.dist-info/entry_points.txt,sha256=E4rCWo_Do_2zCG_GewNuftfVlHF_8b_OvioZre0dfeA,54
26
+ golem_vm_provider-0.1.17.dist-info/RECORD,,
provider/config.py CHANGED
@@ -56,6 +56,73 @@ 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
+ CLOUD_INIT_FALLBACK_DIR: str = "" # Will be set to a temp directory if needed
61
+
62
+ @validator("CLOUD_INIT_DIR", pre=True)
63
+ def resolve_cloud_init_dir(cls, v: str) -> str:
64
+ """Resolve and create cloud-init directory path."""
65
+ import platform
66
+ import tempfile
67
+
68
+ def verify_dir_permissions(path: Path) -> bool:
69
+ """Verify directory has correct permissions and is accessible."""
70
+ try:
71
+ # Create test file
72
+ test_file = path / "permission_test"
73
+ test_file.write_text("test")
74
+ test_file.unlink()
75
+ return True
76
+ except Exception:
77
+ return False
78
+
79
+ if v:
80
+ path = Path(v)
81
+ if not path.is_absolute():
82
+ path = Path.home() / path
83
+ else:
84
+ system = platform.system().lower()
85
+ # Try OS-specific paths first
86
+ if system == "linux" and Path("/snap/bin/multipass").exists():
87
+ # Linux with snap
88
+ path = Path("/var/snap/multipass/common/cloud-init")
89
+ elif system == "linux":
90
+ # Linux without snap
91
+ path = Path("/var/lib/multipass/cloud-init")
92
+ elif system == "darwin":
93
+ # macOS
94
+ path = Path("/Library/Application Support/multipass/cloud-init")
95
+ elif system == "windows":
96
+ # Windows
97
+ path = Path(os.path.expandvars("%ProgramData%\\Multipass\\cloud-init"))
98
+ else:
99
+ path = Path.home() / ".golem" / "provider" / "cloud-init"
100
+
101
+ try:
102
+ # Try to create and verify the directory
103
+ path.mkdir(parents=True, exist_ok=True)
104
+ if platform.system().lower() != "windows":
105
+ path.chmod(0o755) # Readable and executable by owner and others, writable by owner
106
+
107
+ if verify_dir_permissions(path):
108
+ logger.debug(f"Created cloud-init directory at {path}")
109
+ return str(path)
110
+
111
+ # If verification fails, fall back to temp directory
112
+ fallback_path = Path(tempfile.gettempdir()) / "golem" / "cloud-init"
113
+ fallback_path.mkdir(parents=True, exist_ok=True)
114
+ if platform.system().lower() != "windows":
115
+ fallback_path.chmod(0o755)
116
+
117
+ if verify_dir_permissions(fallback_path):
118
+ logger.warning(f"Using fallback cloud-init directory at {fallback_path}")
119
+ return str(fallback_path)
120
+
121
+ raise ValueError("Could not create a writable cloud-init directory")
122
+
123
+ except Exception as e:
124
+ logger.error(f"Failed to create cloud-init directory at {path}: {e}")
125
+ raise ValueError(f"Failed to create cloud-init directory: {e}")
59
126
 
60
127
  @validator("VM_DATA_DIR", pre=True)
61
128
  def resolve_vm_data_dir(cls, v: str) -> str:
provider/vm/cloud_init.py CHANGED
@@ -1,14 +1,44 @@
1
1
  import yaml
2
- import tempfile
2
+ import os
3
+ import subprocess
4
+ from datetime import datetime
3
5
  from pathlib import Path
4
- from typing import Dict, Optional
6
+ from typing import Dict, Optional, Tuple
7
+
8
+ from ..config import settings
9
+ from ..utils.logging import setup_logger
10
+
11
+ logger = setup_logger(__name__)
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
5
35
 
6
36
  def generate_cloud_init(
7
37
  hostname: str,
8
38
  ssh_key: str,
9
39
  packages: Optional[list[str]] = None,
10
40
  runcmd: Optional[list[str]] = None
11
- ) -> str:
41
+ ) -> Tuple[str, str]:
12
42
  """Generate cloud-init configuration.
13
43
 
14
44
  Args:
@@ -18,50 +48,90 @@ def generate_cloud_init(
18
48
  runcmd: List of commands to run on first boot
19
49
 
20
50
  Returns:
21
- Path to cloud-init configuration file
51
+ Tuple of (path to cloud-init configuration file, config_id for debugging)
22
52
  """
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
- }
53
+ # Generate unique config ID for this cloud-init file
54
+ config_id = f"{hostname}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
55
+ config_path = Path(settings.CLOUD_INIT_DIR) / f"{config_id}.yaml"
56
+
57
+ logger.info(f"Generating cloud-init configuration {config_id}")
58
+ try:
59
+ # Start with required #cloud-config header
60
+ yaml_content = "#cloud-config\n"
61
+
62
+ config = {
63
+ "version": 1,
64
+ "hostname": hostname,
65
+ "package_update": True,
66
+ "package_upgrade": True,
67
+ "preserve_hostname": False,
68
+ "ssh_authorized_keys": [ssh_key],
69
+ "users": [{
70
+ "name": "root",
71
+ "ssh_authorized_keys": [ssh_key]
72
+ }],
73
+ "write_files": [
74
+ {
75
+ "path": "/etc/ssh/sshd_config.d/allow_root.conf",
76
+ "content": "PermitRootLogin prohibit-password\n",
77
+ "owner": "root:root",
78
+ "permissions": "0644"
79
+ }
80
+ ],
81
+ "runcmd": [
82
+ "systemctl restart ssh"
83
+ ]
84
+ }
85
+
86
+ if packages:
87
+ config["packages"] = packages
88
+
89
+ if runcmd:
90
+ config["runcmd"].extend(runcmd)
44
91
 
45
- if packages:
46
- config["packages"] = packages
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)
47
95
 
48
- if runcmd:
49
- config["runcmd"].extend(runcmd)
96
+ # Validate the configuration
97
+ if not validate_cloud_init(yaml_content):
98
+ raise Exception("Cloud-init configuration validation failed")
50
99
 
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()
100
+ # Write to file in our managed directory
101
+ with open(config_path, 'w') as f:
102
+ f.write(yaml_content)
103
+
104
+ # Set proper permissions
105
+ if os.name != 'nt': # Skip on Windows
106
+ config_path.chmod(0o644) # World readable but only owner writable
107
+
108
+ logger.debug(f"Cloud-init configuration written to {config_path}")
109
+ logger.debug(f"Cloud-init configuration content:\n{yaml_content}")
110
+
111
+ return str(config_path), config_id
55
112
 
56
- return temp_file.name
113
+ except Exception as e:
114
+ error_msg = f"Failed to generate cloud-init configuration: {str(e)}"
115
+ logger.error(f"{error_msg}\nConfig ID: {config_id}")
116
+ # Don't cleanup on error - keep file for debugging
117
+ if config_path.exists():
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}")
124
+ raise Exception(error_msg)
57
125
 
58
- def cleanup_cloud_init(path: str) -> None:
126
+ def cleanup_cloud_init(path: str, config_id: str) -> None:
59
127
  """Clean up cloud-init configuration file.
60
128
 
61
129
  Args:
62
130
  path: Path to cloud-init configuration file
131
+ config_id: Configuration ID for logging
63
132
  """
64
133
  try:
65
134
  Path(path).unlink()
66
- except Exception:
67
- pass
135
+ logger.debug(f"Cleaned up cloud-init configuration {config_id}")
136
+ except Exception as e:
137
+ 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.