golem-vm-provider 0.1.1__tar.gz → 0.1.7__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 (25) hide show
  1. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/PKG-INFO +3 -3
  2. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/api/routes.py +16 -23
  3. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/main.py +73 -40
  4. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/network/port_verifier.py +36 -19
  5. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/utils/port_display.py +46 -17
  6. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/multipass.py +1 -1
  7. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/port_manager.py +71 -15
  8. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/proxy_manager.py +46 -14
  9. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/pyproject.toml +1 -1
  10. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/README.md +0 -0
  11. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/__init__.py +0 -0
  12. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/api/__init__.py +0 -0
  13. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/api/models.py +0 -0
  14. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/config.py +0 -0
  15. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/discovery/__init__.py +0 -0
  16. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/discovery/advertiser.py +0 -0
  17. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/discovery/resource_tracker.py +0 -0
  18. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/security/ethereum.py +0 -0
  19. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/utils/ascii_art.py +0 -0
  20. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/utils/logging.py +0 -0
  21. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/utils/retry.py +0 -0
  22. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/__init__.py +0 -0
  23. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/cloud_init.py +0 -0
  24. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/models.py +0 -0
  25. {golem_vm_provider-0.1.1 → golem_vm_provider-0.1.7}/provider/vm/name_mapper.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.1
3
+ Version: 0.1.7
4
4
  Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
5
- Home-page: https://github.com/cryptobench/vm-on-golem
6
5
  Keywords: golem,vm,provider,cloud,decentralized
7
6
  Author: Phillip Jensen
8
7
  Author-email: phillip+vm-on-golem@golemgrid.com
@@ -33,6 +32,7 @@ Requires-Dist: requests (>=2.31.0,<3.0.0)
33
32
  Requires-Dist: rich (>=13.7.0,<14.0.0)
34
33
  Requires-Dist: setuptools (>=69.0.3,<70.0.0)
35
34
  Requires-Dist: uvicorn (>=0.15.0,<0.16.0)
35
+ Project-URL: Homepage, https://github.com/cryptobench/vm-on-golem
36
36
  Project-URL: Repository, https://github.com/cryptobench/vm-on-golem
37
37
  Description-Content-Type: text/markdown
38
38
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  from typing import List
3
3
  from pathlib import Path
4
- from fastapi import APIRouter, HTTPException
4
+ from fastapi import APIRouter, HTTPException, Request
5
5
 
6
6
  from ..config import settings
7
7
  from ..utils.logging import setup_logger, PROCESS, SUCCESS
@@ -9,19 +9,12 @@ from ..utils.ascii_art import vm_creation_animation, vm_status_change
9
9
  from ..vm.models import VMInfo, VMStatus, VMAccessInfo, VMConfig, VMResources
10
10
  from .models import CreateVMRequest
11
11
  from ..vm.multipass import MultipassProvider, MultipassError
12
- from ..discovery.resource_tracker import ResourceTracker
13
- from ..vm.port_manager import PortManager
14
12
 
15
13
  logger = setup_logger(__name__)
16
14
  router = APIRouter()
17
15
 
18
- # Initialize components
19
- resource_tracker = ResourceTracker()
20
- port_manager = PortManager()
21
- provider = MultipassProvider(resource_tracker, port_manager)
22
-
23
16
  @router.post("/vms", response_model=VMInfo)
24
- async def create_vm(request: CreateVMRequest) -> VMInfo:
17
+ async def create_vm(request: CreateVMRequest, req: Request) -> VMInfo:
25
18
  """Create a new VM."""
26
19
  try:
27
20
  logger.info(f"📥 Received VM creation request for '{request.name}'")
@@ -47,7 +40,7 @@ async def create_vm(request: CreateVMRequest) -> VMInfo:
47
40
 
48
41
  # Check and allocate resources
49
42
  logger.process("🔄 Allocating resources")
50
- if not await resource_tracker.allocate(resources):
43
+ if not await req.app.state.resource_tracker.allocate(resources):
51
44
  logger.error("❌ Insufficient resources available")
52
45
  raise HTTPException(400, "Insufficient resources available on provider")
53
46
 
@@ -62,7 +55,7 @@ async def create_vm(request: CreateVMRequest) -> VMInfo:
62
55
 
63
56
  # Create VM
64
57
  logger.process(f"🔄 Creating VM with config: {config}")
