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.
Files changed (26) hide show
  1. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/PKG-INFO +1 -1
  2. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/config.py +3 -3
  3. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/main.py +25 -9
  4. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/network/port_verifier.py +9 -3
  5. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/port_manager.py +15 -4
  6. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/proxy_manager.py +109 -32
  7. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/pyproject.toml +1 -1
  8. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/README.md +0 -0
  9. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/__init__.py +0 -0
  10. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/api/__init__.py +0 -0
  11. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/api/models.py +0 -0
  12. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/api/routes.py +0 -0
  13. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/__init__.py +0 -0
  14. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/advertiser.py +0 -0
  15. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/discovery/resource_tracker.py +0 -0
  16. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/security/ethereum.py +0 -0
  17. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/ascii_art.py +0 -0
  18. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/logging.py +0 -0
  19. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/port_display.py +0 -0
  20. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/retry.py +0 -0
  21. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/utils/setup.py +0 -0
  22. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/__init__.py +0 -0
  23. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/cloud_init.py +0 -0
  24. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/models.py +0 -0
  25. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/multipass.py +0 -0
  26. {golem_vm_provider-0.1.20 → golem_vm_provider-0.1.22}/provider/vm/name_mapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.20
3
+ Version: 0.1.22
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
@@ -277,9 +277,9 @@ class Settings(BaseSettings):
277
277
 
278
278
  elif system == "windows":
279
279
  search_paths = [
280
- os.path.expandvars(r"%ProgramFiles%\Multipass"),
281
- os.path.expandvars(r"%ProgramFiles(x86)%\Multipass"),
282
- os.path.expandvars(r"%LocalAppData%\Multipass")
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
- # Port manager is already initialized and verified in startup_event
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=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 and proxy manager references
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 verifications, mark all ports as inaccessible
202
- if not any(result.accessible for result in results.values()):
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 verify ports with any server. Please ensure:\n"
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 SSH ports since provider port was already verified
74
- ssh_ports = list(range(self.start_port, self.end_port))
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
- # Note: _load_state is now async and will be called explicitly during provider setup
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
- with open(state_path, 'r') as f:
172
- state = json.load(f)
173
- # Restore proxy servers from saved state
174
- restore_tasks = []
175
- for requestor_name, proxy_info in state.items():
176
- # Get current multipass name for the requestor's VM
177
- multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
178
- if multipass_name:
179
- # Create task to restore proxy
180
- task = self.add_vm(
181
- vm_id=multipass_name, # Use multipass name for internal tracking
182
- vm_ip=proxy_info['target'],
183
- port=proxy_info['port']
184
- )
185
- restore_tasks.append(task)
186
- else:
187
- logger.warning(f"No multipass name found for requestor VM {requestor_name}")
188
-
189
- # Wait for all proxies to be restored
190
- if restore_tasks:
191
- results = await asyncio.gather(*restore_tasks, return_exceptions=True)
192
- successful = sum(1 for r in results if r is True)
193
- logger.info(f"Restored {successful}/{len(state)} proxy configurations")
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
- with open(self.state_file, 'w') as f:
210
- json.dump(state, f)
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.20"
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"