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.
- golem_vm_provider-0.1.0.dist-info/METADATA +398 -0
- golem_vm_provider-0.1.0.dist-info/RECORD +26 -0
- golem_vm_provider-0.1.0.dist-info/WHEEL +4 -0
- golem_vm_provider-0.1.0.dist-info/entry_points.txt +3 -0
- provider/__init__.py +3 -0
- provider/api/__init__.py +19 -0
- provider/api/models.py +108 -0
- provider/api/routes.py +159 -0
- provider/config.py +160 -0
- provider/discovery/__init__.py +6 -0
- provider/discovery/advertiser.py +179 -0
- provider/discovery/resource_tracker.py +152 -0
- provider/main.py +125 -0
- provider/network/port_verifier.py +287 -0
- provider/security/ethereum.py +41 -0
- provider/utils/ascii_art.py +79 -0
- provider/utils/logging.py +82 -0
- provider/utils/port_display.py +204 -0
- provider/utils/retry.py +48 -0
- provider/vm/__init__.py +31 -0
- provider/vm/cloud_init.py +67 -0
- provider/vm/models.py +205 -0
- provider/vm/multipass.py +427 -0
- provider/vm/name_mapper.py +108 -0
- provider/vm/port_manager.py +196 -0
- provider/vm/proxy_manager.py +239 -0
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()
|