golem-vm-provider 0.1.0__tar.gz → 0.1.2__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.0 → golem_vm_provider-0.1.2}/PKG-INFO +1 -1
- golem_vm_provider-0.1.2/provider/main.py +281 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/network/port_verifier.py +36 -19
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/utils/port_display.py +46 -17
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/port_manager.py +35 -10
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/proxy_manager.py +44 -12
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/pyproject.toml +1 -1
- golem_vm_provider-0.1.0/provider/main.py +0 -125
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/README.md +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/__init__.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/api/__init__.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/api/models.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/api/routes.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/config.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/discovery/__init__.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/discovery/advertiser.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/discovery/resource_tracker.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/security/ethereum.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/utils/ascii_art.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/utils/logging.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/utils/retry.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/__init__.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/cloud_init.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/models.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/multipass.py +0 -0
- {golem_vm_provider-0.1.0 → golem_vm_provider-0.1.2}/provider/vm/name_mapper.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: golem-vm-provider
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.2
|
4
4
|
Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
|
5
5
|
Home-page: https://github.com/cryptobench/vm-on-golem
|
6
6
|
Keywords: golem,vm,provider,cloud,decentralized
|
@@ -0,0 +1,281 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
from fastapi import FastAPI
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from .config import settings
|
7
|
+
from .utils.logging import setup_logger, PROCESS, SUCCESS
|
8
|
+
from .utils.ascii_art import startup_animation
|
9
|
+
from .discovery.resource_tracker import ResourceTracker
|
10
|
+
from .discovery.advertiser import ResourceAdvertiser
|
11
|
+
from .vm.multipass import MultipassProvider
|
12
|
+
from .vm.port_manager import PortManager
|
13
|
+
|
14
|
+
logger = setup_logger(__name__)
|
15
|
+
|
16
|
+
app = FastAPI(title="VM on Golem Provider")
|
17
|
+
|
18
|
+
async def setup_provider() -> None:
|
19
|
+
"""Setup and initialize the provider components."""
|
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
|
25
|
+
logger.process("🔄 Initializing resource tracker...")
|
26
|
+
resource_tracker = ResourceTracker()
|
27
|
+
app.state.resource_tracker = resource_tracker
|
28
|
+
|
29
|
+
# Create provider with resource tracker and port manager
|
30
|
+
logger.process("🔄 Initializing VM provider...")
|
31
|
+
provider = MultipassProvider(resource_tracker, port_manager=port_manager)
|
32
|
+
try:
|
33
|
+
await asyncio.wait_for(provider.initialize(), timeout=30)
|
34
|
+
app.state.provider = provider
|
35
|
+
|
36
|
+
# Store proxy manager reference for cleanup
|
37
|
+
app.state.proxy_manager = provider.proxy_manager
|
38
|
+
|
39
|
+
except asyncio.TimeoutError:
|
40
|
+
logger.error("Provider initialization timed out")
|
41
|
+
raise
|
42
|
+
except Exception as e:
|
43
|
+
logger.error(f"Failed to initialize provider: {e}")
|
44
|
+
raise
|
45
|
+
|
46
|
+
# Create and start advertiser in background
|
47
|
+
logger.process("🔄 Starting resource advertiser...")
|
48
|
+
advertiser = ResourceAdvertiser(
|
49
|
+
resource_tracker=resource_tracker,
|
50
|
+
discovery_url=settings.DISCOVERY_URL,
|
51
|
+
provider_id=settings.PROVIDER_ID
|
52
|
+
)
|
53
|
+
|
54
|
+
# Start advertiser in background task
|
55
|
+
app.state.advertiser_task = asyncio.create_task(advertiser.start())
|
56
|
+
app.state.advertiser = advertiser
|
57
|
+
|
58
|
+
logger.success("✨ Provider setup complete and ready to accept requests")
|
59
|
+
except Exception as e:
|
60
|
+
logger.error(f"Failed to setup provider: {e}")
|
61
|
+
# Attempt cleanup of any initialized components
|
62
|
+
await cleanup_provider()
|
63
|
+
raise
|
64
|
+
|
65
|
+
async def cleanup_provider() -> None:
|
66
|
+
"""Cleanup provider components."""
|
67
|
+
cleanup_errors = []
|
68
|
+
|
69
|
+
# Stop advertiser
|
70
|
+
if hasattr(app.state, "advertiser"):
|
71
|
+
try:
|
72
|
+
await app.state.advertiser.stop()
|
73
|
+
if hasattr(app.state, "advertiser_task"):
|
74
|
+
app.state.advertiser_task.cancel()
|
75
|
+
try:
|
76
|
+
await app.state.advertiser_task
|
77
|
+
except asyncio.CancelledError:
|
78
|
+
pass
|
79
|
+
except Exception as e:
|
80
|
+
cleanup_errors.append(f"Failed to stop advertiser: {e}")
|
81
|
+
|
82
|
+
# Cleanup proxy manager first to stop all proxy servers
|
83
|
+
if hasattr(app.state, "proxy_manager"):
|
84
|
+
try:
|
85
|
+
await asyncio.wait_for(app.state.proxy_manager.cleanup(), timeout=30)
|
86
|
+
except asyncio.TimeoutError:
|
87
|
+
cleanup_errors.append("Proxy manager cleanup timed out")
|
88
|
+
except Exception as e:
|
89
|
+
cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
|
90
|
+
|
91
|
+
# Cleanup provider
|
92
|
+
if hasattr(app.state, "provider"):
|
93
|
+
try:
|
94
|
+
await asyncio.wait_for(app.state.provider.cleanup(), timeout=30)
|
95
|
+
except asyncio.TimeoutError:
|
96
|
+
cleanup_errors.append("Provider cleanup timed out")
|
97
|
+
except Exception as e:
|
98
|
+
cleanup_errors.append(f"Failed to cleanup provider: {e}")
|
99
|
+
|
100
|
+
if cleanup_errors:
|
101
|
+
error_msg = "\n".join(cleanup_errors)
|
102
|
+
logger.error(f"Errors during cleanup:\n{error_msg}")
|
103
|
+
else:
|
104
|
+
logger.success("✨ Provider cleanup complete")
|
105
|
+
|
106
|
+
@app.on_event("startup")
|
107
|
+
async def startup_event():
|
108
|
+
"""Handle application startup."""
|
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
|
151
|
+
|
152
|
+
@app.on_event("shutdown")
|
153
|
+
async def shutdown_event():
|
154
|
+
"""Handle application shutdown."""
|
155
|
+
await cleanup_provider()
|
156
|
+
|
157
|
+
# Import routes after app creation to avoid circular imports
|
158
|
+
from .api import routes
|
159
|
+
app.include_router(routes.router, prefix="/api/v1")
|
160
|
+
|
161
|
+
# Export app for uvicorn
|
162
|
+
__all__ = ["app", "start"]
|
163
|
+
|
164
|
+
def check_requirements():
|
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
|
+
try:
|
190
|
+
# Create and secure directories
|
191
|
+
for directory in [vm_data_dir, ssh_key_dir, proxy_state_dir]:
|
192
|
+
path = Path(directory)
|
193
|
+
path.mkdir(parents=True, exist_ok=True)
|
194
|
+
if directory == ssh_key_dir:
|
195
|
+
path.chmod(0o700) # Secure permissions for SSH keys
|
196
|
+
except Exception as e:
|
197
|
+
logger.error(f"Failed to create required directories: {e}")
|
198
|
+
return False
|
199
|
+
|
200
|
+
return True
|
201
|
+
|
202
|
+
async def verify_provider_port(port: int) -> bool:
|
203
|
+
"""Verify that the provider port is available for binding.
|
204
|
+
|
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")
|
228
|
+
return False
|
229
|
+
|
230
|
+
def start():
|
231
|
+
"""Start the provider server."""
|
232
|
+
import sys
|
233
|
+
from pathlib import Path
|
234
|
+
from dotenv import load_dotenv
|
235
|
+
import uvicorn
|
236
|
+
from .utils.logging import setup_logger
|
237
|
+
from .config import settings
|
238
|
+
|
239
|
+
# Configure logging with debug mode
|
240
|
+
logger = setup_logger(__name__, debug=True)
|
241
|
+
|
242
|
+
try:
|
243
|
+
# Load environment variables from .env file
|
244
|
+
env_path = Path(__file__).parent.parent / '.env'
|
245
|
+
load_dotenv(dotenv_path=env_path)
|
246
|
+
|
247
|
+
# Log environment variables
|
248
|
+
logger.info("Environment variables:")
|
249
|
+
for key, value in os.environ.items():
|
250
|
+
if key.startswith('GOLEM_PROVIDER_'):
|
251
|
+
logger.info(f"{key}={value}")
|
252
|
+
|
253
|
+
# Check requirements
|
254
|
+
if not check_requirements():
|
255
|
+
logger.error("Requirements check failed")
|
256
|
+
sys.exit(1)
|
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")
|
261
|
+
sys.exit(1)
|
262
|
+
|
263
|
+
# Configure uvicorn logging
|
264
|
+
log_config = uvicorn.config.LOGGING_CONFIG
|
265
|
+
log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
266
|
+
|
267
|
+
# Run server
|
268
|
+
logger.process(f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
|
269
|
+
uvicorn.run(
|
270
|
+
"provider:app",
|
271
|
+
host=settings.HOST,
|
272
|
+
port=settings.PORT,
|
273
|
+
reload=settings.DEBUG,
|
274
|
+
log_level="info" if not settings.DEBUG else "debug",
|
275
|
+
log_config=log_config,
|
276
|
+
timeout_keep_alive=60, # Increase keep-alive timeout
|
277
|
+
limit_concurrency=100, # Limit concurrent connections
|
278
|
+
)
|
279
|
+
except Exception as e:
|
280
|
+
logger.error(f"Failed to start provider server: {e}")
|
281
|
+
sys.exit(1)
|
@@ -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=
|
181
|
+
error=error_msg
|
181
182
|
))
|
182
|
-
logger.warning(f"
|
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=
|
197
|
+
error=error_msg
|
188
198
|
))
|
189
|
-
logger.warning(
|
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
|
-
|
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=
|
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
|
-
#
|
258
|
-
|
259
|
-
|
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="
|
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
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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("
|
61
|
+
await self.animate_verification("Verifying provider port status...")
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
73
|
-
print("
|
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
|
-
|
76
|
-
|
77
|
-
|
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"└─
|
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("└─
|
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}")
|
@@ -7,7 +7,7 @@ from typing import Optional, Set, List, Dict
|
|
7
7
|
from threading import Lock
|
8
8
|
|
9
9
|
from ..config import settings
|
10
|
-
from ..network.port_verifier import PortVerifier, PortVerificationResult
|
10
|
+
from ..network.port_verifier import PortVerifier, PortVerificationResult, ServerAttempt
|
11
11
|
from ..utils.port_display import PortVerificationDisplay
|
12
12
|
|
13
13
|
logger = logging.getLogger(__name__)
|
@@ -40,10 +40,10 @@ class PortManager:
|
|
40
40
|
|
41
41
|
# Initialize port verifier with default servers
|
42
42
|
self.port_check_servers = port_check_servers or [
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
"http://
|
43
|
+
"http://localhost:9000", # Local development server
|
44
|
+
"http://portcheck1.golem.network:7466", # Production servers
|
45
|
+
"http://portcheck2.golem.network:7466",
|
46
|
+
"http://portcheck3.golem.network:7466"
|
47
47
|
]
|
48
48
|
self.discovery_port = discovery_port or settings.PORT
|
49
49
|
self.port_verifier = PortVerifier(
|
@@ -68,18 +68,43 @@ class PortManager:
|
|
68
68
|
)
|
69
69
|
display.print_header()
|
70
70
|
|
71
|
-
#
|
72
|
-
|
73
|
-
logger.info(f"
|
71
|
+
# Only verify SSH ports since provider port was already verified
|
72
|
+
ssh_ports = list(range(self.start_port, self.end_port))
|
73
|
+
logger.info(f"Starting port verification...")
|
74
|
+
logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
|
75
|
+
logger.info(f"Using port check servers: {', '.join(self.port_check_servers)}")
|
74
76
|
|
75
|
-
results = await self.port_verifier.verify_ports(
|
77
|
+
results = await self.port_verifier.verify_ports(ssh_ports)
|
76
78
|
|
79
|
+
# Add provider port as verified since we already checked it
|
80
|
+
results[self.discovery_port] = PortVerificationResult(
|
81
|
+
port=self.discovery_port,
|
82
|
+
accessible=True,
|
83
|
+
verified_by="local_verification",
|
84
|
+
attempts=[ServerAttempt(server="local_verification", success=True)]
|
85
|
+
)
|
86
|
+
|
87
|
+
# Check if discovery port was verified
|
88
|
+
if self.discovery_port not in results:
|
89
|
+
error_msg = f"Port {self.discovery_port} verification failed"
|
90
|
+
logger.error(error_msg)
|
91
|
+
display.print_summary(
|
92
|
+
PortVerificationResult(
|
93
|
+
port=self.discovery_port,
|
94
|
+
accessible=False,
|
95
|
+
error=error_msg
|
96
|
+
),
|
97
|
+
{}
|
98
|
+
)
|
99
|
+
return False
|
100
|
+
|
77
101
|
# Display discovery port status with animation
|
78
102
|
discovery_result = results[self.discovery_port]
|
79
103
|
await display.print_discovery_status(discovery_result)
|
80
104
|
|
81
105
|
if not discovery_result.accessible:
|
82
|
-
|
106
|
+
error_msg = discovery_result.error or f"Port {self.discovery_port} is not accessible"
|
107
|
+
logger.error(f"Failed to verify discovery port: {error_msg}")
|
83
108
|
# Print summary before returning
|
84
109
|
display.print_summary(discovery_result, {})
|
85
110
|
return False
|
@@ -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
|
-
|
62
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
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,9 +131,15 @@ class ProxyServer:
|
|
120
131
|
async def stop(self) -> None:
|
121
132
|
"""Stop the proxy server."""
|
122
133
|
if self.server:
|
123
|
-
|
124
|
-
|
125
|
-
|
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."""
|
@@ -230,10 +247,25 @@ class PythonProxyManager:
|
|
230
247
|
|
231
248
|
async def cleanup(self) -> None:
|
232
249
|
"""Remove all proxy configurations."""
|
233
|
-
|
234
|
-
|
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
|
-
|
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.
|
3
|
+
version = "0.1.2"
|
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"
|
@@ -1,125 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
from fastapi import FastAPI
|
3
|
-
from typing import Optional
|
4
|
-
|
5
|
-
from .config import settings
|
6
|
-
from .utils.logging import setup_logger, PROCESS, SUCCESS
|
7
|
-
from .utils.ascii_art import startup_animation
|
8
|
-
from .discovery.resource_tracker import ResourceTracker
|
9
|
-
from .discovery.advertiser import ResourceAdvertiser
|
10
|
-
from .vm.multipass import MultipassProvider
|
11
|
-
from .vm.port_manager import PortManager
|
12
|
-
|
13
|
-
logger = setup_logger(__name__)
|
14
|
-
|
15
|
-
app = FastAPI(title="VM on Golem Provider")
|
16
|
-
|
17
|
-
async def setup_provider() -> None:
|
18
|
-
"""Setup and initialize the provider components."""
|
19
|
-
try:
|
20
|
-
# Initialize port manager (verification already done in run.py)
|
21
|
-
logger.process("🔄 Initializing port manager...")
|
22
|
-
port_manager = PortManager()
|
23
|
-
app.state.port_manager = port_manager
|
24
|
-
|
25
|
-
# Create resource tracker
|
26
|
-
logger.process("🔄 Initializing resource tracker...")
|
27
|
-
resource_tracker = ResourceTracker()
|
28
|
-
app.state.resource_tracker = resource_tracker
|
29
|
-
|
30
|
-
# Create provider with resource tracker and port manager
|
31
|
-
logger.process("🔄 Initializing VM provider...")
|
32
|
-
provider = MultipassProvider(resource_tracker, port_manager=port_manager)
|
33
|
-
try:
|
34
|
-
await asyncio.wait_for(provider.initialize(), timeout=30)
|
35
|
-
app.state.provider = provider
|
36
|
-
|
37
|
-
# Store proxy manager reference for cleanup
|
38
|
-
app.state.proxy_manager = provider.proxy_manager
|
39
|
-
|
40
|
-
except asyncio.TimeoutError:
|
41
|
-
logger.error("Provider initialization timed out")
|
42
|
-
raise
|
43
|
-
except Exception as e:
|
44
|
-
logger.error(f"Failed to initialize provider: {e}")
|
45
|
-
raise
|
46
|
-
|
47
|
-
# Create and start advertiser in background
|
48
|
-
logger.process("🔄 Starting resource advertiser...")
|
49
|
-
advertiser = ResourceAdvertiser(
|
50
|
-
resource_tracker=resource_tracker,
|
51
|
-
discovery_url=settings.DISCOVERY_URL,
|
52
|
-
provider_id=settings.PROVIDER_ID
|
53
|
-
)
|
54
|
-
|
55
|
-
# Start advertiser in background task
|
56
|
-
app.state.advertiser_task = asyncio.create_task(advertiser.start())
|
57
|
-
app.state.advertiser = advertiser
|
58
|
-
|
59
|
-
logger.success("✨ Provider setup complete and ready to accept requests")
|
60
|
-
except Exception as e:
|
61
|
-
logger.error(f"Failed to setup provider: {e}")
|
62
|
-
# Attempt cleanup of any initialized components
|
63
|
-
await cleanup_provider()
|
64
|
-
raise
|
65
|
-
|
66
|
-
async def cleanup_provider() -> None:
|
67
|
-
"""Cleanup provider components."""
|
68
|
-
cleanup_errors = []
|
69
|
-
|
70
|
-
# Stop advertiser
|
71
|
-
if hasattr(app.state, "advertiser"):
|
72
|
-
try:
|
73
|
-
await app.state.advertiser.stop()
|
74
|
-
if hasattr(app.state, "advertiser_task"):
|
75
|
-
app.state.advertiser_task.cancel()
|
76
|
-
try:
|
77
|
-
await app.state.advertiser_task
|
78
|
-
except asyncio.CancelledError:
|
79
|
-
pass
|
80
|
-
except Exception as e:
|
81
|
-
cleanup_errors.append(f"Failed to stop advertiser: {e}")
|
82
|
-
|
83
|
-
# Cleanup proxy manager first to stop all proxy servers
|
84
|
-
if hasattr(app.state, "proxy_manager"):
|
85
|
-
try:
|
86
|
-
await asyncio.wait_for(app.state.proxy_manager.cleanup(), timeout=30)
|
87
|
-
except asyncio.TimeoutError:
|
88
|
-
cleanup_errors.append("Proxy manager cleanup timed out")
|
89
|
-
except Exception as e:
|
90
|
-
cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
|
91
|
-
|
92
|
-
# Cleanup provider
|
93
|
-
if hasattr(app.state, "provider"):
|
94
|
-
try:
|
95
|
-
await asyncio.wait_for(app.state.provider.cleanup(), timeout=30)
|
96
|
-
except asyncio.TimeoutError:
|
97
|
-
cleanup_errors.append("Provider cleanup timed out")
|
98
|
-
except Exception as e:
|
99
|
-
cleanup_errors.append(f"Failed to cleanup provider: {e}")
|
100
|
-
|
101
|
-
if cleanup_errors:
|
102
|
-
error_msg = "\n".join(cleanup_errors)
|
103
|
-
logger.error(f"Errors during cleanup:\n{error_msg}")
|
104
|
-
else:
|
105
|
-
logger.success("✨ Provider cleanup complete")
|
106
|
-
|
107
|
-
@app.on_event("startup")
|
108
|
-
async def startup_event():
|
109
|
-
"""Handle application startup."""
|
110
|
-
# Display startup animation
|
111
|
-
await startup_animation()
|
112
|
-
# Initialize provider
|
113
|
-
await setup_provider()
|
114
|
-
|
115
|
-
@app.on_event("shutdown")
|
116
|
-
async def shutdown_event():
|
117
|
-
"""Handle application shutdown."""
|
118
|
-
await cleanup_provider()
|
119
|
-
|
120
|
-
# Import routes after app creation to avoid circular imports
|
121
|
-
from .api import routes
|
122
|
-
app.include_router(routes.router, prefix="/api/v1")
|
123
|
-
|
124
|
-
# Export app for uvicorn
|
125
|
-
__all__ = ["app"]
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|