65
- vm_info = await provider.create_vm(config)
58
+ vm_info = await req.app.state.provider.create_vm(config)
66
59
 
67
60
  # Show success message
68
61
  await vm_creation_animation(request.name)
@@ -70,7 +63,7 @@ async def create_vm(request: CreateVMRequest) -> VMInfo:
70
63
  except Exception as e:
71
64
  # If VM creation fails, deallocate resources
72
65
  logger.warning("⚠️ VM creation failed, deallocating resources")
73
- await resource_tracker.deallocate(resources)
66
+ await req.app.state.resource_tracker.deallocate(resources)
74
67
  raise
75
68
 
76
69
  except MultipassError as e:
@@ -78,13 +71,13 @@ async def create_vm(request: CreateVMRequest) -> VMInfo:
78
71
  raise HTTPException(500, str(e))
79
72
 
80
73
  @router.get("/vms", response_model=List[VMInfo])
81
- async def list_vms() -> List[VMInfo]:
74
+ async def list_vms(req: Request) -> List[VMInfo]:
82
75
  """List all VMs."""
83
76
  try:
84
77
  logger.info("📋 Listing all VMs")
85
78
  vms = []
86
- for vm_id in resource_tracker.get_allocated_vms():
87
- vm_info = await provider.get_vm_status(vm_id)
79
+ for vm_id in req.app.state.resource_tracker.get_allocated_vms():
80
+ vm_info = await req.app.state.provider.get_vm_status(vm_id)
88
81
  vms.append(vm_info)
89
82
  return vms
90
83
  except MultipassError as e:
@@ -92,11 +85,11 @@ async def list_vms() -> List[VMInfo]:
92
85
  raise HTTPException(500, str(e))
93
86
 
94
87
  @router.get("/vms/{requestor_name}", response_model=VMInfo)
95
- async def get_vm_status(requestor_name: str) -> VMInfo:
88
+ async def get_vm_status(requestor_name: str, req: Request) -> VMInfo:
96
89
  """Get VM status."""
97
90
  try:
98
91
  logger.info(f"🔍 Getting status for VM '{requestor_name}'")
99
- status = await provider.get_vm_status(requestor_name)
92
+ status = await req.app.state.provider.get_vm_status(requestor_name)
100
93
  vm_status_change(requestor_name, status.status.value)
101
94
  return status
102
95
  except MultipassError as e:
@@ -104,16 +97,16 @@ async def get_vm_status(requestor_name: str) -> VMInfo:
104
97
  raise HTTPException(500, str(e))
105
98
 
106
99
  @router.get("/vms/{requestor_name}/access", response_model=VMAccessInfo)
107
- async def get_vm_access(requestor_name: str) -> VMAccessInfo:
100
+ async def get_vm_access(requestor_name: str, req: Request) -> VMAccessInfo:
108
101
  """Get VM access information."""
109
102
  try:
110
103
  # Get VM info
111
- vm = await provider.get_vm_status(requestor_name)
104
+ vm = await req.app.state.provider.get_vm_status(requestor_name)
112
105
  if not vm:
113
106
  raise HTTPException(404, "VM not found")
114
107
 
115
108
  # Get multipass name from mapper
116
- multipass_name = await provider.name_mapper.get_multipass_name(requestor_name)
109
+ multipass_name = await req.app.state.provider.name_mapper.get_multipass_name(requestor_name)
117
110
  if not multipass_name:
118
111
  raise HTTPException(404, "VM mapping not found")
119
112
 
@@ -130,7 +123,7 @@ async def get_vm_access(requestor_name: str) -> VMAccessInfo:
130
123
  raise HTTPException(500, str(e))
131
124
 
132
125
  @router.delete("/vms/{requestor_name}")
