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.
- {golem_vm_provider-0.1.16.dist-info → golem_vm_provider-0.1.18.dist-info}/METADATA +1 -1
- {golem_vm_provider-0.1.16.dist-info → golem_vm_provider-0.1.18.dist-info}/RECORD +7 -6
- provider/config.py +72 -8
- provider/utils/setup.py +100 -0
- provider/vm/cloud_init.py +43 -4
- {golem_vm_provider-0.1.16.dist-info → golem_vm_provider-0.1.18.dist-info}/WHEEL +0 -0
- {golem_vm_provider-0.1.16.dist-info → golem_vm_provider-0.1.18.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=-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=
|
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.
|
24
|
-
golem_vm_provider-0.1.
|
25
|
-
golem_vm_provider-0.1.
|
26
|
-
golem_vm_provider-0.1.
|
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
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
74
|
-
|
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:
|
provider/utils/setup.py
ADDED
@@ -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
|
-
#
|
64
|
-
yaml_content
|
65
|
-
yaml.
|
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
|
-
|
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:
|
File without changes
|
File without changes
|