golem-vm-provider 0.1.26__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.26.dist-info → golem_vm_provider-0.1.27.dist-info}/METADATA +7 -2
- golem_vm_provider-0.1.27.dist-info/RECORD +38 -0
- {golem_vm_provider-0.1.26.dist-info → golem_vm_provider-0.1.27.dist-info}/entry_points.txt +1 -0
- provider/api/models.py +7 -7
- provider/api/routes.py +89 -95
- provider/config.py +50 -28
- provider/container.py +84 -0
- provider/discovery/__init__.py +8 -2
- provider/discovery/advertiser.py +41 -63
- provider/discovery/golem_base_advertiser.py +12 -6
- provider/discovery/resource_monitor.py +34 -0
- provider/discovery/resource_tracker.py +1 -1
- provider/discovery/service.py +24 -0
- provider/main.py +56 -155
- provider/service.py +67 -0
- provider/utils/__init__.py +0 -0
- provider/utils/logging.py +11 -27
- provider/utils/port_display.py +6 -8
- provider/utils/retry.py +39 -0
- provider/vm/__init__.py +1 -1
- provider/vm/models.py +8 -7
- provider/vm/multipass.py +2 -420
- provider/vm/multipass_adapter.py +221 -0
- provider/vm/name_mapper.py +5 -5
- provider/vm/port_manager.py +24 -6
- provider/vm/provider.py +48 -0
- provider/vm/proxy_manager.py +1 -1
- provider/vm/service.py +91 -0
- golem_vm_provider-0.1.26.dist-info/RECORD +0 -30
- {golem_vm_provider-0.1.26.dist-info → golem_vm_provider-0.1.27.dist-info}/WHEEL +0 -0
provider/discovery/advertiser.py
CHANGED
@@ -1,90 +1,71 @@
|
|
1
1
|
import aiohttp
|
2
2
|
import asyncio
|
3
3
|
import logging
|
4
|
-
import
|
5
|
-
from
|
6
|
-
from typing import Dict, Optional
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from typing import Optional
|
7
6
|
|
8
7
|
from ..config import settings
|
9
8
|
from ..utils.retry import async_retry
|
9
|
+
from .resource_tracker import ResourceTracker
|
10
10
|
|
11
11
|
logger = logging.getLogger(__name__)
|
12
12
|
|
13
|
-
class
|
14
|
-
"""
|
15
|
-
|
16
|
-
@
|
17
|
-
def
|
18
|
-
"""
|
19
|
-
|
20
|
-
|
21
|
-
@
|
22
|
-
def
|
23
|
-
"""
|
24
|
-
|
25
|
-
|
26
|
-
@
|
27
|
-
def
|
28
|
-
"""
|
29
|
-
|
30
|
-
|
31
|
-
@
|
32
|
-
def
|
33
|
-
"""
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
"""Get memory usage percentage."""
|
39
|
-
return psutil.virtual_memory().percent
|
40
|
-
|
41
|
-
@staticmethod
|
42
|
-
def get_storage_percent() -> float:
|
43
|
-
"""Get storage usage percentage."""
|
44
|
-
return psutil.disk_usage("/").percent
|
45
|
-
|
46
|
-
class ResourceAdvertiser:
|
47
|
-
"""Advertise available resources to discovery service."""
|
13
|
+
class Advertiser(ABC):
|
14
|
+
"""Abstract base class for advertisers."""
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
async def initialize(self):
|
18
|
+
"""Initialize the advertiser."""
|
19
|
+
pass
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
async def start_loop(self):
|
23
|
+
"""Start the advertising loop."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
async def stop(self):
|
28
|
+
"""Stop the advertising loop."""
|
29
|
+
pass
|
30
|
+
|
31
|
+
@abstractmethod
|
32
|
+
async def post_advertisement(self):
|
33
|
+
"""Post a single advertisement."""
|
34
|
+
pass
|
35
|
+
|
36
|
+
class DiscoveryServerAdvertiser(Advertiser):
|
37
|
+
"""Advertise available resources to a discovery service."""
|
48
38
|
|
49
39
|
def __init__(
|
50
40
|
self,
|
51
41
|
resource_tracker: 'ResourceTracker',
|
52
42
|
discovery_url: Optional[str] = None,
|
53
43
|
provider_id: Optional[str] = None,
|
54
|
-
update_interval: Optional[int] = None
|
55
44
|
):
|
56
45
|
self.resource_tracker = resource_tracker
|
57
46
|
self.discovery_url = discovery_url or settings.DISCOVERY_URL
|
58
47
|
self.provider_id = provider_id or settings.PROVIDER_ID
|
59
|
-
self.update_interval = update_interval or settings.ADVERTISEMENT_INTERVAL
|
60
48
|
self.session: Optional[aiohttp.ClientSession] = None
|
61
49
|
self._stop_event = asyncio.Event()
|
62
50
|
|
63
|
-
async def
|
64
|
-
"""
|
51
|
+
async def initialize(self):
|
52
|
+
"""Initialize the advertiser."""
|
65
53
|
self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# Test discovery service connection with retries
|
54
|
+
self.resource_tracker.on_update(
|
55
|
+
lambda: asyncio.create_task(self.post_advertisement())
|
56
|
+
)
|
70
57
|
try:
|
71
58
|
await self._check_discovery_health()
|
72
59
|
except Exception as e:
|
73
60
|
logger.warning(f"Could not connect to discovery service after retries, continuing without advertising: {e}")
|
74
61
|
return
|
75
|
-
|
62
|
+
|
63
|
+
async def start_loop(self):
|
64
|
+
"""Start advertising resources in a loop."""
|
76
65
|
try:
|
77
66
|
while not self._stop_event.is_set():
|
78
|
-
|
79
|
-
|
80
|
-
except aiohttp.ClientError as e:
|
81
|
-
logger.error(f"Network error posting advertisement: {e}")
|
82
|
-
await asyncio.sleep(min(60, self.update_interval))
|
83
|
-
except Exception as e:
|
84
|
-
logger.error(f"Failed to post advertisement: {e}")
|
85
|
-
await asyncio.sleep(min(60, self.update_interval))
|
86
|
-
else:
|
87
|
-
await asyncio.sleep(self.update_interval)
|
67
|
+
await self.post_advertisement()
|
68
|
+
await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
|
88
69
|
finally:
|
89
70
|
await self.stop()
|
90
71
|
|
@@ -106,19 +87,17 @@ class ResourceAdvertiser:
|
|
106
87
|
raise Exception(f"Discovery service health check failed: {response.status}")
|
107
88
|
|
108
89
|
@async_retry(retries=3, delay=1.0, backoff=2.0, exceptions=(aiohttp.ClientError, asyncio.TimeoutError))
|
109
|
-
async def
|
90
|
+
async def post_advertisement(self):
|
110
91
|
"""Post resource advertisement to discovery service."""
|
111
92
|
if not self.session:
|
112
93
|
raise RuntimeError("Session not initialized")
|
113
94
|
|
114
95
|
resources = self.resource_tracker.get_available_resources()
|
115
96
|
|
116
|
-
# Don't advertise if resources are too low
|
117
97
|
if not self.resource_tracker._meets_minimum_requirements(resources):
|
118
98
|
logger.warning("Resources too low, skipping advertisement")
|
119
99
|
return
|
120
100
|
|
121
|
-
# Get public IP with retries
|
122
101
|
try:
|
123
102
|
ip_address = await self._get_public_ip()
|
124
103
|
except Exception as e:
|
@@ -130,7 +109,7 @@ class ResourceAdvertiser:
|
|
130
109
|
f"{self.discovery_url}/api/v1/advertisements",
|
131
110
|
headers={
|
132
111
|
"X-Provider-ID": self.provider_id,
|
133
|
-
"X-Provider-Signature": "signature",
|
112
|
+
"X-Provider-Signature": "signature",
|
134
113
|
"Content-Type": "application/json"
|
135
114
|
},
|
136
115
|
json={
|
@@ -138,7 +117,7 @@ class ResourceAdvertiser:
|
|
138
117
|
"country": settings.PROVIDER_COUNTRY,
|
139
118
|
"resources": resources
|
140
119
|
},
|
141
|
-
timeout=aiohttp.ClientTimeout(total=5)
|
120
|
+
timeout=aiohttp.ClientTimeout(total=5)
|
142
121
|
) as response:
|
143
122
|
if not response.ok:
|
144
123
|
error_text = await response.text()
|
@@ -159,7 +138,6 @@ class ResourceAdvertiser:
|
|
159
138
|
if not self.session:
|
160
139
|
raise RuntimeError("Session not initialized")
|
161
140
|
|
162
|
-
# Try multiple IP services in case one fails
|
163
141
|
services = [
|
164
142
|
"https://api.ipify.org",
|
165
143
|
"https://ifconfig.me/ip",
|
@@ -2,6 +2,7 @@ import asyncio
|
|
2
2
|
from typing import Optional
|
3
3
|
|
4
4
|
from golem_base_sdk import GolemBaseClient, GolemBaseCreate, GolemBaseUpdate, GolemBaseDelete, Annotation
|
5
|
+
from .advertiser import Advertiser
|
5
6
|
from .golem_base_utils import get_provider_entity_keys
|
6
7
|
from ..config import settings
|
7
8
|
from ..utils.logging import setup_logger
|
@@ -9,7 +10,7 @@ from ..utils.logging import setup_logger
|
|
9
10
|
logger = setup_logger(__name__)
|
10
11
|
|
11
12
|
|
12
|
-
class GolemBaseAdvertiser:
|
13
|
+
class GolemBaseAdvertiser(Advertiser):
|
13
14
|
"""Advertise available resources to the Golem Base network."""
|
14
15
|
|
15
16
|
def __init__(self, resource_tracker: "ResourceTracker"):
|
@@ -89,11 +90,16 @@ class GolemBaseAdvertiser:
|
|
89
90
|
current_annotations = {ann.key: ann.value for ann in metadata.numeric_annotations}
|
90
91
|
current_annotations.update({ann.key: ann.value for ann in metadata.string_annotations})
|
91
92
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
93
|
+
# Full comparison of all annotations
|
94
|
+
expected_annotations = {ann.key: ann.value for ann in string_annotations}
|
95
|
+
expected_annotations.update({ann.key: ann.value for ann in numeric_annotations})
|
96
|
+
|
97
|
+
# Debugging logs to compare annotations
|
98
|
+
logger.info(f"IP address from settings: {ip_address}")
|
99
|
+
logger.info(f"Current on-chain annotations: {current_annotations}")
|
100
|
+
logger.info(f"Expected annotations based on current config: {expected_annotations}")
|
101
|
+
|
102
|
+
if sorted(current_annotations.items()) == sorted(expected_annotations.items()):
|
97
103
|
logger.info("Advertisement is up-to-date. Waiting for expiration.")
|
98
104
|
else:
|
99
105
|
logger.info("Advertisement is outdated. Updating.")
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import psutil
|
2
|
+
|
3
|
+
class ResourceMonitor:
|
4
|
+
"""Monitor system resources."""
|
5
|
+
|
6
|
+
@staticmethod
|
7
|
+
def get_cpu_count() -> int:
|
8
|
+
"""Get number of CPU cores."""
|
9
|
+
return psutil.cpu_count()
|
10
|
+
|
11
|
+
@staticmethod
|
12
|
+
def get_memory_gb() -> int:
|
13
|
+
"""Get available memory in GB."""
|
14
|
+
return psutil.virtual_memory().available // (1024 ** 3)
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def get_storage_gb() -> int:
|
18
|
+
"""Get available storage in GB."""
|
19
|
+
return psutil.disk_usage("/").free // (1024 ** 3)
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def get_cpu_percent() -> float:
|
23
|
+
"""Get CPU usage percentage."""
|
24
|
+
return psutil.cpu_percent(interval=1)
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def get_memory_percent() -> float:
|
28
|
+
"""Get memory usage percentage."""
|
29
|
+
return psutil.virtual_memory().percent
|
30
|
+
|
31
|
+
@staticmethod
|
32
|
+
def get_storage_percent() -> float:
|
33
|
+
"""Get storage usage percentage."""
|
34
|
+
return psutil.disk_usage("/").percent
|
@@ -11,7 +11,7 @@ class ResourceTracker:
|
|
11
11
|
|
12
12
|
def __init__(self):
|
13
13
|
"""Initialize resource tracker."""
|
14
|
-
from .
|
14
|
+
from .resource_monitor import ResourceMonitor
|
15
15
|
self.total_resources = {
|
16
16
|
"cpu": ResourceMonitor.get_cpu_count(),
|
17
17
|
"memory": ResourceMonitor.get_memory_gb(),
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from .advertiser import Advertiser
|
5
|
+
from ..config import settings
|
6
|
+
|
7
|
+
class AdvertisementService:
|
8
|
+
"""Service for managing the advertisement lifecycle."""
|
9
|
+
|
10
|
+
def __init__(self, advertiser: Advertiser):
|
11
|
+
self.advertiser = advertiser
|
12
|
+
self._task: Optional[asyncio.Task] = None
|
13
|
+
|
14
|
+
async def start(self):
|
15
|
+
"""Initialize and start the advertiser."""
|
16
|
+
await self.advertiser.initialize()
|
17
|
+
self._task = asyncio.create_task(self.advertiser.start_loop())
|
18
|
+
|
19
|
+
async def stop(self):
|
20
|
+
"""Stop the advertiser."""
|
21
|
+
if self._task:
|
22
|
+
self._task.cancel()
|
23
|
+
await self._task
|
24
|
+
await self.advertiser.stop()
|
provider/main.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from .api import routes
|
2
2
|
import asyncio
|
3
3
|
import os
|
4
|
+
import socket
|
4
5
|
from fastapi import FastAPI
|
5
6
|
from fastapi.middleware.cors import CORSMiddleware
|
6
7
|
from typing import Optional
|
@@ -9,15 +10,35 @@ from .config import settings
|
|
9
10
|
from .utils.logging import setup_logger, PROCESS, SUCCESS
|
10
11
|
from .utils.ascii_art import startup_animation
|
11
12
|
from .discovery.resource_tracker import ResourceTracker
|
12
|
-
from .discovery.advertiser import
|
13
|
-
from .
|
14
|
-
from .
|
15
|
-
from .
|
16
|
-
from .security.faucet import FaucetClient
|
17
|
-
|
18
|
-
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
|
19
17
|
|
20
18
|
app = FastAPI(title="VM on Golem Provider")
|
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
|
+
|
21
42
|
# Add CORS middleware
|
22
43
|
app.add_middleware(
|
23
44
|
CORSMiddleware,
|
@@ -27,156 +48,18 @@ app.add_middleware(
|
|
27
48
|
allow_headers=["*"], # Allows all headers
|
28
49
|
)
|
29
50
|
|
30
|
-
|
31
|
-
async def setup_provider() -> None:
|
32
|
-
"""Setup and initialize the provider components."""
|
33
|
-
try:
|
34
|
-
# Create resource tracker first
|
35
|
-
logger.process("🔄 Initializing resource tracker...")
|
36
|
-
resource_tracker = ResourceTracker()
|
37
|
-
app.state.resource_tracker = resource_tracker
|
38
|
-
|
39
|
-
# Create provider with resource tracker and temporary port manager
|
40
|
-
logger.process("🔄 Initializing VM provider...")
|
41
|
-
provider = MultipassProvider(
|
42
|
-
resource_tracker, port_manager=None) # Will be set later
|
43
|
-
|
44
|
-
try:
|
45
|
-
# Initialize provider (without port operations)
|
46
|
-
await asyncio.wait_for(provider.initialize(), timeout=30)
|
47
|
-
|
48
|
-
# Store provider reference
|
49
|
-
app.state.provider = provider
|
50
|
-
app.state.proxy_manager = provider.proxy_manager
|
51
|
-
|
52
|
-
# Initialize port manager first to verify all ports
|
53
|
-
logger.process("🔄 Initializing port manager...")
|
54
|
-
port_manager = PortManager(
|
55
|
-
start_port=settings.PORT_RANGE_START,
|
56
|
-
end_port=settings.PORT_RANGE_END,
|
57
|
-
discovery_port=settings.PORT,
|
58
|
-
skip_verification=settings.SKIP_PORT_VERIFICATION
|
59
|
-
)
|
60
|
-
|
61
|
-
if not await port_manager.initialize():
|
62
|
-
raise RuntimeError("Port verification failed")
|
63
|
-
|
64
|
-
# Store port manager references
|
65
|
-
app.state.port_manager = port_manager
|
66
|
-
provider.port_manager = port_manager
|
67
|
-
app.state.proxy_manager.port_manager = port_manager
|
68
|
-
|
69
|
-
# Now restore proxy configurations using only verified ports
|
70
|
-
logger.process("🔄 Restoring proxy configurations...")
|
71
|
-
await app.state.proxy_manager._load_state()
|
72
|
-
|
73
|
-
except asyncio.TimeoutError:
|
74
|
-
logger.error("Provider initialization timed out")
|
75
|
-
raise
|
76
|
-
except Exception as e:
|
77
|
-
logger.error(f"Failed to initialize provider: {e}")
|
78
|
-
raise
|
79
|
-
|
80
|
-
# Create advertiser
|
81
|
-
logger.process("🔄 Initializing resource advertiser...")
|
82
|
-
if settings.DISCOVERY_DRIVER == "golem-base":
|
83
|
-
advertiser = GolemBaseAdvertiser(
|
84
|
-
resource_tracker=resource_tracker
|
85
|
-
)
|
86
|
-
await advertiser.initialize()
|
87
|
-
else:
|
88
|
-
advertiser = ResourceAdvertiser(
|
89
|
-
resource_tracker=resource_tracker,
|
90
|
-
discovery_url=settings.DISCOVERY_URL,
|
91
|
-
provider_id=settings.PROVIDER_ID
|
92
|
-
)
|
93
|
-
app.state.advertiser = advertiser
|
94
|
-
|
95
|
-
logger.success(
|
96
|
-
"✨ Provider setup complete and ready to accept requests")
|
97
|
-
except Exception as e:
|
98
|
-
logger.error(f"Failed to setup provider: {e}")
|
99
|
-
# Attempt cleanup of any initialized components
|
100
|
-
await cleanup_provider()
|
101
|
-
raise
|
102
|
-
|
103
|
-
|
104
|
-
async def cleanup_provider() -> None:
|
105
|
-
"""Cleanup provider components."""
|
106
|
-
cleanup_errors = []
|
107
|
-
|
108
|
-
# Stop advertiser
|
109
|
-
if hasattr(app.state, "advertiser"):
|
110
|
-
try:
|
111
|
-
await app.state.advertiser.stop()
|
112
|
-
if hasattr(app.state, "advertiser_task"):
|
113
|
-
app.state.advertiser_task.cancel()
|
114
|
-
try:
|
115
|
-
await app.state.advertiser_task
|
116
|
-
except asyncio.CancelledError:
|
117
|
-
pass
|
118
|
-
except Exception as e:
|
119
|
-
cleanup_errors.append(f"Failed to stop advertiser: {e}")
|
120
|
-
|
121
|
-
# Cleanup proxy manager first to stop all proxy servers
|
122
|
-
if hasattr(app.state, "proxy_manager"):
|
123
|
-
try:
|
124
|
-
await asyncio.wait_for(app.state.proxy_manager.cleanup(), timeout=30)
|
125
|
-
except asyncio.TimeoutError:
|
126
|
-
cleanup_errors.append("Proxy manager cleanup timed out")
|
127
|
-
except Exception as e:
|
128
|
-
cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
|
129
|
-
|
130
|
-
# Cleanup provider
|
131
|
-
if hasattr(app.state, "provider"):
|
132
|
-
try:
|
133
|
-
await asyncio.wait_for(app.state.provider.cleanup(), timeout=30)
|
134
|
-
except asyncio.TimeoutError:
|
135
|
-
cleanup_errors.append("Provider cleanup timed out")
|
136
|
-
except Exception as e:
|
137
|
-
cleanup_errors.append(f"Failed to cleanup provider: {e}")
|
138
|
-
|
139
|
-
if cleanup_errors:
|
140
|
-
error_msg = "\n".join(cleanup_errors)
|
141
|
-
logger.error(f"Errors during cleanup:\n{error_msg}")
|
142
|
-
else:
|
143
|
-
logger.success("✨ Provider cleanup complete")
|
144
|
-
|
145
|
-
|
146
51
|
@app.on_event("startup")
|
147
52
|
async def startup_event():
|
148
53
|
"""Handle application startup."""
|
149
|
-
|
150
|
-
|
151
|
-
await startup_animation()
|
152
|
-
|
153
|
-
# Initialize provider
|
154
|
-
await setup_provider()
|
155
|
-
|
156
|
-
# Check wallet balance and request funds if needed
|
157
|
-
faucet_client = FaucetClient(
|
158
|
-
faucet_url=settings.FAUCET_URL,
|
159
|
-
captcha_url=settings.CAPTCHA_URL,
|
160
|
-
captcha_api_key=settings.CAPTCHA_API_KEY,
|
161
|
-
)
|
162
|
-
await faucet_client.get_funds(settings.PROVIDER_ID)
|
163
|
-
|
164
|
-
# Post initial advertisement and start advertising loop
|
165
|
-
if isinstance(app.state.advertiser, GolemBaseAdvertiser):
|
166
|
-
await app.state.advertiser.post_advertisement()
|
167
|
-
app.state.advertiser_task = asyncio.create_task(app.state.advertiser.start_loop())
|
168
|
-
|
169
|
-
except Exception as e:
|
170
|
-
logger.error(f"Startup failed: {e}")
|
171
|
-
# Ensure proper cleanup
|
172
|
-
await cleanup_provider()
|
173
|
-
raise
|
54
|
+
provider_service = container.provider_service()
|
55
|
+
await provider_service.setup(app)
|
174
56
|
|
175
57
|
|
176
58
|
@app.on_event("shutdown")
|
177
59
|
async def shutdown_event():
|
178
60
|
"""Handle application shutdown."""
|
179
|
-
|
61
|
+
provider_service = container.provider_service()
|
62
|
+
await provider_service.cleanup()
|
180
63
|
|
181
64
|
# Import routes after app creation to avoid circular imports
|
182
65
|
app.include_router(routes.router, prefix="/api/v1")
|
@@ -225,6 +108,9 @@ async def verify_provider_port(port: int) -> bool:
|
|
225
108
|
return False
|
226
109
|
|
227
110
|
|
111
|
+
# The get_local_ip function has been removed as this logic is now handled in config.py
|
112
|
+
|
113
|
+
|
228
114
|
import typer
|
229
115
|
|
230
116
|
cli = typer.Typer()
|
@@ -232,21 +118,36 @@ cli = typer.Typer()
|
|
232
118
|
@cli.command()
|
233
119
|
def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
|
234
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."""
|
235
130
|
import sys
|
236
131
|
from pathlib import Path
|
237
132
|
from dotenv import load_dotenv
|
238
133
|
import uvicorn
|
239
134
|
from .utils.logging import setup_logger
|
135
|
+
|
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)
|
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
|
240
145
|
from .config import settings
|
241
146
|
|
242
147
|
# Configure logging with debug mode
|
243
|
-
logger = setup_logger(__name__, debug=
|
148
|
+
logger = setup_logger(__name__, debug=dev_mode)
|
244
149
|
|
245
150
|
try:
|
246
|
-
# Load environment variables from .env file
|
247
|
-
env_path = Path(__file__).parent.parent / '.env'
|
248
|
-
load_dotenv(dotenv_path=env_path)
|
249
|
-
|
250
151
|
# Log environment variables
|
251
152
|
logger.info("Environment variables:")
|
252
153
|
for key, value in os.environ.items():
|
@@ -275,7 +176,7 @@ def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="S
|
|
275
176
|
host=settings.HOST,
|
276
177
|
port=settings.PORT,
|
277
178
|
reload=settings.DEBUG,
|
278
|
-
log_level="
|
179
|
+
log_level="debug" if dev_mode else "info",
|
279
180
|
log_config=log_config,
|
280
181
|
timeout_keep_alive=60, # Increase keep-alive timeout
|
281
182
|
limit_concurrency=100, # Limit concurrent connections
|
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
|
|