133
- async def delete_vm(requestor_name: str) -> None:
126
+ async def delete_vm(requestor_name: str, req: Request) -> None:
134
127
  """Delete a VM.
135
128
 
136
129
  Args:
@@ -140,14 +133,14 @@ async def delete_vm(requestor_name: str) -> None:
140
133
  logger.process(f"🗑️ Deleting VM '{requestor_name}'")
141
134
 
142
135
  # Get multipass name from mapper
143
- multipass_name = await provider.name_mapper.get_multipass_name(requestor_name)
136
+ multipass_name = await req.app.state.provider.name_mapper.get_multipass_name(requestor_name)
144
137
  if not multipass_name:
145
138
  logger.warning(f"No multipass name found for VM '{requestor_name}' (may have been already deleted)")
146
139
  return
147
140
 
148
141
  try:
149
142
  vm_status_change(requestor_name, "STOPPING", "Cleanup in progress")
150
- await provider.delete_vm(requestor_name)
143
+ await req.app.state.provider.delete_vm(requestor_name)
151
144
  vm_status_change(requestor_name, "TERMINATED", "Cleanup complete")
152
145
  logger.success(f"✨ Successfully deleted VM '{requestor_name}'")
153
146
  except MultipassError as e:
@@ -18,10 +18,8 @@ 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
- # Initialize port manager (verification already done in run.py)
22
- logger.process("🔄 Initializing port manager...")
23
- port_manager = PortManager()
24
- app.state.port_manager = port_manager
21
+ # Port manager is already initialized and verified in startup_event
22
+ port_manager = app.state.port_manager
25
23
 
26
24
  # Create resource tracker
27
25
  logger.process("🔄 Initializing resource tracker...")
@@ -108,10 +106,48 @@ async def cleanup_provider() -> None:
108
106
  @app.on_event("startup")
109
107
  async def startup_event():
110
108
  """Handle application startup."""
111
- # Display startup animation
112
- await startup_animation()
113
- # Initialize provider
114
- await setup_provider()
109
+ try:
110
+ # Display startup animation
111
+ await startup_animation()
112
+
113
+ # Verify ports first
114
+ from .vm.port_manager import PortManager
115
+ from .utils.port_display import PortVerificationDisplay
116
+ from .config import settings
117
+
118
+ display = PortVerificationDisplay(
119
+ provider_port=settings.PORT,
120
+ port_range_start=settings.PORT_RANGE_START,
121
+ port_range_end=settings.PORT_RANGE_END
122
+ )
123
+ display.print_header()
124
+
125
+ # Initialize port manager
126
+ logger.process("🔄 Verifying port accessibility...")
127
+ port_manager = PortManager(
128
+ start_port=settings.PORT_RANGE_START,
129
+ end_port=settings.PORT_RANGE_END,
130
+ discovery_port=settings.PORT
131
+ )
132
+ if not await port_manager.initialize():
133
+ logger.error("Port verification failed. Please ensure:")
134
+ logger.error(f"1. Port {settings.PORT} is accessible for provider access")
135
+ logger.error(f"2. Some ports in range {settings.PORT_RANGE_START}-{settings.PORT_RANGE_END} are accessible for VM access")
136
+ logger.error("3. Your firewall/router is properly configured")
137
+ raise RuntimeError("Port verification failed")
138
+
139
+ logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
140
+
141
+ # Store port manager in app state for later use
142
+ app.state.port_manager = port_manager
143
+
144
+ # Initialize provider
145
+ await setup_provider()
146
+ except Exception as e:
147
+ logger.error(f"Startup failed: {e}")
148
+ # Ensure proper cleanup
149
+ await cleanup_provider()
150
+ raise
115
151
 
116
152
  @app.on_event("shutdown")
117
153
  async def shutdown_event():
@@ -163,40 +199,37 @@ def check_requirements():
163
199
 
164
200
  return True
165
201
 
166
- async def verify_ports():
167
- """Verify port accessibility before starting server."""
168
- from .vm.port_manager import PortManager
169
- from .utils.port_display import PortVerificationDisplay
170
- from .config import settings
202
+ async def verify_provider_port(port: int) -> bool:
203
+ """Verify that the provider port is available for binding.
171
204
 
