golem-vm-provider 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
provider/main.py ADDED
@@ -0,0 +1,125 @@
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"]
@@ -0,0 +1,287 @@
1
+ import socket
2
+ import asyncio
3
+ import aiohttp
4
+ import logging
5
+ from typing import Set, List, Dict, Optional, Tuple
6
+ from dataclasses import dataclass
7
+ import requests
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class ServerAttempt:
14
+ """Result of a single server's verification attempt."""
15
+ server: str
16
+ success: bool
17
+ error: Optional[str] = None
18
+
19
+ @dataclass
20
+ class PortVerificationResult:
21
+ """Result of port verification."""
22
+ port: int
23
+ accessible: bool
24
+ error: str = None
25
+ verified_by: str = None # Server that successfully verified the port
26
+ attempts: List[ServerAttempt] = None # Track all server attempts
27
+
28
+ class PortVerifier:
29
+ """Verifies port accessibility both locally and externally."""
30
+
31
+ def __init__(self, port_check_servers: List[str], discovery_port: int = 7466):
32
+ """Initialize port verifier.
33
+
34
+ Args:
35
+ port_check_servers: List of URLs for port checking services
36
+ discovery_port: Port used for discovery service
37
+ """
38
+ self.port_check_servers = port_check_servers
39
+ self.discovery_port = discovery_port
40
+
41
+ async def verify_local_binding(self, ports: List[int]) -> Set[int]:
42
+ """Try to bind to ports locally to verify availability.
43
+
44
+ Args:
45
+ ports: List of ports to verify
46
+
47
+ Returns:
48
+ Set of ports that were successfully bound
49
+ """
50
+ available_ports = set()
51
+ temp_listeners = []
52
+
53
+ for port in ports:
54
+ try:
55
+ # For discovery port, create a temporary TCP listener
56
+ if port == self.discovery_port:
57
+ try:
58
+ server = await asyncio.start_server(
59
+ lambda r, w: None, # Empty callback since we just need to listen
60
+ '0.0.0.0',
61
+ port
62
+ )
63
+ temp_listeners.append(server)
64
+ available_ports.add(port)
65
+ logger.debug(f"Created temporary listener for discovery port {port}")
66
+ continue
67
+ except Exception as e:
68
+ if isinstance(e, OSError) and e.errno == 98: # Address already in use
69
+ # This might be our own server starting up
70
+ available_ports.add(port)
71
+ logger.debug(f"Port {port} is already in use - this is expected if our server is starting")
72
+ continue
73
+ logger.debug(f"Failed to create temporary listener for discovery port {port}: {e}")
74
+ continue
75
+
76
+ # For other ports, create a TCP listener
77
+ try:
78
+ server = await asyncio.start_server(
79
+ lambda r, w: None, # Empty callback since we just need to listen
80
+ '0.0.0.0',
81
+ port
82
+ )
83
+ temp_listeners.append(server)
84
+ available_ports.add(port)
85
+ logger.debug(f"Created temporary listener on port {port}")
86
+ except Exception as e:
87
+ if isinstance(e, OSError) and e.errno == 98: # Address already in use
88
+ logger.debug(f"Port {port} is already in use")
89
+ else:
90
+ logger.debug(f"Failed to bind to port {port}: {e}")
91
+ continue
92
+ except Exception as e:
93
+ logger.debug(f"Failed to bind to port {port}: {e}")
94
+ continue
95
+
96
+ try:
97
+ # Keep all temporary listeners active during verification
98
+ yield available_ports
99
+ finally:
100
+ # Cleanup temporary listeners
101
+ for server in temp_listeners:
102
+ server.close()
103
+ await server.wait_closed()
104
+ if temp_listeners:
105
+ logger.debug(f"Closed {len(temp_listeners)} temporary listeners")
106
+
107
+ async def _get_public_ip(self) -> str:
108
+ """Get public IP address using external service."""
109
+ try:
110
+ async with aiohttp.ClientSession() as session:
111
+ async with session.get('https://api.ipify.org') as response:
112
+ return await response.text()
113
+ except Exception as e:
114
+ # Fallback to non-async request if aiohttp fails
115
+ return requests.get('https://api.ipify.org').text
116
+
117
+ async def verify_external_access(
118
+ self,
119
+ ports: Set[int]
120
+ ) -> Dict[int, PortVerificationResult]:
121
+ """Verify external accessibility using port check servers.
122
+
123
+ Args:
124
+ ports: Set of ports to verify
125
+
126
+ Returns:
127
+ Dictionary mapping ports to their verification results
128
+ """
129
+ results = {}
130
+ attempts = []
131
+
132
+ # Try each server
133
+ for server in self.port_check_servers:
134
+ try:
135
+ public_ip = await self._get_public_ip()
136
+
137
+ async with aiohttp.ClientSession() as session:
138
+ response = await session.post(
139
+ f"{server}/check-ports",
140
+ json={
141
+ "provider_ip": public_ip,
142
+ "ports": list(ports)
143
+ },
144
+ timeout=30 # 30 second timeout for port checking
145
+ )
146
+
147
+ if response.status == 200:
148
+ data = await response.json()
149
+ if data["success"]:
150
+ # Convert server results to PortVerificationResult objects
151
+ for port_str, result in data["results"].items():
152
+ port = int(port_str)
153
+ if port not in results or not results[port].accessible:
154
+ # Only update if we haven't found a successful verification yet
155
+ results[port] = PortVerificationResult(
156
+ port=port,
157
+ accessible=result["accessible"],
158
+ error=result.get("error"),
159
+ verified_by=server if result["accessible"] else None,
160
+ attempts=[] # Will be filled at the end
161
+ )
162
+ attempts.append(ServerAttempt(server=server, success=True))
163
+ logger.info(f"Port verification completed using {server}")
164
+ else:
165
+ attempts.append(ServerAttempt(
166
+ server=server,
167
+ success=False,
168
+ error="Server returned unsuccessful response"
169
+ ))
170
+ else:
171
+ attempts.append(ServerAttempt(
172
+ server=server,
173
+ success=False,
174
+ error=f"Server returned status {response.status}"
175
+ ))
176
+ except asyncio.TimeoutError:
177
+ attempts.append(ServerAttempt(
178
+ server=server,
179
+ success=False,
180
+ error="Connection timed out"
181
+ ))
182
+ logger.warning(f"Timeout while verifying ports with {server}")
183
+ except Exception as e:
184
+ attempts.append(ServerAttempt(
185
+ server=server,
186
+ success=False,
187
+ error=str(e)
188
+ ))
189
+ logger.warning(f"Failed to verify ports with {server}: {e}")
190
+
191
+ # If no successful verifications, mark all ports as inaccessible
192
+ if not any(result.accessible for result in results.values()):
193
+ logger.error("Failed to verify ports with any server")
194
+ results = {
195
+ port: PortVerificationResult(
196
+ port=port,
197
+ accessible=False,
198
+ error="Failed to verify with any port check server",
199
+ attempts=[] # Will be filled below
200
+ )
201
+ for port in ports
202
+ }
203
+
204
+ # Add attempts to all results
205
+ for result in results.values():
206
+ result.attempts = attempts
207
+
208
+ return results
209
+
210
+ async def _create_temp_listener(self, port: int) -> Optional[asyncio.Server]:
211
+ """Create a temporary TCP listener for port verification."""
212
+ try:
213
+ # First check if port is already in use
214
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
215
+ sock.settimeout(1)
216
+ result = sock.connect_ex(('127.0.0.1', port))
217
+ sock.close()
218
+
219
+ if result == 0:
220
+ logger.debug(f"Port {port} is already in use - this is expected if our server is running")
221
+ return None
222
+
223
+ try:
224
+ server = await asyncio.start_server(
225
+ lambda r, w: None, # Empty callback since we just need to listen
226
+ '0.0.0.0',
227
+ port
228
+ )
229
+ logger.debug(f"Created temporary listener on port {port}")
230
+ return server
231
+ except PermissionError:
232
+ logger.error(f"Permission denied when trying to bind to port {port}")
233
+ return None
234
+ except OSError as e:
235
+ if e.errno == 98: # Address already in use
236
+ logger.debug(f"Port {port} is already in use - this is expected if our server is running")
237
+ return None
238
+ logger.error(f"Failed to create listener on port {port}: {e}")
239
+ return None
240
+ except Exception as e:
241
+ logger.error(f"Unexpected error creating listener on port {port}: {e}")
242
+ return None
243
+
244
+ except Exception as e:
245
+ logger.error(f"Failed to check port {port} status: {e}")
246
+ return None
247
+
248
+ async def verify_ports(self, ports: List[int]) -> Dict[int, PortVerificationResult]:
249
+ """Verify ports both locally and externally.
250
+
251
+ Args:
252
+ ports: List of ports to verify
253
+
254
+ Returns:
255
+ Dictionary mapping ports to their verification results
256
+ """
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
260
+
261
+ # First verify ports with local binding
262
+ logger.info("Checking local port availability...")
263
+ async for local_available in self.verify_local_binding(ports):
264
+ if not local_available:
265
+ logger.error("No ports available for local binding")
266
+ return {
267
+ port: PortVerificationResult(
268
+ port=port,
269
+ accessible=False,
270
+ error="Failed to bind locally"
271
+ )
272
+ for port in ports
273
+ }
274
+
275
+ # Verify external access while listeners are active
276
+ logger.info("Starting external port verification...")
277
+ results = await self.verify_external_access(local_available)
278
+
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}")
286
+
287
+ return results
@@ -0,0 +1,41 @@
1
+ """Ethereum key management for provider identity."""
2
+ import os
3
+ from pathlib import Path
4
+ from eth_account import Account
5
+ import json
6
+
7
+ class EthereumIdentity:
8
+ """Manage provider's Ethereum identity."""
9
+
10
+ def __init__(self, key_dir: str = None):
11
+ if key_dir is None:
12
+ key_dir = str(Path.home() / ".golem" / "provider" / "keys")
13
+ self.key_dir = Path(key_dir)
14
+ self.key_file = self.key_dir / "provider_key.json"
15
+
16
+ def get_or_create_identity(self) -> str:
17
+ """Get existing provider ID or create new one from Ethereum key."""
18
+ # Create directory if it doesn't exist
19
+ self.key_dir.mkdir(parents=True, exist_ok=True)
20
+ self.key_dir.chmod(0o700) # Secure permissions
21
+
22
+ # Check for existing key
23
+ if self.key_file.exists():
24
+ with open(self.key_file) as f:
25
+ key_data = json.load(f)
26
+ return key_data["address"]
27
+
28
+ # Generate new key
29
+ Account.enable_unaudited_hdwallet_features()
30
+ acct = Account.create()
31
+
32
+ # Save key securely
33
+ key_data = {
34
+ "address": acct.address,
35
+ "private_key": acct.key.hex()
36
+ }
37
+ with open(self.key_file, "w") as f:
38
+ json.dump(key_data, f)
39
+ self.key_file.chmod(0o600) # Secure permissions
40
+
41
+ return acct.address
@@ -0,0 +1,79 @@
1
+ from rich.console import Console
2
+ from rich.live import Live
3
+ from rich.panel import Panel
4
+ from rich.text import Text
5
+ from rich.style import Style
6
+ import time
7
+ import asyncio
8
+
9
+ # Initialize Rich console
10
+ console = Console()
11
+
12
+ # ASCII Art Logo
13
+ LOGO = """
14
+ [bold cyan] _ ____ __ __[/bold cyan]
15
+ [bold cyan]| | / / |/ / /___ _____[/bold cyan]
16
+ [bold cyan]| | / / /|_/ / __/ // / _ \\[/bold cyan]
17
+ [bold cyan]| |/ / / / / /_/ _ / __/[/bold cyan]
18
+ [bold cyan]|___/_/ /_/\\__/_//_/\\___/[/bold cyan]
19
+ [bold magenta] ____ _ __[/bold magenta] [bold yellow]______[/bold yellow] [bold green]____ __ ________ ___ [/bold green]
20
+ [bold magenta] / __ \\/ | / /[/bold magenta] [bold yellow]/ ____/[/bold yellow] [bold green]/ __ \\/ / / ____/ |/ /[/bold green]
21
+ [bold magenta] / / / / |/ /[/bold magenta] [bold yellow]/ / __[/bold yellow] [bold green]/ / / / / / __/ / /|_/ / [/bold green]
22
+ [bold magenta]/ /_/ / /| /[/bold magenta] [bold yellow]/ /_/ /[/bold yellow] [bold green]/ /_/ / /___/ /___/ / / / [/bold green]
23
+ [bold magenta]\\____/_/ |_/[/bold magenta] [bold yellow]\\____/[/bold yellow] [bold green]/_____/_____/_____/_/ /_/ [/bold green]
24
+ """
25
+
26
+ # Spinner frames (reduced for faster animation)
27
+ SPINNER_FRAMES = ["⠋", "⠙", "⠸", "⠴"]
28
+
29
+ def display_logo():
30
+ """Display the VM on Golem logo."""
31
+ console.print(LOGO)
32
+ console.print()
33
+
34
+ async def startup_animation():
35
+ """Display startup animation."""
36
+ display_logo()
37
+
38
+ with Live(refresh_per_second=8) as live:
39
+ # Startup message
40
+ live.console.print("\n[bold yellow]🚀 Initializing VM on Golem Provider...[/bold yellow]")
41
+ await asyncio.sleep(0.1)
42
+
43
+ # Components check animation (reduced)
44
+ components = [
45
+ "Loading configuration",
46
+ "Starting provider services",
47
+ "Connecting to network"
48
+ ]
49
+
50
+ for component in components:
51
+ for frame in SPINNER_FRAMES:
52
+ live.update(f"[bold blue]{frame}[/bold blue] {component}...")
53
+ await asyncio.sleep(0.1)
54
+ live.console.print(f"[bold green]✓[/bold green] {component} [dim]complete[/dim]")
55
+
56
+ # Final ready message
57
+ live.console.print("\n[bold green]✨ VM on Golem Provider is ready![/bold green]")
58
+ live.console.print("[bold cyan]🌐 Listening for incoming requests on port 7466[/bold cyan]")
59
+ live.console.print("[dim]Press Ctrl+C to stop the server[/dim]")
60
+
61
+ async def vm_creation_animation(vm_name: str):
62
+ """Display VM creation success message.
63
+
64
+ Args:
65
+ vm_name: Name of the VM being created
66
+ """
67
+ console.print(f"[bold green]✨ VM '{vm_name}' is now being rented. Access information has been forwarded to the requestor.[/bold green]")
68
+
69
+ def vm_status_change(vm_id: str, status: str, details: str = ""):
70
+ """Display VM status change.
71
+
72
+ Args:
73
+ vm_id: VM identifier
74
+ status: New status
75
+ details: Additional details (optional)
76
+ """
77
+ # Only show final status
78
+ if status.lower() == "running":
79
+ console.print(f"[green]✓[/green] VM {vm_id} is {status.lower()}")
@@ -0,0 +1,82 @@
1
+ import logging
2
+ import colorlog
3
+ import sys
4
+ from typing import Optional
5
+
6
+ # Import standard logging levels
7
+ from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
8
+
9
+ # Custom log levels
10
+ PROCESS = 25 # Between INFO and WARNING
11
+ SUCCESS = 35 # Between WARNING and ERROR
12
+
13
+ # Add custom levels to logging
14
+ logging.addLevelName(PROCESS, 'PROCESS')
15
+ logging.addLevelName(SUCCESS, 'SUCCESS')
16
+
17
+ def process(self, message, *args, **kwargs):
18
+ """Log 'msg % args' with severity 'PROCESS'."""
19
+ if self.isEnabledFor(PROCESS):
20
+ self._log(PROCESS, message, args, **kwargs)
21
+
22
+ def success(self, message, *args, **kwargs):
23
+ """Log 'msg % args' with severity 'SUCCESS'."""
24
+ if self.isEnabledFor(SUCCESS):
25
+ self._log(SUCCESS, message, args, **kwargs)
26
+
27
+ # Add methods to Logger class
28
+ logging.Logger.process = process
29
+ logging.Logger.success = success
30
+
31
+ def setup_logger(name: Optional[str] = None, debug: bool = False) -> logging.Logger:
32
+ """Setup and return a colored logger.
33
+
34
+ Args:
35
+ name: Logger name (optional)
36
+ debug: Whether to show debug logs (optional)
37
+
38
+ Returns:
39
+ Configured logger instance
40
+ """
41
+ logger = logging.getLogger(name or __name__)
42
+ logger.handlers = [] # Clear existing handlers
43
+
44
+ # Fancy handler for important logs
45
+ fancy_handler = colorlog.StreamHandler(sys.stdout)
46
+ fancy_formatter = colorlog.ColoredFormatter(
47
+ "%(log_color)s[%(asctime)s] %(message)s",
48
+ datefmt="%Y-%m-%d %H:%M:%S",
49
+ reset=True,
50
+ log_colors={
51
+ 'INFO': 'green',
52
+ 'PROCESS': 'yellow',
53
+ 'WARNING': 'yellow',
54
+ 'SUCCESS': 'green,bold',
55
+ 'ERROR': 'red',
56
+ 'CRITICAL': 'red,bold',
57
+ },
58
+ secondary_log_colors={},
59
+ style='%'
60
+ )
61
+ fancy_handler.setFormatter(fancy_formatter)
62
+ fancy_handler.addFilter(lambda record: record.levelno in [INFO, PROCESS, SUCCESS, WARNING, ERROR, CRITICAL])
63
+ logger.addHandler(fancy_handler)
64
+
65
+ if debug:
66
+ # Debug handler for detailed logs
67
+ debug_handler = logging.StreamHandler(sys.stdout)
68
+ debug_formatter = logging.Formatter(
69
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
70
+ datefmt="%Y-%m-%d %H:%M:%S"
71
+ )
72
+ debug_handler.setFormatter(debug_formatter)
73
+ debug_handler.addFilter(lambda record: record.levelno == DEBUG)
74
+ logger.addHandler(debug_handler)
75
+ logger.setLevel(logging.DEBUG)
76
+ else:
77
+ logger.setLevel(logging.INFO)
78
+
79
+ return logger
80
+
81
+ # Create default logger
82
+ logger = setup_logger()