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.
- {golem_vm_provider-0.1.14.dist-info → golem_vm_provider-0.1.17.dist-info}/METADATA +1 -1
- {golem_vm_provider-0.1.14.dist-info → golem_vm_provider-0.1.17.dist-info}/RECORD +7 -7
- provider/config.py +67 -0
- provider/vm/cloud_init.py +107 -37
- provider/vm/multipass.py +11 -8
- {golem_vm_provider-0.1.14.dist-info → golem_vm_provider-0.1.17.dist-info}/WHEEL +0 -0
- {golem_vm_provider-0.1.14.dist-info → golem_vm_provider-0.1.17.dist-info}/entry_points.txt +0 -0
@@ -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=
|
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=
|
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=
|
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.
|
24
|
-
golem_vm_provider-0.1.
|
25
|
-
golem_vm_provider-0.1.
|
26
|
-
golem_vm_provider-0.1.
|
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
|
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
|
-
|
51
|
+
Tuple of (path to cloud-init configuration file, config_id for debugging)
|
22
52
|
"""
|
23
|
-
config
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
96
|
+
# Validate the configuration
|
97
|
+
if not validate_cloud_init(yaml_content):
|
98
|
+
raise Exception("Cloud-init configuration validation failed")
|
50
99
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
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.
|
File without changes
|
File without changes
|