172
- display = PortVerificationDisplay(
173
- provider_port=settings.PORT,
174
- port_range_start=settings.PORT_RANGE_START,
175
- port_range_end=settings.PORT_RANGE_END
176
- )
177
- display.print_header()
178
-
179
- # Initialize port manager
180
- logger.process("🔄 Verifying port accessibility...")
181
- port_manager = PortManager(
182
- start_port=settings.PORT_RANGE_START,
183
- end_port=settings.PORT_RANGE_END,
184
- discovery_port=settings.PORT
185
- )
186
- if not await port_manager.initialize():
187
- logger.error("Port verification failed. Please ensure:")
188
- logger.error(f"1. Port {settings.PORT} is accessible for provider access")
189
- logger.error(f"2. Some ports in range {settings.PORT_RANGE_START}-{settings.PORT_RANGE_END} are accessible for VM access")
190
- logger.error("3. Your firewall/router is properly configured")
205
+ Args:
206
+ port: The port to verify
207
+
208
+ Returns:
209
+ bool: True if the port is available, False otherwise
210
+ """
211
+ try:
212
+ # Try to create a temporary listener
213
+ server = await asyncio.start_server(
214
+ lambda r, w: None, # Empty callback
215
+ '0.0.0.0',
216
+ port
217
+ )
218
+ server.close()
219
+ await server.wait_closed()
220
+ logger.info(f" Provider port {port} is available")
221
+ return True
222
+ except Exception as e:
223
+ logger.error(f" Provider port {port} is not available: {e}")
224
+ logger.error("Please ensure:")
225
+ logger.error(f"1. Port {port} is not in use by another application")
226
+ logger.error("2. You have permission to bind to this port")
227
+ logger.error("3. Your firewall allows binding to this port")
191
228
  return False
192
-
193
- logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
194
- return True
195
229
 
196
230
  def start():
197
231
  """Start the provider server."""
198
232
  import sys
199
- import asyncio
200
233
  from pathlib import Path
201
234
  from dotenv import load_dotenv
202
235
  import uvicorn
@@ -221,10 +254,10 @@ def start():
221
254
  if not check_requirements():
222
255
  logger.error("Requirements check failed")
223
256
  sys.exit(1)
224
-
225
- # Verify ports before starting server
226
- if not asyncio.run(verify_ports()):
227
- logger.error("Port verification failed")
257
+
258
+ # Verify provider port is available
259
+ if not asyncio.run(verify_provider_port(settings.PORT)):
260
+ logger.error(f"Provider port {settings.PORT} is not available")
228
261
  sys.exit(1)
229
262
 
230
263
  # Configure uvicorn logging
@@ -165,37 +165,53 @@ class PortVerifier:
165
165
  attempts.append(ServerAttempt(
166
166
  server=server,
167
167
  success=False,
168
- error="Server returned unsuccessful response"
168
+ error=f"Server {server} returned unsuccessful response"
169
169
  ))
170
170
  else:
171
171
  attempts.append(ServerAttempt(
172
172
  server=server,
173
173
  success=False,
174
- error=f"Server returned status {response.status}"
174
+ error=f"Server {server} returned status {response.status}"
175
175
  ))
176
176
  except asyncio.TimeoutError:
177
+ error_msg = f"Connection to {server} timed out after 30 seconds"
177
178
  attempts.append(ServerAttempt(
178
179
  server=server,
179
180
  success=False,
180
- error="Connection timed out"
181
+ error=error_msg
181
182
  ))
182
- logger.warning(f"Timeout while verifying ports with {server}")
183
+ logger.warning(f"{error_msg}. Please ensure the port check server is running and accessible.")
184
+ except aiohttp.ClientConnectorError as e:
185
+ error_msg = f"Could not connect to {server}: Connection refused"
186
+ attempts.append(ServerAttempt(
187
+ server=server,
188
+ success=False,
189
+ error=error_msg
190
+ ))
191
+ logger.warning(f"{error_msg}. Please ensure the port check server is running.")
183
192
  except Exception as e:
193
+ error_msg = f"Failed to verify ports with {server}: {str(e)}"
184
194
  attempts.append(ServerAttempt(
185
195
  server=server,
186
196
  success=False,
187
- error=str(e)
197
+ error=error_msg
188
198
  ))
189
- logger.warning(f"Failed to verify ports with {server}: {e}")
199
+ logger.warning(error_msg)
190
200
 
191
201
  # If no successful verifications, mark all ports as inaccessible
192
202
  if not any(result.accessible for result in results.values()):
193
- logger.error("Failed to verify ports with any server")
203
+ error_msg = (
204
+ "Failed to verify ports with any server. Please ensure:\n"
205
+ "1. At least one port check server is running and accessible\n"
206
+ "2. Your network connection is stable\n"
207
+ "3. The server URLs are correct"
208
+ )
209
+ logger.error(error_msg)
194
210
  results = {
195
211
  port: PortVerificationResult(
196
212
  port=port,
197
213
  accessible=False,
198
- error="Failed to verify with any port check server",
214
+ error=error_msg,
199
215
  attempts=[] # Will be filled below
200
216
  )
201
217
  for port in ports
@@ -254,9 +270,11 @@ class PortVerifier:
254
270
  Returns:
255
271
  Dictionary mapping ports to their verification results
256
272
  """
