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.
- {golem_vm_provider-0.1.13.dist-info → golem_vm_provider-0.1.16.dist-info}/METADATA +1 -1
- {golem_vm_provider-0.1.13.dist-info → golem_vm_provider-0.1.16.dist-info}/RECORD +8 -8
- provider/config.py +81 -33
- provider/main.py +4 -32
- provider/vm/cloud_init.py +68 -37
- provider/vm/multipass.py +11 -8
- {golem_vm_provider-0.1.13.dist-info → golem_vm_provider-0.1.16.dist-info}/WHEEL +0 -0
- {golem_vm_provider-0.1.13.dist-info → golem_vm_provider-0.1.16.dist-info}/entry_points.txt +0 -0
@@ -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=
|
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=
|
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=
|
17
|
+
provider/vm/cloud_init.py,sha256=lgOlpiWTtdZi6jbYceA4XERITAfJZjWi12XI3aAIIF8,3267
|
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.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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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.
|
151
|
+
logger.info(f"Checking multipass binary at: {v}")
|
112
152
|
if not validate_path(v):
|
113
|
-
|
114
|
-
|
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.
|
159
|
+
logger.info("No multipass path provided, attempting auto-detection...")
|
118
160
|
system = platform.system().lower()
|
119
|
-
logger.
|
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.
|
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.
|
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.
|
189
|
+
logger.info(f"✓ Found multipass via snap at {snap_path}")
|
148
190
|
return snap_path
|
149
191
|
except subprocess.CalledProcessError:
|
150
|
-
logger.
|
192
|
+
logger.info("✗ Multipass not installed via snap")
|
151
193
|
pass
|
152
194
|
except subprocess.CalledProcessError:
|
153
|
-
logger.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
-
#
|
191
|
-
|
192
|
-
|
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"
|
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
|
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
|
-
|
27
|
+
Tuple of (path to cloud-init configuration file, config_id for debugging)
|
22
28
|
"""
|
23
|
-
config
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
"
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
"
|
36
|
-
"
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
60
|
+
if runcmd:
|
61
|
+
config["runcmd"].extend(runcmd)
|
47
62
|
|
48
|
-
|
49
|
-
|
63
|
+
# Validate YAML before writing
|
64
|
+
yaml_content = yaml.safe_dump(config)
|
65
|
+
yaml.safe_load(yaml_content) # Validate by parsing
|
50
66
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
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
|