golem-vm-provider 0.1.20__tar.gz → 0.1.22__tar.gz
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.20 → golem_vm_provider-0.1.22}/PKG-INFO +1 -1
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/config.py +3 -3
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/main.py +25 -9
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/network/port_verifier.py +9 -3
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/port_manager.py +15 -4
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/proxy_manager.py +109 -32
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/pyproject.toml +1 -1
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/README.md +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/__init__.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/api/__init__.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/api/models.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/api/routes.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/__init__.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/advertiser.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/resource_tracker.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/security/ethereum.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/ascii_art.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/logging.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/port_display.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/retry.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/setup.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/__init__.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/cloud_init.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/models.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/multipass.py +0 -0
- {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/name_mapper.py +0 -0
@@ -277,9 +277,9 @@ class Settings(BaseSettings):
|
|
277
277
|
|
278
278
|
elif system == "windows":
|
279
279
|
search_paths = [
|
280
|
-
os.path.expandvars(r"%ProgramFiles
|
281
|
-
os.path.expandvars(r"%ProgramFiles(x86)
|
282
|
-
os.path.expandvars(r"%LocalAppData
|
280
|
+
os.path.join(os.path.expandvars(r"%ProgramFiles%"), "Multipass", "bin"),
|
281
|
+
os.path.join(os.path.expandvars(r"%ProgramFiles(x86)%"), "Multipass", "bin"),
|
282
|
+
os.path.join(os.path.expandvars(r"%LocalAppData%"), "Multipass", "bin")
|
283
283
|
]
|
284
284
|
logger.info(f"Checking Windows paths: {', '.join(search_paths)}")
|
285
285
|
|
@@ -18,28 +18,44 @@ app = FastAPI(title="VM on Golem Provider")
|
|
18
18
|
async def setup_provider() -> None:
|
19
19
|
"""Setup and initialize the provider components."""
|
20
20
|
try:
|
21
|
-
#
|
22
|
-
port_manager = app.state.port_manager
|
23
|
-
|
24
|
-
# Create resource tracker
|
21
|
+
# Create resource tracker first
|
25
22
|
logger.process("🔄 Initializing resource tracker...")
|
26
23
|
resource_tracker = ResourceTracker()
|
27
24
|
app.state.resource_tracker = resource_tracker
|
28
|
-
|
29
|
-
# Create provider with resource tracker and port manager
|
25
|
+
|
26
|
+
# Create provider with resource tracker and temporary port manager
|
30
27
|
logger.process("🔄 Initializing VM provider...")
|
31
|
-
provider = MultipassProvider(resource_tracker, port_manager=
|
28
|
+
provider = MultipassProvider(resource_tracker, port_manager=None) # Will be set later
|
29
|
+
|
32
30
|
try:
|
31
|
+
# Initialize provider (without port operations)
|
33
32
|
await asyncio.wait_for(provider.initialize(), timeout=30)
|
34
33
|
|
35
|
-
# Store provider
|
34
|
+
# Store provider reference
|
36
35
|
app.state.provider = provider
|
37
36
|
app.state.proxy_manager = provider.proxy_manager
|
38
37
|
|
39
|
-
# Restore proxy configurations
|
38
|
+
# Restore proxy configurations first
|
40
39
|
logger.process("🔄 Restoring proxy configurations...")
|
41
40
|
await app.state.proxy_manager._load_state()
|
42
41
|
|
42
|
+
# Now initialize port manager with knowledge of restored proxies
|
43
|
+
logger.process("🔄 Initializing port manager...")
|
44
|
+
port_manager = PortManager(
|
45
|
+
start_port=settings.PORT_RANGE_START,
|
46
|
+
end_port=settings.PORT_RANGE_END,
|
47
|
+
discovery_port=settings.PORT,
|
48
|
+
existing_ports=app.state.proxy_manager.get_active_ports()
|
49
|
+
)
|
50
|
+
|
51
|
+
if not await port_manager.initialize():
|
52
|
+
raise RuntimeError("Port verification failed")
|
53
|
+
|
54
|
+
# Update provider and proxy manager with verified port manager
|
55
|
+
app.state.port_manager = port_manager
|
56
|
+
provider.port_manager = port_manager
|
57
|
+
app.state.proxy_manager.port_manager = port_manager
|
58
|
+
|
43
59
|
except asyncio.TimeoutError:
|
44
60
|
logger.error("Provider initialization timed out")
|
45
61
|
raise
|
@@ -198,15 +198,21 @@ class PortVerifier:
|
|
198
198
|
))
|
199
199
|
logger.warning(error_msg)
|
200
200
|
|
201
|
-
# If no successful
|
202
|
-
if not any(
|
201
|
+
# If no servers were successful, fail verification
|
202
|
+
if not any(attempt.success for attempt in attempts):
|
203
203
|
error_msg = (
|
204
|
-
"Failed to
|
204
|
+
"Failed to connect to any port check servers. Please ensure:\n"
|
205
205
|
"1. At least one port check server is running and accessible\n"
|
206
206
|
"2. Your network connection is stable\n"
|
207
207
|
"3. The server URLs are correct"
|
208
208
|
)
|
209
209
|
logger.error(error_msg)
|
210
|
+
raise RuntimeError(error_msg)
|
211
|
+
|
212
|
+
# If no successful verifications but servers were reachable, mark ports as inaccessible
|
213
|
+
if not any(result.accessible for result in results.values()):
|
214
|
+
error_msg = "No ports were verified as accessible"
|
215
|
+
logger.error(error_msg)
|
210
216
|
results = {
|
211
217
|
port: PortVerificationResult(
|
212
218
|
port=port,
|
@@ -23,7 +23,8 @@ class PortManager:
|
|
23
23
|
end_port: int = 50900,
|
24
24
|
state_file: Optional[str] = None,
|
25
25
|
port_check_servers: Optional[List[str]] = None,
|
26
|
-
discovery_port: Optional[int] = None
|
26
|
+
discovery_port: Optional[int] = None,
|
27
|
+
existing_ports: Optional[Set[int]] = None
|
27
28
|
):
|
28
29
|
"""Initialize the port manager.
|
29
30
|
|
@@ -32,6 +33,8 @@ class PortManager:
|
|
32
33
|
end_port: End of port range (exclusive)
|
33
34
|
state_file: Path to persist port assignments
|
34
35
|
port_check_servers: List of URLs for port checking services
|
36
|
+
discovery_port: Port used for discovery service
|
37
|
+
existing_ports: Set of ports that should be considered in use
|
35
38
|
"""
|
36
39
|
self.start_port = start_port
|
37
40
|
self.end_port = end_port
|
@@ -40,12 +43,12 @@ class PortManager:
|
|
40
43
|
self.lock = Lock()
|
41
44
|
self._used_ports: dict[str, int] = {} # vm_id -> port
|
42
45
|
self.verified_ports: Set[int] = set()
|
46
|
+
self._existing_ports = existing_ports or set()
|
43
47
|
|
44
48
|
# Initialize port verifier with default servers
|
45
49
|
self.port_check_servers = port_check_servers or [
|
46
50
|
"http://localhost:9000", # Local development server
|
47
51
|
"http://195.201.39.101:9000", # Production servers
|
48
|
-
|
49
52
|
]
|
50
53
|
self.discovery_port = discovery_port or settings.PORT
|
51
54
|
self.port_verifier = PortVerifier(
|
@@ -53,7 +56,14 @@ class PortManager:
|
|
53
56
|
discovery_port=self.discovery_port
|
54
57
|
)
|
55
58
|
|
59
|
+
# Load state after setting existing ports
|
56
60
|
self._load_state()
|
61
|
+
|
62
|
+
# Mark existing ports as used and remove from verified ports
|
63
|
+
for port in self._existing_ports:
|
64
|
+
if port in self.verified_ports:
|
65
|
+
self.verified_ports.remove(port)
|
66
|
+
logger.debug(f"Marked port {port} as in use from existing ports")
|
57
67
|
|
58
68
|
async def initialize(self) -> bool:
|
59
69
|
"""Initialize port manager with verification.
|
@@ -70,8 +80,9 @@ class PortManager:
|
|
70
80
|
)
|
71
81
|
display.print_header()
|
72
82
|
|
73
|
-
# Only verify
|
74
|
-
|
83
|
+
# Only verify ports that aren't already marked as in use
|
84
|
+
available_ports = set(range(self.start_port, self.end_port)) - self._existing_ports
|
85
|
+
ssh_ports = list(available_ports)
|
75
86
|
logger.info(f"Starting port verification...")
|
76
87
|
logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
|
77
88
|
logger.info(
|
@@ -3,7 +3,7 @@ import json
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Optional, Dict
|
6
|
+
from typing import Optional, Dict, Set
|
7
7
|
from asyncio import Task, Transport, Protocol
|
8
8
|
|
9
9
|
from .port_manager import PortManager
|
@@ -146,14 +146,14 @@ class PythonProxyManager:
|
|
146
146
|
|
147
147
|
def __init__(
|
148
148
|
self,
|
149
|
-
port_manager: PortManager,
|
149
|
+
port_manager: Optional[PortManager],
|
150
150
|
name_mapper: "VMNameMapper",
|
151
151
|
state_file: Optional[str] = None
|
152
152
|
):
|
153
153
|
"""Initialize the proxy manager.
|
154
154
|
|
155
155
|
Args:
|
156
|
-
port_manager: Port allocation manager
|
156
|
+
port_manager: Port allocation manager (optional during startup)
|
157
157
|
name_mapper: VM name mapping manager
|
158
158
|
state_file: Path to persist proxy state
|
159
159
|
"""
|
@@ -161,53 +161,130 @@ class PythonProxyManager:
|
|
161
161
|
self.name_mapper = name_mapper
|
162
162
|
self.state_file = state_file or os.path.expanduser("~/.golem/provider/proxy_state.json")
|
163
163
|
self._proxies: Dict[str, ProxyServer] = {} # multipass_name -> ProxyServer
|
164
|
-
|
164
|
+
self._state_version = 1 # For future state schema migrations
|
165
|
+
self._active_ports: Dict[str, int] = {} # multipass_name -> port
|
165
166
|
|
167
|
+
def get_active_ports(self) -> Set[int]:
|
168
|
+
"""Get set of ports that should be considered in use.
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
Set of ports that are allocated to VMs
|
172
|
+
"""
|
173
|
+
return set(self._active_ports.values())
|
174
|
+
|
166
175
|
async def _load_state(self) -> None:
|
167
176
|
"""Load and restore proxy state from file."""
|
168
177
|
try:
|
169
178
|
state_path = Path(self.state_file)
|
170
|
-
if state_path.exists():
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
179
|
+
if not state_path.exists():
|
180
|
+
return
|
181
|
+
|
182
|
+
with open(state_path, 'r') as f:
|
183
|
+
state = json.load(f)
|
184
|
+
|
185
|
+
# Check state version for future migrations
|
186
|
+
if state.get('version', 1) != self._state_version:
|
187
|
+
logger.warning(f"State version mismatch: {state.get('version')} != {self._state_version}")
|
188
|
+
|
189
|
+
# First load all port allocations
|
190
|
+
for requestor_name, proxy_info in state.get('proxies', {}).items():
|
191
|
+
multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
|
192
|
+
if multipass_name:
|
193
|
+
self._active_ports[multipass_name] = proxy_info['port']
|
194
|
+
|
195
|
+
# Then attempt to restore proxies with retries
|
196
|
+
restore_tasks = []
|
197
|
+
for requestor_name, proxy_info in state.get('proxies', {}).items():
|
198
|
+
multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
|
199
|
+
if multipass_name:
|
200
|
+
task = self._restore_proxy_with_retry(
|
201
|
+
multipass_name=multipass_name,
|
202
|
+
vm_ip=proxy_info['target'],
|
203
|
+
port=proxy_info['port']
|
204
|
+
)
|
205
|
+
restore_tasks.append(task)
|
206
|
+
else:
|
207
|
+
logger.warning(f"No multipass name found for requestor VM {requestor_name}")
|
208
|
+
|
209
|
+
# Wait for all restore attempts
|
210
|
+
if restore_tasks:
|
211
|
+
results = await asyncio.gather(*restore_tasks, return_exceptions=True)
|
212
|
+
successful = sum(1 for r in results if r is True)
|
213
|
+
logger.info(f"Restored {successful}/{len(state.get('proxies', {}))} proxy configurations")
|
214
|
+
|
194
215
|
except Exception as e:
|
195
216
|
logger.error(f"Failed to load proxy state: {e}")
|
217
|
+
|
218
|
+
async def _restore_proxy_with_retry(
|
219
|
+
self,
|
220
|
+
multipass_name: str,
|
221
|
+
vm_ip: str,
|
222
|
+
port: int,
|
223
|
+
max_retries: int = 3,
|
224
|
+
initial_delay: float = 1.0
|
225
|
+
) -> bool:
|
226
|
+
"""Attempt to restore a proxy with exponential backoff retry.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
multipass_name: Multipass VM name
|
230
|
+
vm_ip: VM IP address
|
231
|
+
port: Port to use
|
232
|
+
max_retries: Maximum number of retry attempts
|
233
|
+
initial_delay: Initial delay between retries (doubles each attempt)
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
bool: True if restoration was successful
|
237
|
+
"""
|
238
|
+
delay = initial_delay
|
239
|
+
for attempt in range(max_retries):
|
240
|
+
try:
|
241
|
+
if attempt > 0:
|
242
|
+
logger.info(f"Retry attempt {attempt + 1} for {multipass_name} on port {port}")
|
243
|
+
await asyncio.sleep(delay)
|
244
|
+
delay *= 2 # Exponential backoff
|
245
|
+
|
246
|
+
# Attempt to create proxy
|
247
|
+
proxy = ProxyServer(port, vm_ip)
|
248
|
+
await proxy.start()
|
249
|
+
|
250
|
+
self._proxies[multipass_name] = proxy
|
251
|
+
logger.info(f"Successfully restored proxy for {multipass_name} on port {port}")
|
252
|
+
return True
|
253
|
+
|
254
|
+
except Exception as e:
|
255
|
+
logger.warning(f"Attempt {attempt + 1} failed for {multipass_name}: {e}")
|
256
|
+
if attempt == max_retries - 1:
|
257
|
+
logger.error(f"Failed to restore proxy for {multipass_name} after {max_retries} attempts")
|
258
|
+
# Remove from active ports if all retries failed
|
259
|
+
self._active_ports.pop(multipass_name, None)
|
260
|
+
return False
|
196
261
|
|
197
262
|
async def _save_state(self) -> None:
|
198
263
|
"""Save current proxy state to file using requestor names."""
|
199
264
|
try:
|
200
|
-
state = {
|
265
|
+
state = {
|
266
|
+
'version': self._state_version,
|
267
|
+
'proxies': {}
|
268
|
+
}
|
269
|
+
|
201
270
|
for multipass_name, proxy in self._proxies.items():
|
202
271
|
requestor_name = await self.name_mapper.get_requestor_name(multipass_name)
|
203
272
|
if requestor_name:
|
204
|
-
state[requestor_name] = {
|
273
|
+
state['proxies'][requestor_name] = {
|
205
274
|
'port': proxy.listen_port,
|
206
275
|
'target': proxy.target_host
|
207
276
|
}
|
277
|
+
|
278
|
+
# Save to temporary file first
|
279
|
+
temp_file = f"{self.state_file}.tmp"
|
208
280
|
os.makedirs(os.path.dirname(self.state_file), exist_ok=True)
|
209
|
-
|
210
|
-
|
281
|
+
|
282
|
+
with open(temp_file, 'w') as f:
|
283
|
+
json.dump(state, f, indent=2)
|
284
|
+
|
285
|
+
# Atomic rename
|
286
|
+
os.replace(temp_file, self.state_file)
|
287
|
+
|
211
288
|
except Exception as e:
|
212
289
|
logger.error(f"Failed to save proxy state: {e}")
|
213
290
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "golem-vm-provider"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.22"
|
4
4
|
description = "VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network"
|
5
5
|
authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
|
6
6
|
readme = "README.md"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/resource_tracker.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|