257
- # Separate discovery port from other ports
258
- other_ports = [p for p in ports if p != self.discovery_port]
259
- discovery_port_included = self.discovery_port in ports
273
+ # Only verify the ports provided - discovery port is handled separately
274
+ logger.info(f"Verifying {len(ports)} SSH ports...")
275
+ if not ports:
276
+ logger.warning("No ports to verify")
277
+ return {}
260
278
 
261
279
  # First verify ports with local binding
262
280
  logger.info("Checking local port availability...")
@@ -267,7 +285,7 @@ class PortVerifier:
267
285
  port: PortVerificationResult(
268
286
  port=port,
269
287
  accessible=False,
270
- error="Failed to bind locally"
288
+ error=f"Port {port} could not be bound locally"
271
289
  )
272
290
  for port in ports
273
291
  }
@@ -276,12 +294,11 @@ class PortVerifier:
276
294
  logger.info("Starting external port verification...")
277
295
  results = await self.verify_external_access(local_available)
278
296
 
279
- # Log detailed results for discovery port
280
- if discovery_port_included and self.discovery_port in results:
281
- result = results[self.discovery_port]
282
- if result.accessible:
283
- logger.info(f"Discovery port {self.discovery_port} verified successfully by {result.verified_by}")
284
- else:
285
- logger.error(f"Discovery port {self.discovery_port} verification failed: {result.error}")
297
+ # Log verification results
298
+ accessible_ports = [port for port, result in results.items() if result.accessible]
299
+ if accessible_ports:
300
+ logger.info(f"Successfully verified {len(accessible_ports)} SSH ports: {', '.join(map(str, sorted(accessible_ports)))}")
301
+ else:
302
+ logger.warning("No SSH ports were verified as accessible")
286
303
 
287
304
  return results
@@ -58,23 +58,29 @@ class PortVerificationDisplay:
58
58
  print("\n📡 Provider Accessibility (Required)")
59
59
  print("--------------------------------")
60
60
 
61
- await self.animate_verification("Checking provider accessibility...")
61
+ await self.animate_verification("Verifying provider port status...")
62
62
 
63
- status_badge = "✅ Accessible" if result.accessible else "❌ Not Accessible"
64
- print(f"[{status_badge}] Port {self.provider_port}")
65
- print(f"└─ Status: {'Accessible' if result.accessible else 'Not Accessible'}")
66
-
67
- # Show external/internal access
68
- if result.accessible:
69
- print("└─ Access: External ✓ | Internal ✓")
70
- print("└─ Requestors can discover and connect to your provider")
63
+ if result.verified_by == "local_verification":
64
+ print(f"[✅ Ready] Port {self.provider_port}")
65
+ print("└─ Status: Port available and bound")
66
+ print("└─ Access: Local binding successful")
67
+ print("└─ Provider can start and accept connections")
71
68
  else:
72
- print("└─ Access: External | Internal ")
73
- print("└─ Requestors cannot discover or connect to your provider")
69
+ status_badge = " Accessible" if result.accessible else "❌ Not Accessible"
70
+ print(f"[{status_badge}] Port {self.provider_port}")
71
+ print(f"└─ Status: {'Accessible' if result.accessible else 'Not Accessible'}")
74
72
 
75
- # Show verification server if successful
76
- if result.verified_by:
77
- print(f"└─ Verified By: {result.verified_by}")
73
+ # Show external/internal access
74
+ if result.accessible:
75
+ print("└─ Access: External ✓ | Internal ✓")
76
+ print("└─ Requestors can discover and connect to your provider")
77
+ else:
78
+ print("└─ Access: External ✗ | Internal ✗")
79
+ print("└─ Requestors cannot discover or connect to your provider")
80
+
81
+ # Show verification server if successful
82
+ if result.verified_by:
83
+ print(f"└─ Verified By: {result.verified_by}")
78
84
 
79
85
  async def print_ssh_status(self, results: Dict[int, PortVerificationResult]):
80
86
  """Print SSH ports status with progress bar.
@@ -171,7 +177,7 @@ class PortVerificationDisplay:
171
177
 
172
178
  print("\nNeed help? Visit our troubleshooting guide: docs.golem.network/ports")
173
179
 
174
- def print_summary(self, discovery_result: PortVerificationResult,
180
+ def print_summary(self, discovery_result: Optional[PortVerificationResult],
175
181
  ssh_results: Dict[int, PortVerificationResult]):
176
182
  """Print a precise, actionable summary of the verification status.
