golem-vm-provider 0.1.24__py3-none-any.whl → 0.1.27__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.24.dist-info → golem_vm_provider-0.1.27.dist-info}/METADATA +17 -10
- golem_vm_provider-0.1.27.dist-info/RECORD +38 -0
- {golem_vm_provider-0.1.24.dist-info → golem_vm_provider-0.1.27.dist-info}/WHEEL +1 -1
- golem_vm_provider-0.1.27.dist-info/entry_points.txt +4 -0
- provider/api/models.py +10 -10
- provider/api/routes.py +89 -95
- provider/config.py +101 -21
- provider/container.py +84 -0
- provider/discovery/__init__.py +8 -2
- provider/discovery/advertiser.py +41 -63
- provider/discovery/golem_base_advertiser.py +135 -0
- provider/discovery/golem_base_utils.py +10 -0
- provider/discovery/resource_monitor.py +34 -0
- provider/discovery/resource_tracker.py +1 -1
- provider/discovery/service.py +24 -0
- provider/main.py +88 -171
- provider/security/ethereum.py +9 -13
- provider/security/faucet.py +132 -0
- provider/service.py +67 -0
- provider/utils/__init__.py +0 -0
- provider/utils/logging.py +11 -27
- provider/utils/port_display.py +27 -1
- provider/utils/retry.py +39 -0
- provider/vm/__init__.py +1 -1
- provider/vm/models.py +13 -12
- provider/vm/multipass.py +2 -416
- provider/vm/multipass_adapter.py +221 -0
- provider/vm/name_mapper.py +5 -5
- provider/vm/port_manager.py +67 -26
- provider/vm/provider.py +48 -0
- provider/vm/proxy_manager.py +4 -3
- provider/vm/service.py +91 -0
- golem_vm_provider-0.1.24.dist-info/RECORD +0 -27
- golem_vm_provider-0.1.24.dist-info/entry_points.txt +0 -3
provider/main.py
CHANGED
@@ -1,185 +1,73 @@
|
|
1
|
+
from .api import routes
|
1
2
|
import asyncio
|
2
3
|
import os
|
4
|
+
import socket
|
3
5
|
from fastapi import FastAPI
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
4
7
|
from typing import Optional
|
5
8
|
|
6
9
|
from .config import settings
|
7
10
|
from .utils.logging import setup_logger, PROCESS, SUCCESS
|
8
11
|
from .utils.ascii_art import startup_animation
|
9
12
|
from .discovery.resource_tracker import ResourceTracker
|
10
|
-
from .discovery.advertiser import
|
11
|
-
from .
|
12
|
-
from .
|
13
|
-
|
14
|
-
logger = setup_logger(__name__)
|
13
|
+
from .discovery.advertiser import DiscoveryServerAdvertiser
|
14
|
+
from .container import Container
|
15
|
+
from .service import ProviderService
|
16
|
+
from .utils.logging import setup_logger
|
15
17
|
|
16
18
|
app = FastAPI(title="VM on Golem Provider")
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# Store port manager references
|
50
|
-
app.state.port_manager = port_manager
|
51
|
-
provider.port_manager = port_manager
|
52
|
-
app.state.proxy_manager.port_manager = port_manager
|
53
|
-
|
54
|
-
# Now restore proxy configurations using only verified ports
|
55
|
-
logger.process("🔄 Restoring proxy configurations...")
|
56
|
-
await app.state.proxy_manager._load_state()
|
57
|
-
|
58
|
-
except asyncio.TimeoutError:
|
59
|
-
logger.error("Provider initialization timed out")
|
60
|
-
raise
|
61
|
-
except Exception as e:
|
62
|
-
logger.error(f"Failed to initialize provider: {e}")
|
63
|
-
raise
|
64
|
-
|
65
|
-
# Create and start advertiser in background
|
66
|
-
logger.process("🔄 Starting resource advertiser...")
|
67
|
-
advertiser = ResourceAdvertiser(
|
68
|
-
resource_tracker=resource_tracker,
|
69
|
-
discovery_url=settings.DISCOVERY_URL,
|
70
|
-
provider_id=settings.PROVIDER_ID
|
71
|
-
)
|
72
|
-
|
73
|
-
# Start advertiser in background task
|
74
|
-
app.state.advertiser_task = asyncio.create_task(advertiser.start())
|
75
|
-
app.state.advertiser = advertiser
|
76
|
-
|
77
|
-
logger.success("✨ Provider setup complete and ready to accept requests")
|
78
|
-
except Exception as e:
|
79
|
-
logger.error(f"Failed to setup provider: {e}")
|
80
|
-
# Attempt cleanup of any initialized components
|
81
|
-
await cleanup_provider()
|
82
|
-
raise
|
83
|
-
|
84
|
-
async def cleanup_provider() -> None:
|
85
|
-
"""Cleanup provider components."""
|
86
|
-
cleanup_errors = []
|
87
|
-
|
88
|
-
# Stop advertiser
|
89
|
-
if hasattr(app.state, "advertiser"):
|
90
|
-
try:
|
91
|
-
await app.state.advertiser.stop()
|
92
|
-
if hasattr(app.state, "advertiser_task"):
|
93
|
-
app.state.advertiser_task.cancel()
|
94
|
-
try:
|
95
|
-
await app.state.advertiser_task
|
96
|
-
except asyncio.CancelledError:
|
97
|
-
pass
|
98
|
-
except Exception as e:
|
99
|
-
cleanup_errors.append(f"Failed to stop advertiser: {e}")
|
100
|
-
|
101
|
-
# Cleanup proxy manager first to stop all proxy servers
|
102
|
-
if hasattr(app.state, "proxy_manager"):
|
103
|
-
try:
|
104
|
-
await asyncio.wait_for(app.state.proxy_manager.cleanup(), timeout=30)
|
105
|
-
except asyncio.TimeoutError:
|
106
|
-
cleanup_errors.append("Proxy manager cleanup timed out")
|
107
|
-
except Exception as e:
|
108
|
-
cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
|
109
|
-
|
110
|
-
# Cleanup provider
|
111
|
-
if hasattr(app.state, "provider"):
|
112
|
-
try:
|
113
|
-
await asyncio.wait_for(app.state.provider.cleanup(), timeout=30)
|
114
|
-
except asyncio.TimeoutError:
|
115
|
-
cleanup_errors.append("Provider cleanup timed out")
|
116
|
-
except Exception as e:
|
117
|
-
cleanup_errors.append(f"Failed to cleanup provider: {e}")
|
118
|
-
|
119
|
-
if cleanup_errors:
|
120
|
-
error_msg = "\n".join(cleanup_errors)
|
121
|
-
logger.error(f"Errors during cleanup:\n{error_msg}")
|
122
|
-
else:
|
123
|
-
logger.success("✨ Provider cleanup complete")
|
19
|
+
container = Container()
|
20
|
+
container.config.from_pydantic(settings)
|
21
|
+
app.container = container
|
22
|
+
container.wire(modules=[".api.routes"])
|
23
|
+
|
24
|
+
from .vm.models import VMNotFoundError
|
25
|
+
from fastapi import Request
|
26
|
+
from fastapi.responses import JSONResponse
|
27
|
+
|
28
|
+
@app.exception_handler(VMNotFoundError)
|
29
|
+
async def vm_not_found_exception_handler(request: Request, exc: VMNotFoundError):
|
30
|
+
return JSONResponse(
|
31
|
+
status_code=404,
|
32
|
+
content={"message": str(exc)},
|
33
|
+
)
|
34
|
+
|
35
|
+
@app.exception_handler(Exception)
|
36
|
+
async def generic_exception_handler(request: Request, exc: Exception):
|
37
|
+
return JSONResponse(
|
38
|
+
status_code=500,
|
39
|
+
content={"message": "An unexpected error occurred"},
|
40
|
+
)
|
41
|
+
|
42
|
+
# Add CORS middleware
|
43
|
+
app.add_middleware(
|
44
|
+
CORSMiddleware,
|
45
|
+
allow_origins=["*"], # Allows all origins
|
46
|
+
allow_credentials=True,
|
47
|
+
allow_methods=["*"], # Allows all methods
|
48
|
+
allow_headers=["*"], # Allows all headers
|
49
|
+
)
|
124
50
|
|
125
51
|
@app.on_event("startup")
|
126
52
|
async def startup_event():
|
127
53
|
"""Handle application startup."""
|
128
|
-
|
129
|
-
|
130
|
-
await startup_animation()
|
131
|
-
|
132
|
-
# Verify ports first
|
133
|
-
from .vm.port_manager import PortManager
|
134
|
-
from .utils.port_display import PortVerificationDisplay
|
135
|
-
from .config import settings
|
54
|
+
provider_service = container.provider_service()
|
55
|
+
await provider_service.setup(app)
|
136
56
|
|
137
|
-
display = PortVerificationDisplay(
|
138
|
-
provider_port=settings.PORT,
|
139
|
-
port_range_start=settings.PORT_RANGE_START,
|
140
|
-
port_range_end=settings.PORT_RANGE_END
|
141
|
-
)
|
142
|
-
display.print_header()
|
143
|
-
|
144
|
-
# Initialize port manager
|
145
|
-
logger.process("🔄 Verifying port accessibility...")
|
146
|
-
port_manager = PortManager(
|
147
|
-
start_port=settings.PORT_RANGE_START,
|
148
|
-
end_port=settings.PORT_RANGE_END,
|
149
|
-
discovery_port=settings.PORT
|
150
|
-
)
|
151
|
-
if not await port_manager.initialize():
|
152
|
-
logger.error("Port verification failed. Please ensure:")
|
153
|
-
logger.error(f"1. Port {settings.PORT} is accessible for provider access")
|
154
|
-
logger.error(f"2. Some ports in range {settings.PORT_RANGE_START}-{settings.PORT_RANGE_END} are accessible for VM access")
|
155
|
-
logger.error("3. Your firewall/router is properly configured")
|
156
|
-
raise RuntimeError("Port verification failed")
|
157
|
-
|
158
|
-
logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
|
159
|
-
|
160
|
-
# Store port manager in app state for later use
|
161
|
-
app.state.port_manager = port_manager
|
162
|
-
|
163
|
-
# Initialize provider
|
164
|
-
await setup_provider()
|
165
|
-
except Exception as e:
|
166
|
-
logger.error(f"Startup failed: {e}")
|
167
|
-
# Ensure proper cleanup
|
168
|
-
await cleanup_provider()
|
169
|
-
raise
|
170
57
|
|
171
58
|
@app.on_event("shutdown")
|
172
59
|
async def shutdown_event():
|
173
60
|
"""Handle application shutdown."""
|
174
|
-
|
61
|
+
provider_service = container.provider_service()
|
62
|
+
await provider_service.cleanup()
|
175
63
|
|
176
64
|
# Import routes after app creation to avoid circular imports
|
177
|
-
from .api import routes
|
178
65
|
app.include_router(routes.router, prefix="/api/v1")
|
179
66
|
|
180
67
|
# Export app for uvicorn
|
181
68
|
__all__ = ["app", "start"]
|
182
69
|
|
70
|
+
|
183
71
|
def check_requirements():
|
184
72
|
"""Check if all requirements are met."""
|
185
73
|
try:
|
@@ -190,12 +78,13 @@ def check_requirements():
|
|
190
78
|
logger.error(f"Requirements check failed: {e}")
|
191
79
|
return False
|
192
80
|
|
81
|
+
|
193
82
|
async def verify_provider_port(port: int) -> bool:
|
194
83
|
"""Verify that the provider port is available for binding.
|
195
|
-
|
84
|
+
|
196
85
|
Args:
|
197
86
|
port: The port to verify
|
198
|
-
|
87
|
+
|
199
88
|
Returns:
|
200
89
|
bool: True if the port is available, False otherwise
|
201
90
|
"""
|
@@ -218,23 +107,47 @@ async def verify_provider_port(port: int) -> bool:
|
|
218
107
|
logger.error("3. Your firewall allows binding to this port")
|
219
108
|
return False
|
220
109
|
|
221
|
-
|
110
|
+
|
111
|
+
# The get_local_ip function has been removed as this logic is now handled in config.py
|
112
|
+
|
113
|
+
|
114
|
+
import typer
|
115
|
+
|
116
|
+
cli = typer.Typer()
|
117
|
+
|
118
|
+
@cli.command()
|
119
|
+
def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
|
222
120
|
"""Start the provider server."""
|
121
|
+
run_server(dev_mode=False, no_verify_port=no_verify_port)
|
122
|
+
|
123
|
+
@cli.command()
|
124
|
+
def dev(no_verify_port: bool = typer.Option(True, "--no-verify-port", help="Skip provider port verification.")):
|
125
|
+
"""Start the provider server in development mode."""
|
126
|
+
run_server(dev_mode=True, no_verify_port=no_verify_port)
|
127
|
+
|
128
|
+
def run_server(dev_mode: bool, no_verify_port: bool):
|
129
|
+
"""Helper to run the uvicorn server."""
|
223
130
|
import sys
|
224
131
|
from pathlib import Path
|
225
132
|
from dotenv import load_dotenv
|
226
133
|
import uvicorn
|
227
134
|
from .utils.logging import setup_logger
|
228
|
-
from .config import settings
|
229
135
|
|
230
|
-
#
|
231
|
-
|
136
|
+
# Load appropriate .env file
|
137
|
+
env_file = ".env.dev" if dev_mode else ".env"
|
138
|
+
env_path = Path(__file__).parent.parent / env_file
|
139
|
+
load_dotenv(dotenv_path=env_path)
|
232
140
|
|
141
|
+
# The logic for setting the public IP in dev mode is now handled in config.py
|
142
|
+
# The following lines are no longer needed and have been removed.
|
143
|
+
|
144
|
+
# Import settings after loading env
|
145
|
+
from .config import settings
|
146
|
+
|
147
|
+
# Configure logging with debug mode
|
148
|
+
logger = setup_logger(__name__, debug=dev_mode)
|
149
|
+
|
233
150
|
try:
|
234
|
-
# Load environment variables from .env file
|
235
|
-
env_path = Path(__file__).parent.parent / '.env'
|
236
|
-
load_dotenv(dotenv_path=env_path)
|
237
|
-
|
238
151
|
# Log environment variables
|
239
152
|
logger.info("Environment variables:")
|
240
153
|
for key, value in os.environ.items():
|
@@ -245,24 +158,25 @@ def start():
|
|
245
158
|
if not check_requirements():
|
246
159
|
logger.error("Requirements check failed")
|
247
160
|
sys.exit(1)
|
248
|
-
|
161
|
+
|
249
162
|
# Verify provider port is available
|
250
|
-
if not asyncio.run(verify_provider_port(settings.PORT)):
|
163
|
+
if not no_verify_port and not asyncio.run(verify_provider_port(settings.PORT)):
|
251
164
|
logger.error(f"Provider port {settings.PORT} is not available")
|
252
165
|
sys.exit(1)
|
253
|
-
|
166
|
+
|
254
167
|
# Configure uvicorn logging
|
255
168
|
log_config = uvicorn.config.LOGGING_CONFIG
|
256
169
|
log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
257
|
-
|
170
|
+
|
258
171
|
# Run server
|
259
|
-
logger.process(
|
172
|
+
logger.process(
|
173
|
+
f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
|
260
174
|
uvicorn.run(
|
261
175
|
"provider:app",
|
262
176
|
host=settings.HOST,
|
263
177
|
port=settings.PORT,
|
264
178
|
reload=settings.DEBUG,
|
265
|
-
log_level="
|
179
|
+
log_level="debug" if dev_mode else "info",
|
266
180
|
log_config=log_config,
|
267
181
|
timeout_keep_alive=60, # Increase keep-alive timeout
|
268
182
|
limit_concurrency=100, # Limit concurrent connections
|
@@ -270,3 +184,6 @@ def start():
|
|
270
184
|
except Exception as e:
|
271
185
|
logger.error(f"Failed to start provider server: {e}")
|
272
186
|
sys.exit(1)
|
187
|
+
|
188
|
+
if __name__ == "__main__":
|
189
|
+
cli()
|
provider/security/ethereum.py
CHANGED
@@ -13,29 +13,25 @@ class EthereumIdentity:
|
|
13
13
|
self.key_dir = Path(key_dir)
|
14
14
|
self.key_file = self.key_dir / "provider_key.json"
|
15
15
|
|
16
|
-
def get_or_create_identity(self) -> str:
|
17
|
-
"""Get existing provider ID or create new one
|
18
|
-
# Create directory if it doesn't exist
|
16
|
+
def get_or_create_identity(self) -> (str, str):
|
17
|
+
"""Get existing provider ID and private key, or create a new one."""
|
19
18
|
self.key_dir.mkdir(parents=True, exist_ok=True)
|
20
|
-
self.key_dir.chmod(0o700)
|
21
|
-
|
22
|
-
# Check for existing key
|
19
|
+
self.key_dir.chmod(0o700)
|
20
|
+
|
23
21
|
if self.key_file.exists():
|
24
|
-
with open(self.key_file) as f:
|
22
|
+
with open(self.key_file, "r") as f:
|
25
23
|
key_data = json.load(f)
|
26
|
-
return key_data["address"]
|
27
|
-
|
28
|
-
# Generate new key
|
24
|
+
return key_data["address"], key_data["private_key"]
|
25
|
+
|
29
26
|
Account.enable_unaudited_hdwallet_features()
|
30
27
|
acct = Account.create()
|
31
28
|
|
32
|
-
# Save key securely
|
33
29
|
key_data = {
|
34
30
|
"address": acct.address,
|
35
31
|
"private_key": acct.key.hex()
|
36
32
|
}
|
37
33
|
with open(self.key_file, "w") as f:
|
38
34
|
json.dump(key_data, f)
|
39
|
-
self.key_file.chmod(0o600)
|
35
|
+
self.key_file.chmod(0o600)
|
40
36
|
|
41
|
-
return acct.address
|
37
|
+
return acct.address, acct.key.hex()
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import asyncio
|
2
|
+
import hashlib
|
3
|
+
import httpx
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from golem_base_sdk import GolemBaseClient
|
7
|
+
from provider.utils.logging import setup_logger
|
8
|
+
|
9
|
+
logger = setup_logger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class FaucetClient:
|
13
|
+
"""A client for interacting with a Proof of Work-protected faucet."""
|
14
|
+
|
15
|
+
def __init__(self, faucet_url: str, captcha_url: str, captcha_api_key: str):
|
16
|
+
self.faucet_url = faucet_url
|
17
|
+
self.captcha_url = captcha_url
|
18
|
+
self.captcha_api_key = captcha_api_key
|
19
|
+
self.api_endpoint = f"{faucet_url}/api"
|
20
|
+
self.client: Optional[GolemBaseClient] = None
|
21
|
+
|
22
|
+
async def _ensure_client(self):
|
23
|
+
if not self.client:
|
24
|
+
from ..config import settings
|
25
|
+
private_key_hex = settings.ETHEREUM_PRIVATE_KEY.replace("0x", "")
|
26
|
+
private_key_bytes = bytes.fromhex(private_key_hex)
|
27
|
+
self.client = await GolemBaseClient.create_ro_client(
|
28
|
+
rpc_url=settings.GOLEM_BASE_RPC_URL,
|
29
|
+
ws_url=settings.GOLEM_BASE_WS_URL,
|
30
|
+
)
|
31
|
+
|
32
|
+
async def check_balance(self, address: str) -> Optional[float]:
|
33
|
+
"""Check the balance of the given address."""
|
34
|
+
await self._ensure_client()
|
35
|
+
try:
|
36
|
+
balance_wei = await self.client.http_client().eth.get_balance(address)
|
37
|
+
balance_eth = self.client.http_client().from_wei(balance_wei, 'ether')
|
38
|
+
return float(balance_eth)
|
39
|
+
except Exception as e:
|
40
|
+
logger.error(f"Failed to check balance: {e}")
|
41
|
+
return None
|
42
|
+
|
43
|
+
async def get_funds(self, address: str) -> Optional[str]:
|
44
|
+
"""Request funds from the faucet for the given address."""
|
45
|
+
try:
|
46
|
+
balance = await self.check_balance(address)
|
47
|
+
if balance is not None and balance > 0.01:
|
48
|
+
logger.info(f"Sufficient funds ({balance} ETH), skipping faucet request.")
|
49
|
+
return None
|
50
|
+
|
51
|
+
logger.info("Requesting funds from faucet...")
|
52
|
+
challenge_data = await self._get_challenge()
|
53
|
+
if not challenge_data:
|
54
|
+
return None
|
55
|
+
|
56
|
+
challenge_list = challenge_data.get("challenge")
|
57
|
+
token = challenge_data.get("token")
|
58
|
+
|
59
|
+
if not challenge_list or not token:
|
60
|
+
logger.error(f"Invalid challenge data received: {challenge_data}")
|
61
|
+
return None
|
62
|
+
|
63
|
+
solutions = []
|
64
|
+
for salt, target in challenge_list:
|
65
|
+
nonce = self._solve_challenge(salt, target)
|
66
|
+
solutions.append([salt, target, nonce])
|
67
|
+
|
68
|
+
redeemed_token = await self._redeem_solution(token, solutions)
|
69
|
+
if not redeemed_token:
|
70
|
+
return None
|
71
|
+
|
72
|
+
tx_hash = await self._request_faucet(address, redeemed_token)
|
73
|
+
if tx_hash:
|
74
|
+
logger.success(f"Successfully requested funds. Transaction hash: {tx_hash}")
|
75
|
+
return tx_hash
|
76
|
+
except Exception as e:
|
77
|
+
import traceback
|
78
|
+
logger.error(f"Failed to get funds from faucet: {e}")
|
79
|
+
logger.error(traceback.format_exc())
|
80
|
+
return None
|
81
|
+
|
82
|
+
async def _get_challenge(self) -> Optional[dict]:
|
83
|
+
"""Get a PoW challenge from the faucet."""
|
84
|
+
try:
|
85
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
86
|
+
url = f"{self.captcha_url}/{self.captcha_api_key}/api/challenge"
|
87
|
+
response = await client.post(url)
|
88
|
+
response.raise_for_status()
|
89
|
+
return response.json()
|
90
|
+
except httpx.HTTPStatusError as e:
|
91
|
+
logger.error(f"Failed to get PoW challenge: {e.response.text}")
|
92
|
+
return None
|
93
|
+
|
94
|
+
def _solve_challenge(self, salt: str, target: str) -> int:
|
95
|
+
"""Solve the PoW challenge."""
|
96
|
+
target_hash = bytes.fromhex(target)
|
97
|
+
nonce = 0
|
98
|
+
while True:
|
99
|
+
hasher = hashlib.sha256()
|
100
|
+
hasher.update(f"{salt}{nonce}".encode())
|
101
|
+
if hasher.digest().startswith(target_hash):
|
102
|
+
return nonce
|
103
|
+
nonce += 1
|
104
|
+
|
105
|
+
async def _redeem_solution(self, token: str, solutions: list) -> Optional[str]:
|
106
|
+
"""Redeem the PoW solution to get a CAPTCHA token."""
|
107
|
+
try:
|
108
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
109
|
+
url = f"{self.captcha_url}/{self.captcha_api_key}/api/redeem"
|
110
|
+
response = await client.post(
|
111
|
+
url,
|
112
|
+
json={"token": token, "solutions": solutions}
|
113
|
+
)
|
114
|
+
response.raise_for_status()
|
115
|
+
return response.json().get("token")
|
116
|
+
except httpx.HTTPStatusError as e:
|
117
|
+
logger.error(f"Failed to redeem PoW solution: {e.response.text}")
|
118
|
+
return None
|
119
|
+
|
120
|
+
async def _request_faucet(self, address: str, token: str) -> Optional[str]:
|
121
|
+
"""Request funds from the faucet with the CAPTCHA token."""
|
122
|
+
try:
|
123
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
124
|
+
response = await client.post(
|
125
|
+
f"{self.api_endpoint}/faucet",
|
126
|
+
json={"address": address, "captchaToken": token}
|
127
|
+
)
|
128
|
+
response.raise_for_status()
|
129
|
+
return response.json().get("txHash")
|
130
|
+
except httpx.HTTPStatusError as e:
|
131
|
+
logger.error(f"Faucet request failed: {e.response.text}")
|
132
|
+
return None
|
provider/service.py
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
import asyncio
|
2
|
+
from fastapi import FastAPI
|
3
|
+
|
4
|
+
from .utils.logging import setup_logger
|
5
|
+
from .vm.service import VMService
|
6
|
+
from .discovery.service import AdvertisementService
|
7
|
+
|
8
|
+
logger = setup_logger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
class ProviderService:
|
12
|
+
"""Service for managing the provider's lifecycle."""
|
13
|
+
|
14
|
+
def __init__(self, vm_service: VMService, advertisement_service: AdvertisementService, port_manager):
|
15
|
+
self.vm_service = vm_service
|
16
|
+
self.advertisement_service = advertisement_service
|
17
|
+
self.port_manager = port_manager
|
18
|
+
|
19
|
+
async def setup(self, app: FastAPI):
|
20
|
+
"""Setup and initialize the provider components."""
|
21
|
+
from .config import settings
|
22
|
+
from .utils.ascii_art import startup_animation
|
23
|
+
from .security.faucet import FaucetClient
|
24
|
+
|
25
|
+
try:
|
26
|
+
# Display startup animation
|
27
|
+
await startup_animation()
|
28
|
+
|
29
|
+
logger.process("🔄 Initializing provider...")
|
30
|
+
|
31
|
+
# Setup directories
|
32
|
+
self._setup_directories()
|
33
|
+
|
34
|
+
# Initialize services
|
35
|
+
await self.port_manager.initialize()
|
36
|
+
await self.vm_service.provider.initialize()
|
37
|
+
await self.advertisement_service.start()
|
38
|
+
|
39
|
+
# Check wallet balance and request funds if needed
|
40
|
+
faucet_client = FaucetClient(
|
41
|
+
faucet_url=settings.FAUCET_URL,
|
42
|
+
captcha_url=settings.CAPTCHA_URL,
|
43
|
+
captcha_api_key=settings.CAPTCHA_API_KEY,
|
44
|
+
)
|
45
|
+
await faucet_client.get_funds(settings.PROVIDER_ID)
|
46
|
+
|
47
|
+
logger.success("✨ Provider setup complete")
|
48
|
+
except Exception as e:
|
49
|
+
logger.error(f"Startup failed: {e}")
|
50
|
+
await self.cleanup()
|
51
|
+
raise
|
52
|
+
|
53
|
+
async def cleanup(self):
|
54
|
+
"""Cleanup provider components."""
|
55
|
+
logger.process("🔄 Cleaning up provider...")
|
56
|
+
await self.advertisement_service.stop()
|
57
|
+
await self.vm_service.provider.cleanup()
|
58
|
+
logger.success("✨ Provider cleanup complete")
|
59
|
+
|
60
|
+
def _setup_directories(self):
|
61
|
+
"""Create necessary directories for the provider."""
|
62
|
+
from .config import settings
|
63
|
+
from pathlib import Path
|
64
|
+
|
65
|
+
Path(settings.VM_DATA_DIR).mkdir(parents=True, exist_ok=True)
|
66
|
+
Path(settings.SSH_KEY_DIR).mkdir(parents=True, exist_ok=True)
|
67
|
+
Path(settings.CLOUD_INIT_DIR).mkdir(parents=True, exist_ok=True)
|
File without changes
|
provider/utils/logging.py
CHANGED
@@ -39,42 +39,26 @@ def setup_logger(name: Optional[str] = None, debug: bool = False) -> logging.Log
|
|
39
39
|
Configured logger instance
|
40
40
|
"""
|
41
41
|
logger = logging.getLogger(name or __name__)
|
42
|
-
logger.handlers
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
"%(log_color)s[%(asctime)s] %(message)s",
|
42
|
+
if logger.handlers:
|
43
|
+
return logger # Already configured
|
44
|
+
|
45
|
+
handler = colorlog.StreamHandler(sys.stdout)
|
46
|
+
formatter = colorlog.ColoredFormatter(
|
47
|
+
"%(log_color)s[%(asctime)s] %(levelname)s: %(message)s",
|
48
48
|
datefmt="%Y-%m-%d %H:%M:%S",
|
49
|
-
reset=True,
|
50
49
|
log_colors={
|
50
|
+
'DEBUG': 'cyan',
|
51
51
|
'INFO': 'green',
|
52
52
|
'PROCESS': 'yellow',
|
53
53
|
'WARNING': 'yellow',
|
54
54
|
'SUCCESS': 'green,bold',
|
55
55
|
'ERROR': 'red',
|
56
56
|
'CRITICAL': 'red,bold',
|
57
|
-
}
|
58
|
-
secondary_log_colors={},
|
59
|
-
style='%'
|
57
|
+
}
|
60
58
|
)
|
61
|
-
|
62
|
-
|
63
|
-
logger.
|
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)
|
59
|
+
handler.setFormatter(formatter)
|
60
|
+
logger.addHandler(handler)
|
61
|
+
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
78
62
|
|
79
63
|
return logger
|
80
64
|
|