177
183
 
@@ -181,17 +187,38 @@ class PortVerificationDisplay:
181
187
  """
182
188
  print("\n🎯 Current Status:", end=" ")
183
189
 
190
+ if discovery_result is None:
191
+ print("Verification Failed")
192
+ print(f"└─ Error: Discovery port {self.provider_port} verification failed")
193
+ print("└─ Impact: Provider cannot start without discovery port access")
194
+ print(f"└─ Fix: Ensure port {self.provider_port} is available and not in use")
195
+ return
196
+
184
197
  accessible_ssh_ports = [port for port, result in ssh_results.items() if result.accessible]
185
198
 
186
199
  if not discovery_result.accessible:
187
200
  print("Provider Not Discoverable")
188
- print(f"└─ Reason: Port {self.provider_port} is not accessible")
201
+ print(f"└─ Error: {discovery_result.error or f'Port {self.provider_port} is not accessible'}")
189
202
  print("└─ Impact: Requestors cannot find or connect to your provider")
190
203
  print(f"└─ Fix: Configure port forwarding for port {self.provider_port}")
204
+ if discovery_result.attempts:
205
+ print("\n📋 Verification Attempts")
206
+ print("--------------------")
207
+ for attempt in discovery_result.attempts:
208
+ status = "✅" if attempt.success else "❌"
209
+ print(f"{status} {attempt.server}")
210
+ if not attempt.success and attempt.error:
211
+ print(f" └─ Error: {attempt.error}")
212
+ print("\n🔍 Troubleshooting Tips")
213
+ print("-------------------")
214
+ print("1. Check if port check servers are running")
215
+ print("2. Verify your network connection")
216
+ print("3. Ensure no firewall is blocking the connections")
217
+ print("\nFor detailed setup instructions, visit: docs.golem.network/provider/ports")
191
218
 
192
219
  elif not accessible_ssh_ports:
193
220
  print("VMs Not Accessible")
194
- print("└─ Reason: No VM access ports are available")
221
+ print("└─ Error: No VM access ports are available")
195
222
  print("└─ Impact: Requestors will not be able to access their rented VMs")
196
223
  print(f"└─ Fix: Configure port forwarding for range {self.port_range_start}-{self.port_range_end}")
197
224
 
@@ -202,3 +229,5 @@ class PortVerificationDisplay:
202
229
  print(f"└─ Capacity: Can handle up to {len(accessible_ssh_ports)} concurrent VMs")
203
230
  if len(accessible_ssh_ports) <= 5:
204
231
  print("└─ Recommendation: Open more ports for higher capacity")
232
+ if discovery_result.verified_by:
233
+ print(f"└─ Verified By: {discovery_result.verified_by}")
@@ -37,7 +37,7 @@ class MultipassProvider(VMProvider):
37
37
  self.vm_data_dir.mkdir(parents=True, exist_ok=True)
38
38
 
39
39
  # Initialize managers
40
- self.proxy_manager = PythonProxyManager()
40
+ self.proxy_manager = PythonProxyManager(port_manager=port_manager)
41
41
  self.name_mapper = VMNameMapper(self.vm_data_dir / "vm_names.json")
42
42
 
43
43
  def _verify_installation(self) -> None:
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import json
3
+ import socket
3
4
  import logging
4
5
  import asyncio
5
6
  from pathlib import Path
@@ -7,7 +8,7 @@ from typing import Optional, Set, List, Dict
7
8
  from threading import Lock
8
9
 
9
10
  from ..config import settings
10
- from ..network.port_verifier import PortVerifier, PortVerificationResult
11
+ from ..network.port_verifier import PortVerifier, PortVerificationResult, ServerAttempt
11
12
  from ..utils.port_display import PortVerificationDisplay
12
13
 
13
14
  logger = logging.getLogger(__name__)
@@ -40,10 +41,10 @@ class PortManager:
40
41
 
41
42
  # Initialize port verifier with default servers
42
43
  self.port_check_servers = port_check_servers or [
43
- # "http://portcheck1.golem.network:7466",
44
- # "http://portcheck2.golem.network:7466",
45
- # "http://portcheck3.golem.network:7466",
46
- "http://localhost:9000" # Fallback for local development
44
+ "http://localhost:9000", # Local development server
45
+ "http://portcheck1.golem.network:7466", # Production servers
46
+ "http://portcheck2.golem.network:7466",
47
+ "http://portcheck3.golem.network:7466"
47
48
  ]
48
49
  self.discovery_port = discovery_port or settings.PORT
49
50
  self.port_verifier = PortVerifier(
@@ -68,18 +69,43 @@ class PortManager:
68
69
  )
69
70
  display.print_header()
70
71
 
71
- # Verify all ports together (discovery port and SSH ports)
72
- all_ports = [self.discovery_port] + list(range(self.start_port, self.end_port))
73
- logger.info(f"Verifying all ports: discovery port {self.discovery_port} and SSH ports {self.start_port}-{self.end_port}...")
72
+ # Only verify SSH ports since provider port was already verified
73
+ ssh_ports = list(range(self.start_port, self.end_port))
74
+ logger.info(f"Starting port verification...")
75
+ logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
76
+ logger.info(f"Using port check servers: {', '.join(self.port_check_servers)}")
74
77
 
75
- results = await self.port_verifier.verify_ports(all_ports)
78
+ results = await self.port_verifier.verify_ports(ssh_ports)
76
79
 
80
+ # Add provider port as verified since we already checked it
81
+ results[self.discovery_port] = PortVerificationResult(
82
+ port=self.discovery_port,
83
+ accessible=True,
84
+ verified_by="local_verification",
85
+ attempts=[ServerAttempt(server="local_verification", success=True)]
86
+ )
87
+
88
+ # Check if discovery port was verified
89
+ if self.discovery_port not in results:
90
+ error_msg = f"Port {self.discovery_port} verification failed"
91
+ logger.error(error_msg)
92
+ display.print_summary(
93
+ PortVerificationResult(
94
+ port=self.discovery_port,
95
+ accessible=False,
96
+ error=error_msg
97
+ ),
98
+ {}
99
+ )
100
+ return False
101
+
77
102
  # Display discovery port status with animation
78
103
  discovery_result = results[self.discovery_port]
79
104
  await display.print_discovery_status(discovery_result)
80
105
 
81
106
  if not discovery_result.accessible:
82
- logger.error(f"Failed to verify discovery port: {discovery_result.error}")
107
+ error_msg = discovery_result.error or f"Port {self.discovery_port} is not accessible"
108
+ logger.error(f"Failed to verify discovery port: {error_msg}")
83
109
  # Print summary before returning
84
110
  display.print_summary(discovery_result, {})
85
111
  return False
@@ -147,7 +173,23 @@ class PortManager:
147
173
  if vm_id in self._used_ports:
148
174
  port = self._used_ports[vm_id]
149
175
  if port in self.verified_ports:
150
- return port
176
+ # Quick check if port is still available
177
+ try:
178
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
179
+ sock.settimeout(1)
180
+ result = sock.connect_ex(('127.0.0.1', port))
181
+ sock.close()
182
+
183
+ if result != 0: # Port is available
184
+ return port
185
+ else:
186
+ # Port is in use, remove from verified ports
187
+ self.verified_ports.remove(port)
188
+ self._used_ports.pop(vm_id)
189
+ except Exception as e:
190
+ logger.debug(f"Failed to check port {port}: {e}")
191
+ # Keep the port if check fails, let proxy setup handle any issues
192
+ return port
151
193
  else:
152
194
  # Previously allocated port is no longer verified
153
195
  self._used_ports.pop(vm_id)
@@ -157,10 +199,24 @@ class PortManager:
157
199
  # Find first available verified port
158
200
  for port in sorted(self.verified_ports):
159
201
  if port not in used_ports:
160
- self._used_ports[vm_id] = port
161
- self._save_state()
162
- logger.info(f"Allocated verified port {port} for VM {vm_id}")
163
- return port
202
+ # Quick check if port is actually available
203
+ try:
204
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
205
+ sock.settimeout(1)
206
+ result = sock.connect_ex(('127.0.0.1', port))
207
+ sock.close()
208
+
209
+ if result != 0: # Port is available
210
+ self._used_ports[vm_id] = port
211
+ self._save_state()
212
+ logger.info(f"Allocated verified port {port} for VM {vm_id}")
213
+ return port
214
+ else:
215
+ # Port is in use, remove from verified ports
216
+ self.verified_ports.remove(port)
217
+ except Exception as e:
218
+ logger.debug(f"Failed to check port {port}: {e}")
219
+ continue
164
220
 
165
221
  logger.error("No verified ports available for allocation")
166
222
  return None
@@ -58,8 +58,16 @@ class SSHProxyProtocol(Protocol):
58
58
  """Handle connection loss."""
59
59
  if exc:
60
60
  logger.error(f"Client connection lost with error: {exc}")
61
- if self.target_transport and not self.target_transport.is_closing():
62
- self.target_transport.close()
61
+
62
+ # Ensure target connection is properly closed
63
+ if self.target_transport:
64
+ if not self.target_transport.is_closing():
65
+ self.target_transport.close()
66
+ self.target_transport = None
67
+
68
+ # Clear any buffered data
69
+ if self.buffer:
70
+ self.buffer.clear()
63
71
 
64
72
  class SSHTargetProtocol(Protocol):
65
73
  """Protocol for handling target SSH connections."""
@@ -82,9 +90,12 @@ class SSHTargetProtocol(Protocol):
82
90
  """Handle connection loss."""
83
91
  if exc:
84
92
  logger.error(f"Target connection lost with error: {exc}")
85
- if (self.client_protocol.transport and
86
- not self.client_protocol.transport.is_closing()):
87
- self.client_protocol.transport.close()
93
+
94
+ # Ensure client connection is properly closed
95
+ if self.client_protocol and self.client_protocol.transport:
96
+ if not self.client_protocol.transport.is_closing():
97
+ self.client_protocol.transport.close()
98
+ self.client_protocol.transport = None
88
99
 
89
100
  class ProxyServer:
90
101
  """Manages a single proxy server instance."""
@@ -120,16 +131,22 @@ class ProxyServer:
120
131
  async def stop(self) -> None:
121
132
  """Stop the proxy server."""
122
133
  if self.server:
123
- self.server.close()
124
- await self.server.wait_closed()
125
- logger.info(f"Proxy server on port {self.listen_port} stopped")
134
+ try:
135
+ # Close the server
136
+ self.server.close()
137
+ await self.server.wait_closed()
138
+ logger.info(f"Proxy server on port {self.listen_port} stopped")
139
+ except Exception as e:
140
+ logger.error(f"Error stopping proxy server on port {self.listen_port}: {e}")
141
+ finally:
142
+ self.server = None
126
143
 
127
144
  class PythonProxyManager:
128
145
  """Manages proxy servers for VM SSH access."""
129
146
 
130
147
  def __init__(
131
148
  self,
132
- port_manager: Optional[PortManager] = None,
149
+ port_manager: PortManager,
133
150
  state_file: Optional[str] = None
134
151
  ):
135
152
  """Initialize the proxy manager.
@@ -138,7 +155,7 @@ class PythonProxyManager:
138
155
  port_manager: Port allocation manager
139
156
  state_file: Path to persist proxy state
140
157
  """
141
- self.port_manager = port_manager or PortManager()
158
+ self.port_manager = port_manager
142
159
  self.state_file = state_file or os.path.expanduser("~/.golem/provider/proxy_state.json")
143
160
  self._proxies: Dict[str, ProxyServer] = {} # vm_id -> ProxyServer
144
161
  self._load_state()
@@ -230,10 +247,25 @@ class PythonProxyManager:
230
247
 
231
248
  async def cleanup(self) -> None:
232
249
  """Remove all proxy configurations."""
233
- try:
234
- for vm_id in list(self._proxies.keys()):
250
+ cleanup_errors = []
251
+
252
+ # Stop all proxy servers
253
+ for vm_id in list(self._proxies.keys()):
254
+ try:
235
255
  await self.remove_vm(vm_id)
256
+ except Exception as e:
257
+ cleanup_errors.append(f"Failed to remove proxy for VM {vm_id}: {e}")
258
+
259
+ try:
236
260
  self._save_state()
237
- logger.info("Cleaned up all proxy configurations")
238
261
  except Exception as e:
239
- logger.error(f"Failed to cleanup proxy configurations: {e}")
262
+ cleanup_errors.append(f"Failed to save state: {e}")
263
+
264
+ if cleanup_errors:
265
+ error_msg = "\n".join(cleanup_errors)
266
+ logger.error(f"Errors during proxy cleanup:\n{error_msg}")
267
+ else:
268
+ logger.info("Cleaned up all proxy configurations")
269
+
270
+ # Clear internal state
271
+ self._proxies.clear()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "golem-vm-provider"
3
- version = "0.1.1"
3
+ version = "0.1.7"
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"