golem-vm-provider 0.1.37__tar.gz → 0.1.39__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/PKG-INFO +44 -2
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/README.md +43 -1
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/api/models.py +10 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/api/routes.py +22 -1
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/config.py +70 -1
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/container.py +38 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/advertiser.py +28 -5
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/golem_base_advertiser.py +8 -1
- golem_vm_provider-0.1.39/provider/discovery/multi_advertiser.py +28 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/service.py +8 -1
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/main.py +131 -0
- golem_vm_provider-0.1.39/provider/payments/blockchain_service.py +135 -0
- golem_vm_provider-0.1.39/provider/payments/monitor.py +64 -0
- golem_vm_provider-0.1.39/provider/payments/stream_map.py +40 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/service.py +19 -0
- golem_vm_provider-0.1.39/provider/utils/pricing.py +171 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/utils/retry.py +7 -1
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/multipass_adapter.py +19 -7
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/service.py +19 -2
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/pyproject.toml +1 -1
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/__init__.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/api/__init__.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/__init__.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/golem_base_utils.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/resource_monitor.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/resource_tracker.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/network/port_verifier.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/security/ethereum.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/security/faucet.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/utils/__init__.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/utils/ascii_art.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/utils/logging.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/utils/port_display.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/utils/setup.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/__init__.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/cloud_init.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/models.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/multipass.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/name_mapper.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/port_manager.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/provider.py +0 -0
- {golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/vm/proxy_manager.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: golem-vm-provider
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.39
|
4
4
|
Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
|
5
5
|
Keywords: golem,vm,provider,cloud,decentralized
|
6
6
|
Author: Phillip Jensen
|
@@ -230,6 +230,29 @@ GOLEM_PROVIDER_DISCOVERY_URL="http://discovery.golem.network:9001"
|
|
230
230
|
GOLEM_PROVIDER_ADVERTISEMENT_INTERVAL=240
|
231
231
|
```
|
232
232
|
|
233
|
+
### Streaming Payments (Polygon GLM)
|
234
|
+
|
235
|
+
Enable on‑chain stream‑gated rentals by configuring the following (env prefix `GOLEM_PROVIDER_`):
|
236
|
+
|
237
|
+
- `POLYGON_RPC_URL` — Polygon PoS RPC URL (e.g., https://polygon-rpc.com)
|
238
|
+
- `STREAM_PAYMENT_ADDRESS` — StreamPayment contract address; if non‑zero, VM creation requires a valid `stream_id`
|
239
|
+
- `GLM_TOKEN_ADDRESS` — GLM ERC20 address (for info endpoint)
|
240
|
+
|
241
|
+
Optional background automation (all disabled by default):
|
242
|
+
|
243
|
+
- `STREAM_MIN_REMAINING_SECONDS` — minimum remaining runway to keep a VM running (default 3600)
|
244
|
+
- `STREAM_MONITOR_ENABLED` — stop VMs when remaining runway < threshold (default false)
|
245
|
+
- `STREAM_MONITOR_INTERVAL_SECONDS` — how frequently to check runway (default 60)
|
246
|
+
- `STREAM_WITHDRAW_ENABLED` — periodically withdraw vested funds (default false)
|
247
|
+
- `STREAM_WITHDRAW_INTERVAL_SECONDS` — how often to attempt withdrawals (default 1800)
|
248
|
+
- `STREAM_MIN_WITHDRAW_WEI` — only withdraw when >= this amount (gas‑aware)
|
249
|
+
|
250
|
+
When enabled, the provider verifies each VM creation request’s `stream_id` and refuses to start the VM if:
|
251
|
+
|
252
|
+
- stream recipient != provider’s Ethereum address
|
253
|
+
- deposit is zero, stream not started, or stream halted
|
254
|
+
- (Optional) remaining runway < `STREAM_MIN_REMAINING_SECONDS`
|
255
|
+
|
233
256
|
## API Reference
|
234
257
|
|
235
258
|
### Create VM
|
@@ -245,7 +268,8 @@ Request:
|
|
245
268
|
"name": "my-webserver",
|
246
269
|
"cpu_cores": 2,
|
247
270
|
"memory_gb": 4,
|
248
|
-
"storage_gb": 20
|
271
|
+
"storage_gb": 20,
|
272
|
+
"stream_id": 123 // required when STREAM_PAYMENT_ADDRESS is set
|
249
273
|
}
|
250
274
|
```
|
251
275
|
|
@@ -273,6 +297,24 @@ Response:
|
|
273
297
|
- Delete VM: `DELETE /api/v1/vms/{vm_id}`
|
274
298
|
- Stop VM: `POST /api/v1/vms/{vm_id}/stop`
|
275
299
|
- Get Access Info: `GET /api/v1/vms/{vm_id}/access`
|
300
|
+
|
301
|
+
### Provider Info
|
302
|
+
|
303
|
+
```bash
|
304
|
+
GET /api/v1/provider/info
|
305
|
+
```
|
306
|
+
|
307
|
+
Response:
|
308
|
+
|
309
|
+
```json
|
310
|
+
{
|
311
|
+
"provider_id": "0xProviderEthereumAddress",
|
312
|
+
"stream_payment_address": "0xStreamPayment",
|
313
|
+
"glm_token_address": "0xGLM"
|
314
|
+
}
|
315
|
+
```
|
316
|
+
|
317
|
+
Use this endpoint to discover the correct recipient for creating a GLM stream.
|
276
318
|
|
277
319
|
## Operations
|
278
320
|
|
@@ -187,6 +187,29 @@ GOLEM_PROVIDER_DISCOVERY_URL="http://discovery.golem.network:9001"
|
|
187
187
|
GOLEM_PROVIDER_ADVERTISEMENT_INTERVAL=240
|
188
188
|
```
|
189
189
|
|
190
|
+
### Streaming Payments (Polygon GLM)
|
191
|
+
|
192
|
+
Enable on‑chain stream‑gated rentals by configuring the following (env prefix `GOLEM_PROVIDER_`):
|
193
|
+
|
194
|
+
- `POLYGON_RPC_URL` — Polygon PoS RPC URL (e.g., https://polygon-rpc.com)
|
195
|
+
- `STREAM_PAYMENT_ADDRESS` — StreamPayment contract address; if non‑zero, VM creation requires a valid `stream_id`
|
196
|
+
- `GLM_TOKEN_ADDRESS` — GLM ERC20 address (for info endpoint)
|
197
|
+
|
198
|
+
Optional background automation (all disabled by default):
|
199
|
+
|
200
|
+
- `STREAM_MIN_REMAINING_SECONDS` — minimum remaining runway to keep a VM running (default 3600)
|
201
|
+
- `STREAM_MONITOR_ENABLED` — stop VMs when remaining runway < threshold (default false)
|
202
|
+
- `STREAM_MONITOR_INTERVAL_SECONDS` — how frequently to check runway (default 60)
|
203
|
+
- `STREAM_WITHDRAW_ENABLED` — periodically withdraw vested funds (default false)
|
204
|
+
- `STREAM_WITHDRAW_INTERVAL_SECONDS` — how often to attempt withdrawals (default 1800)
|
205
|
+
- `STREAM_MIN_WITHDRAW_WEI` — only withdraw when >= this amount (gas‑aware)
|
206
|
+
|
207
|
+
When enabled, the provider verifies each VM creation request’s `stream_id` and refuses to start the VM if:
|
208
|
+
|
209
|
+
- stream recipient != provider’s Ethereum address
|
210
|
+
- deposit is zero, stream not started, or stream halted
|
211
|
+
- (Optional) remaining runway < `STREAM_MIN_REMAINING_SECONDS`
|
212
|
+
|
190
213
|
## API Reference
|
191
214
|
|
192
215
|
### Create VM
|
@@ -202,7 +225,8 @@ Request:
|
|
202
225
|
"name": "my-webserver",
|
203
226
|
"cpu_cores": 2,
|
204
227
|
"memory_gb": 4,
|
205
|
-
"storage_gb": 20
|
228
|
+
"storage_gb": 20,
|
229
|
+
"stream_id": 123 // required when STREAM_PAYMENT_ADDRESS is set
|
206
230
|
}
|
207
231
|
```
|
208
232
|
|
@@ -230,6 +254,24 @@ Response:
|
|
230
254
|
- Delete VM: `DELETE /api/v1/vms/{vm_id}`
|
231
255
|
- Stop VM: `POST /api/v1/vms/{vm_id}/stop`
|
232
256
|
- Get Access Info: `GET /api/v1/vms/{vm_id}/access`
|
257
|
+
|
258
|
+
### Provider Info
|
259
|
+
|
260
|
+
```bash
|
261
|
+
GET /api/v1/provider/info
|
262
|
+
```
|
263
|
+
|
264
|
+
Response:
|
265
|
+
|
266
|
+
```json
|
267
|
+
{
|
268
|
+
"provider_id": "0xProviderEthereumAddress",
|
269
|
+
"stream_payment_address": "0xStreamPayment",
|
270
|
+
"glm_token_address": "0xGLM"
|
271
|
+
}
|
272
|
+
```
|
273
|
+
|
274
|
+
Use this endpoint to discover the correct recipient for creating a GLM stream.
|
233
275
|
|
234
276
|
## Operations
|
235
277
|
|
@@ -17,6 +17,10 @@ class CreateVMRequest(BaseModel):
|
|
17
17
|
image: str = Field(default="24.04") # Ubuntu 24.04 LTS
|
18
18
|
ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
|
19
19
|
description="SSH public key for VM access")
|
20
|
+
stream_id: Optional[int] = Field(
|
21
|
+
default=None,
|
22
|
+
description="On-chain StreamPayment stream id used to fund this VM"
|
23
|
+
)
|
20
24
|
|
21
25
|
@field_validator("name")
|
22
26
|
def validate_name(cls, v: str) -> str:
|
@@ -106,3 +110,9 @@ class ProviderStatusResponse(BaseModel):
|
|
106
110
|
resources: Dict[str, int]
|
107
111
|
vm_count: int
|
108
112
|
max_vms: int
|
113
|
+
|
114
|
+
|
115
|
+
class ProviderInfoResponse(BaseModel):
|
116
|
+
provider_id: str
|
117
|
+
stream_payment_address: str
|
118
|
+
glm_token_address: str
|
@@ -11,7 +11,8 @@ from ..container import Container
|
|
11
11
|
from ..utils.logging import setup_logger
|
12
12
|
from ..utils.ascii_art import vm_creation_animation, vm_status_change
|
13
13
|
from ..vm.models import VMInfo, VMAccessInfo, VMConfig, VMResources, VMNotFoundError
|
14
|
-
from .models import CreateVMRequest
|
14
|
+
from .models import CreateVMRequest, ProviderInfoResponse
|
15
|
+
from ..payments.blockchain_service import StreamPaymentReader
|
15
16
|
from ..vm.service import VMService
|
16
17
|
from ..vm.multipass_adapter import MultipassError
|
17
18
|
|
@@ -31,6 +32,15 @@ async def create_vm(
|
|
31
32
|
logger.info(f"📥 Received VM creation request for '{request.name}'")
|
32
33
|
|
33
34
|
resources = request.resources or VMResources()
|
35
|
+
|
36
|
+
# If payments are enabled, require a valid stream before starting
|
37
|
+
if settings["STREAM_PAYMENT_ADDRESS"] and settings["STREAM_PAYMENT_ADDRESS"] != "0x0000000000000000000000000000000000000000":
|
38
|
+
if request.stream_id is None:
|
39
|
+
raise HTTPException(status_code=400, detail="stream_id required when payments are enabled")
|
40
|
+
reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
|
41
|
+
ok, reason = reader.verify_stream(int(request.stream_id), settings["PROVIDER_ID"])
|
42
|
+
if not ok:
|
43
|
+
raise HTTPException(status_code=400, detail=f"invalid stream: {reason}")
|
34
44
|
|
35
45
|
# Create VM config
|
36
46
|
config = VMConfig(
|
@@ -46,6 +56,9 @@ async def create_vm(
|
|
46
56
|
except MultipassError as e:
|
47
57
|
logger.error(f"Failed to create VM: {e}")
|
48
58
|
raise HTTPException(status_code=500, detail=str(e))
|
59
|
+
except HTTPException:
|
60
|
+
# Propagate explicit HTTP errors (e.g., payment gating)
|
61
|
+
raise
|
49
62
|
except Exception as e:
|
50
63
|
logger.error(f"An unexpected error occurred: {e}")
|
51
64
|
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
@@ -168,3 +181,11 @@ async def delete_vm(
|
|
168
181
|
except Exception as e:
|
169
182
|
logger.error(f"An unexpected error occurred: {e}")
|
170
183
|
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
184
|
+
@router.get("/provider/info", response_model=ProviderInfoResponse)
|
185
|
+
@inject
|
186
|
+
async def provider_info(settings: Settings = Depends(Provide[Container.config])) -> ProviderInfoResponse:
|
187
|
+
return ProviderInfoResponse(
|
188
|
+
provider_id=settings["PROVIDER_ID"],
|
189
|
+
stream_payment_address=settings["STREAM_PAYMENT_ADDRESS"],
|
190
|
+
glm_token_address=settings["GLM_TOKEN_ADDRESS"],
|
191
|
+
)
|
@@ -39,7 +39,8 @@ def ensure_config() -> None:
|
|
39
39
|
print("Using default settings – run with --help to customize")
|
40
40
|
|
41
41
|
|
42
|
-
|
42
|
+
if not os.environ.get("GOLEM_PROVIDER_SKIP_BOOTSTRAP") and not os.environ.get("PYTEST_CURRENT_TEST"):
|
43
|
+
ensure_config()
|
43
44
|
|
44
45
|
|
45
46
|
class Settings(BaseSettings):
|
@@ -125,12 +126,53 @@ class Settings(BaseSettings):
|
|
125
126
|
# Discovery Service Settings
|
126
127
|
DISCOVERY_URL: str = "http://195.201.39.101:9001"
|
127
128
|
ADVERTISER_TYPE: str = "golem_base" # or "discovery_server"
|
129
|
+
# Deprecated: use platform-specific intervals below
|
128
130
|
ADVERTISEMENT_INTERVAL: int = 240 # seconds
|
131
|
+
DISCOVERY_ADVERTISEMENT_INTERVAL: int = 240 # seconds
|
132
|
+
GOLEM_BASE_ADVERTISEMENT_INTERVAL: int = 3600 # seconds (on-chain cost, keep higher)
|
129
133
|
|
130
134
|
# Golem Base Settings
|
131
135
|
GOLEM_BASE_RPC_URL: str = "https://ethwarsaw.holesky.golemdb.io/rpc"
|
132
136
|
GOLEM_BASE_WS_URL: str = "wss://ethwarsaw.holesky.golemdb.io/rpc/ws"
|
133
137
|
|
138
|
+
# Polygon / Payments
|
139
|
+
POLYGON_RPC_URL: str = Field(
|
140
|
+
default="https://polygon-rpc.com",
|
141
|
+
description="Polygon PoS RPC URL for GLM payments"
|
142
|
+
)
|
143
|
+
STREAM_PAYMENT_ADDRESS: str = Field(
|
144
|
+
default="0x0000000000000000000000000000000000000000",
|
145
|
+
description="Deployed StreamPayment contract address"
|
146
|
+
)
|
147
|
+
GLM_TOKEN_ADDRESS: str = Field(
|
148
|
+
default="0x0000000000000000000000000000000000000000",
|
149
|
+
description="GLM ERC20 token address on target network"
|
150
|
+
)
|
151
|
+
STREAM_MIN_REMAINING_SECONDS: int = Field(
|
152
|
+
default=3600,
|
153
|
+
description="Minimum remaining seconds required to keep a VM running"
|
154
|
+
)
|
155
|
+
STREAM_MONITOR_ENABLED: bool = Field(
|
156
|
+
default=False,
|
157
|
+
description="Enable background monitor to stop VMs when runway < threshold"
|
158
|
+
)
|
159
|
+
STREAM_WITHDRAW_ENABLED: bool = Field(
|
160
|
+
default=False,
|
161
|
+
description="Enable background withdrawals for active streams"
|
162
|
+
)
|
163
|
+
STREAM_MONITOR_INTERVAL_SECONDS: int = Field(
|
164
|
+
default=60,
|
165
|
+
description="How frequently to check stream runway"
|
166
|
+
)
|
167
|
+
STREAM_WITHDRAW_INTERVAL_SECONDS: int = Field(
|
168
|
+
default=1800,
|
169
|
+
description="How frequently to attempt withdrawals"
|
170
|
+
)
|
171
|
+
STREAM_MIN_WITHDRAW_WEI: int = Field(
|
172
|
+
default=0,
|
173
|
+
description="Min withdrawable amount (wei) before triggering withdraw"
|
174
|
+
)
|
175
|
+
|
134
176
|
# VM Settings
|
135
177
|
MAX_VMS: int = 10
|
136
178
|
DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
|
@@ -274,6 +316,14 @@ class Settings(BaseSettings):
|
|
274
316
|
# Rate Limiting
|
275
317
|
RATE_LIMIT_PER_MINUTE: int = 100
|
276
318
|
|
319
|
+
# Retry/Timeout Settings (for long-running external calls)
|
320
|
+
RETRY_ATTEMPTS: int = 5
|
321
|
+
RETRY_DELAY_SECONDS: float = 2.0
|
322
|
+
RETRY_BACKOFF: float = 2.0
|
323
|
+
CREATE_VM_MAX_RETRIES: int = 15
|
324
|
+
CREATE_VM_RETRY_DELAY_SECONDS: float = 5.0
|
325
|
+
LAUNCH_TIMEOUT_SECONDS: int = 300
|
326
|
+
|
277
327
|
# Multipass Settings
|
278
328
|
MULTIPASS_BINARY_PATH: str = Field(
|
279
329
|
default="",
|
@@ -467,6 +517,25 @@ class Settings(BaseSettings):
|
|
467
517
|
logger.info(f"Using manually provided IP: {v}")
|
468
518
|
return v
|
469
519
|
|
520
|
+
# Pricing Settings (configured in USD; auto-converted to GLM)
|
521
|
+
# Per-month prices per unit
|
522
|
+
PRICE_USD_PER_CORE_MONTH: float = Field(default=5.0, ge=0)
|
523
|
+
PRICE_USD_PER_GB_RAM_MONTH: float = Field(default=2.0, ge=0)
|
524
|
+
PRICE_USD_PER_GB_STORAGE_MONTH: float = Field(default=0.1, ge=0)
|
525
|
+
|
526
|
+
# Auto-updated GLM-denominated prices (derived from USD via CoinGecko)
|
527
|
+
PRICE_GLM_PER_CORE_MONTH: float = Field(default=0.0, ge=0)
|
528
|
+
PRICE_GLM_PER_GB_RAM_MONTH: float = Field(default=0.0, ge=0)
|
529
|
+
PRICE_GLM_PER_GB_STORAGE_MONTH: float = Field(default=0.0, ge=0)
|
530
|
+
|
531
|
+
# CoinGecko integration
|
532
|
+
COINGECKO_API_URL: str = "https://api.coingecko.com/api/v3"
|
533
|
+
COINGECKO_IDS: str = "golem,golem-network-tokens" # try both, first wins
|
534
|
+
PRICING_UPDATE_ENABLED: bool = True
|
535
|
+
PRICING_UPDATE_MIN_DELTA_PERCENT: float = Field(default=1.0, ge=0.0)
|
536
|
+
PRICING_UPDATE_INTERVAL_DISCOVERY: int = 900 # 15 minutes
|
537
|
+
PRICING_UPDATE_INTERVAL_GOLEM_BASE: int = 14400 # 4 hours
|
538
|
+
|
470
539
|
class Config:
|
471
540
|
env_prefix = "GOLEM_PROVIDER_"
|
472
541
|
case_sensitive = True
|
@@ -6,6 +6,7 @@ from .config import settings
|
|
6
6
|
from .discovery.resource_tracker import ResourceTracker
|
7
7
|
from .discovery.golem_base_advertiser import GolemBaseAdvertiser
|
8
8
|
from .discovery.advertiser import DiscoveryServerAdvertiser
|
9
|
+
from .discovery.multi_advertiser import MultiAdvertiser
|
9
10
|
from .discovery.service import AdvertisementService
|
10
11
|
from .service import ProviderService
|
11
12
|
from .vm.multipass_adapter import MultipassAdapter
|
@@ -13,6 +14,9 @@ from .vm.service import VMService
|
|
13
14
|
from .vm.name_mapper import VMNameMapper
|
14
15
|
from .vm.port_manager import PortManager
|
15
16
|
from .vm.proxy_manager import PythonProxyManager
|
17
|
+
from .payments.stream_map import StreamMap
|
18
|
+
from .payments.blockchain_service import StreamPaymentReader, StreamPaymentClient, StreamPaymentConfig as _SPC
|
19
|
+
from .payments.monitor import StreamMonitor
|
16
20
|
|
17
21
|
|
18
22
|
class Container(containers.DeclarativeContainer):
|
@@ -32,6 +36,10 @@ class Container(containers.DeclarativeContainer):
|
|
32
36
|
DiscoveryServerAdvertiser,
|
33
37
|
resource_tracker=resource_tracker,
|
34
38
|
),
|
39
|
+
both=providers.Singleton(
|
40
|
+
MultiAdvertiser,
|
41
|
+
resource_tracker=resource_tracker,
|
42
|
+
),
|
35
43
|
)
|
36
44
|
|
37
45
|
advertisement_service = providers.Singleton(
|
@@ -44,6 +52,11 @@ class Container(containers.DeclarativeContainer):
|
|
44
52
|
db_path=Path(settings.VM_DATA_DIR) / "vm_names.json",
|
45
53
|
)
|
46
54
|
|
55
|
+
stream_map = providers.Singleton(
|
56
|
+
StreamMap,
|
57
|
+
storage_path=Path(settings.VM_DATA_DIR) / "streams.json",
|
58
|
+
)
|
59
|
+
|
47
60
|
port_manager = providers.Singleton(
|
48
61
|
PortManager,
|
49
62
|
start_port=config.PORT_RANGE_START,
|
@@ -76,6 +89,31 @@ class Container(containers.DeclarativeContainer):
|
|
76
89
|
name_mapper=vm_name_mapper,
|
77
90
|
)
|
78
91
|
|
92
|
+
# Payments
|
93
|
+
stream_reader = providers.Factory(
|
94
|
+
StreamPaymentReader,
|
95
|
+
rpc_url=config.POLYGON_RPC_URL,
|
96
|
+
contract_address=config.STREAM_PAYMENT_ADDRESS,
|
97
|
+
)
|
98
|
+
stream_client = providers.Factory(
|
99
|
+
StreamPaymentClient,
|
100
|
+
cfg=providers.Callable(
|
101
|
+
lambda rpc, addr, pk: _SPC(rpc_url=rpc, contract_address=addr, private_key=pk),
|
102
|
+
config.POLYGON_RPC_URL,
|
103
|
+
config.STREAM_PAYMENT_ADDRESS,
|
104
|
+
config.ETHEREUM_PRIVATE_KEY,
|
105
|
+
),
|
106
|
+
)
|
107
|
+
|
108
|
+
stream_monitor = providers.Singleton(
|
109
|
+
StreamMonitor,
|
110
|
+
stream_map=stream_map,
|
111
|
+
vm_service=vm_service,
|
112
|
+
reader=stream_reader,
|
113
|
+
client=stream_client,
|
114
|
+
settings=config,
|
115
|
+
)
|
116
|
+
|
79
117
|
provider_service = providers.Singleton(
|
80
118
|
ProviderService,
|
81
119
|
vm_service=vm_service,
|
@@ -65,7 +65,7 @@ class DiscoveryServerAdvertiser(Advertiser):
|
|
65
65
|
try:
|
66
66
|
while not self._stop_event.is_set():
|
67
67
|
await self.post_advertisement()
|
68
|
-
await asyncio.sleep(settings.
|
68
|
+
await asyncio.sleep(settings.DISCOVERY_ADVERTISEMENT_INTERVAL)
|
69
69
|
finally:
|
70
70
|
await self.stop()
|
71
71
|
|
@@ -76,7 +76,12 @@ class DiscoveryServerAdvertiser(Advertiser):
|
|
76
76
|
await self.session.close()
|
77
77
|
self.session = None
|
78
78
|
|
79
|
-
@async_retry(
|
79
|
+
@async_retry(
|
80
|
+
retries=settings.RETRY_ATTEMPTS,
|
81
|
+
delay=settings.RETRY_DELAY_SECONDS,
|
82
|
+
backoff=settings.RETRY_BACKOFF,
|
83
|
+
exceptions=(aiohttp.ClientError, asyncio.TimeoutError),
|
84
|
+
)
|
80
85
|
async def _check_discovery_health(self):
|
81
86
|
"""Check discovery service health with retries."""
|
82
87
|
if not self.session:
|
@@ -86,7 +91,12 @@ class DiscoveryServerAdvertiser(Advertiser):
|
|
86
91
|
if not response.ok:
|
87
92
|
raise Exception(f"Discovery service health check failed: {response.status}")
|
88
93
|
|
89
|
-
@async_retry(
|
94
|
+
@async_retry(
|
95
|
+
retries=settings.RETRY_ATTEMPTS,
|
96
|
+
delay=settings.RETRY_DELAY_SECONDS,
|
97
|
+
backoff=settings.RETRY_BACKOFF,
|
98
|
+
exceptions=(aiohttp.ClientError, asyncio.TimeoutError),
|
99
|
+
)
|
90
100
|
async def post_advertisement(self):
|
91
101
|
"""Post resource advertisement to discovery service."""
|
92
102
|
if not self.session:
|
@@ -115,7 +125,15 @@ class DiscoveryServerAdvertiser(Advertiser):
|
|
115
125
|
json={
|
116
126
|
"ip_address": ip_address,
|
117
127
|
"country": settings.PROVIDER_COUNTRY,
|
118
|
-
"resources": resources
|
128
|
+
"resources": resources,
|
129
|
+
"pricing": {
|
130
|
+
"usd_per_core_month": settings.PRICE_USD_PER_CORE_MONTH,
|
131
|
+
"usd_per_gb_ram_month": settings.PRICE_USD_PER_GB_RAM_MONTH,
|
132
|
+
"usd_per_gb_storage_month": settings.PRICE_USD_PER_GB_STORAGE_MONTH,
|
133
|
+
"glm_per_core_month": settings.PRICE_GLM_PER_CORE_MONTH,
|
134
|
+
"glm_per_gb_ram_month": settings.PRICE_GLM_PER_GB_RAM_MONTH,
|
135
|
+
"glm_per_gb_storage_month": settings.PRICE_GLM_PER_GB_STORAGE_MONTH,
|
136
|
+
}
|
119
137
|
},
|
120
138
|
timeout=aiohttp.ClientTimeout(total=5)
|
121
139
|
) as response:
|
@@ -132,7 +150,12 @@ class DiscoveryServerAdvertiser(Advertiser):
|
|
132
150
|
logger.error("Advertisement request timed out")
|
133
151
|
raise
|
134
152
|
|
135
|
-
@async_retry(
|
153
|
+
@async_retry(
|
154
|
+
retries=settings.RETRY_ATTEMPTS,
|
155
|
+
delay=settings.RETRY_DELAY_SECONDS,
|
156
|
+
backoff=settings.RETRY_BACKOFF,
|
157
|
+
exceptions=(aiohttp.ClientError, asyncio.TimeoutError),
|
158
|
+
)
|
136
159
|
async def _get_public_ip(self) -> str:
|
137
160
|
"""Get public IP address with retries."""
|
138
161
|
if not self.session:
|
{golem_vm_provider-0.1.37 → golem_vm_provider-0.1.39}/provider/discovery/golem_base_advertiser.py
RENAMED
@@ -36,7 +36,7 @@ class GolemBaseAdvertiser(Advertiser):
|
|
36
36
|
try:
|
37
37
|
while not self._stop_event.is_set():
|
38
38
|
await self.post_advertisement()
|
39
|
-
await asyncio.sleep(settings.
|
39
|
+
await asyncio.sleep(settings.GOLEM_BASE_ADVERTISEMENT_INTERVAL)
|
40
40
|
finally:
|
41
41
|
await self.stop()
|
42
42
|
|
@@ -70,11 +70,18 @@ class GolemBaseAdvertiser(Advertiser):
|
|
70
70
|
Annotation(key="golem_ip_address", value=ip_address),
|
71
71
|
Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
|
72
72
|
Annotation(key="golem_provider_name", value=settings.PROVIDER_NAME),
|
73
|
+
Annotation(key="golem_price_currency", value="USD/GLM"),
|
73
74
|
]
|
74
75
|
numeric_annotations = [
|
75
76
|
Annotation(key="golem_cpu", value=resources["cpu"]),
|
76
77
|
Annotation(key="golem_memory", value=resources["memory"]),
|
77
78
|
Annotation(key="golem_storage", value=resources["storage"]),
|
79
|
+
Annotation(key="golem_price_usd_core_month", value=float(settings.PRICE_USD_PER_CORE_MONTH)),
|
80
|
+
Annotation(key="golem_price_usd_ram_gb_month", value=float(settings.PRICE_USD_PER_GB_RAM_MONTH)),
|
81
|
+
Annotation(key="golem_price_usd_storage_gb_month", value=float(settings.PRICE_USD_PER_GB_STORAGE_MONTH)),
|
82
|
+
Annotation(key="golem_price_glm_core_month", value=float(settings.PRICE_GLM_PER_CORE_MONTH)),
|
83
|
+
Annotation(key="golem_price_glm_ram_gb_month", value=float(settings.PRICE_GLM_PER_GB_RAM_MONTH)),
|
84
|
+
Annotation(key="golem_price_glm_storage_gb_month", value=float(settings.PRICE_GLM_PER_GB_STORAGE_MONTH)),
|
78
85
|
]
|
79
86
|
|
80
87
|
if len(existing_keys) > 1:
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from .advertiser import Advertiser
|
5
|
+
from .golem_base_advertiser import GolemBaseAdvertiser
|
6
|
+
from .advertiser import DiscoveryServerAdvertiser
|
7
|
+
from .resource_tracker import ResourceTracker
|
8
|
+
|
9
|
+
|
10
|
+
class MultiAdvertiser(Advertiser):
|
11
|
+
"""Advertise to both Golem Base and the Discovery Server."""
|
12
|
+
|
13
|
+
def __init__(self, resource_tracker: ResourceTracker):
|
14
|
+
self.golem = GolemBaseAdvertiser(resource_tracker)
|
15
|
+
self.discovery = DiscoveryServerAdvertiser(resource_tracker)
|
16
|
+
|
17
|
+
async def initialize(self):
|
18
|
+
await asyncio.gather(self.golem.initialize(), self.discovery.initialize())
|
19
|
+
|
20
|
+
async def start_loop(self):
|
21
|
+
await asyncio.gather(self.golem.start_loop(), self.discovery.start_loop())
|
22
|
+
|
23
|
+
async def stop(self):
|
24
|
+
await asyncio.gather(self.golem.stop(), self.discovery.stop())
|
25
|
+
|
26
|
+
async def post_advertisement(self):
|
27
|
+
await asyncio.gather(self.golem.post_advertisement(), self.discovery.post_advertisement())
|
28
|
+
|
@@ -21,4 +21,11 @@ class AdvertisementService:
|
|
21
21
|
if self._task:
|
22
22
|
self._task.cancel()
|
23
23
|
await self._task
|
24
|
-
await self.advertiser.stop()
|
24
|
+
await self.advertiser.stop()
|
25
|
+
|
26
|
+
async def trigger_update(self):
|
27
|
+
"""Trigger an immediate advertisement update."""
|
28
|
+
try:
|
29
|
+
await self.advertiser.post_advertisement()
|
30
|
+
except Exception:
|
31
|
+
pass
|
@@ -121,6 +121,8 @@ except ImportError:
|
|
121
121
|
import importlib_metadata as metadata
|
122
122
|
|
123
123
|
cli = typer.Typer()
|
124
|
+
pricing_app = typer.Typer(help="Configure USD pricing; auto-converts to GLM.")
|
125
|
+
cli.add_typer(pricing_app, name="pricing")
|
124
126
|
|
125
127
|
def print_version(ctx: typer.Context, value: bool):
|
126
128
|
if not value:
|
@@ -149,6 +151,64 @@ def dev(no_verify_port: bool = typer.Option(True, "--no-verify-port", help="Skip
|
|
149
151
|
"""Start the provider server in development mode."""
|
150
152
|
run_server(dev_mode=True, no_verify_port=no_verify_port)
|
151
153
|
|
154
|
+
def _env_path_for(dev_mode: Optional[bool]) -> str:
|
155
|
+
from pathlib import Path
|
156
|
+
env_file = ".env.dev" if dev_mode else ".env"
|
157
|
+
return str(Path(__file__).parent.parent / env_file)
|
158
|
+
|
159
|
+
def _write_env_vars(path: str, updates: dict):
|
160
|
+
# Simple .env updater: preserves other lines, replaces/append updated keys
|
161
|
+
import re
|
162
|
+
import io
|
163
|
+
try:
|
164
|
+
with open(path, "r") as f:
|
165
|
+
lines = f.readlines()
|
166
|
+
except FileNotFoundError:
|
167
|
+
lines = []
|
168
|
+
|
169
|
+
kv = {**updates}
|
170
|
+
pattern = re.compile(r"^(?P<k>[A-Z0-9_]+)=.*$")
|
171
|
+
out = []
|
172
|
+
seen = set()
|
173
|
+
for line in lines:
|
174
|
+
m = pattern.match(line.strip())
|
175
|
+
if not m:
|
176
|
+
out.append(line)
|
177
|
+
continue
|
178
|
+
k = m.group("k")
|
179
|
+
if k in kv:
|
180
|
+
out.append(f"{k}={kv[k]}\n")
|
181
|
+
seen.add(k)
|
182
|
+
else:
|
183
|
+
out.append(line)
|
184
|
+
for k, v in kv.items():
|
185
|
+
if k not in seen:
|
186
|
+
out.append(f"{k}={v}\n")
|
187
|
+
|
188
|
+
with open(path, "w") as f:
|
189
|
+
f.writelines(out)
|
190
|
+
|
191
|
+
def _print_pricing_examples(glm_usd):
|
192
|
+
from decimal import Decimal
|
193
|
+
from .utils.pricing import calculate_monthly_cost, calculate_monthly_cost_usd
|
194
|
+
from .vm.models import VMResources
|
195
|
+
examples = [
|
196
|
+
("Small", VMResources(cpu=1, memory=1, storage=10)),
|
197
|
+
("Medium", VMResources(cpu=2, memory=4, storage=20)),
|
198
|
+
("Example 2c/2g/10g", VMResources(cpu=2, memory=2, storage=10)),
|
199
|
+
]
|
200
|
+
# Maintain legacy header for tests while adding a clearer caption
|
201
|
+
print("\nExample monthly costs with current settings:")
|
202
|
+
print("(Estimated monthly earnings with your current pricing)")
|
203
|
+
for name, res in examples:
|
204
|
+
glm = calculate_monthly_cost(res)
|
205
|
+
usd = calculate_monthly_cost_usd(res, glm_usd)
|
206
|
+
usd_str = f"${usd:.2f}" if usd is not None else "—"
|
207
|
+
glm_str = f"{glm:.4f} GLM"
|
208
|
+
print(
|
209
|
+
f"- {name} ({res.cpu}C, {res.memory}GB RAM, {res.storage}GB Disk): ~{usd_str} per month (~{glm_str})"
|
210
|
+
)
|
211
|
+
|
152
212
|
def run_server(dev_mode: bool, no_verify_port: bool):
|
153
213
|
"""Helper to run the uvicorn server."""
|
154
214
|
import sys
|
@@ -209,3 +269,74 @@ def run_server(dev_mode: bool, no_verify_port: bool):
|
|
209
269
|
|
210
270
|
if __name__ == "__main__":
|
211
271
|
cli()
|
272
|
+
|
273
|
+
|
274
|
+
@pricing_app.command("show")
|
275
|
+
def pricing_show():
|
276
|
+
"""Show current USD and GLM per-unit monthly prices and examples."""
|
277
|
+
from decimal import Decimal
|
278
|
+
from .utils.pricing import fetch_glm_usd_price, update_glm_unit_prices_from_usd
|
279
|
+
|
280
|
+
print("Current pricing (per month):")
|
281
|
+
print(
|
282
|
+
f" - USD per unit: CPU ${settings.PRICE_USD_PER_CORE_MONTH}/core, RAM ${settings.PRICE_USD_PER_GB_RAM_MONTH}/GB, Disk ${settings.PRICE_USD_PER_GB_STORAGE_MONTH}/GB"
|
283
|
+
)
|
284
|
+
glm_usd = fetch_glm_usd_price()
|
285
|
+
if not glm_usd:
|
286
|
+
print("Error: Could not fetch GLM/USD price. Please try again later.")
|
287
|
+
raise typer.Exit(code=1)
|
288
|
+
# Coerce to Decimal for calculations if needed
|
289
|
+
from decimal import Decimal
|
290
|
+
if not isinstance(glm_usd, Decimal):
|
291
|
+
glm_usd = Decimal(str(glm_usd))
|
292
|
+
update_glm_unit_prices_from_usd(glm_usd)
|
293
|
+
print(f" - GLM price: ${glm_usd} per GLM")
|
294
|
+
print(f" - Rate: {glm_usd} USD/GLM")
|
295
|
+
print(
|
296
|
+
f" - GLM per unit: CPU {round(float(settings.PRICE_GLM_PER_CORE_MONTH), 6)} GLM/core, RAM {round(float(settings.PRICE_GLM_PER_GB_RAM_MONTH), 6)} GLM/GB, Disk {round(float(settings.PRICE_GLM_PER_GB_STORAGE_MONTH), 6)} GLM/GB"
|
297
|
+
)
|
298
|
+
_print_pricing_examples(glm_usd)
|
299
|
+
|
300
|
+
|
301
|
+
@pricing_app.command("set")
|
302
|
+
def pricing_set(
|
303
|
+
usd_per_core: float = typer.Option(
|
304
|
+
..., "--usd-per-core", "--core-usd", help="USD per CPU core per month"
|
305
|
+
),
|
306
|
+
usd_per_mem: float = typer.Option(
|
307
|
+
..., "--usd-per-mem", "--ram-usd", help="USD per GB of RAM per month"
|
308
|
+
),
|
309
|
+
usd_per_disk: float = typer.Option(
|
310
|
+
..., "--usd-per-disk", "--usd-per-storage", "--storage-usd", help="USD per GB of disk per month"
|
311
|
+
),
|
312
|
+
dev: bool = typer.Option(False, "--dev", help="Write to .env.dev instead of .env"),
|
313
|
+
):
|
314
|
+
"""Set USD pricing; GLM rates auto-update via CoinGecko in background."""
|
315
|
+
if usd_per_core < 0 or usd_per_mem < 0 or usd_per_disk < 0:
|
316
|
+
raise typer.BadParameter("All pricing values must be >= 0")
|
317
|
+
env_path = _env_path_for(dev)
|
318
|
+
updates = {
|
319
|
+
"GOLEM_PROVIDER_PRICE_USD_PER_CORE_MONTH": usd_per_core,
|
320
|
+
"GOLEM_PROVIDER_PRICE_USD_PER_GB_RAM_MONTH": usd_per_mem,
|
321
|
+
"GOLEM_PROVIDER_PRICE_USD_PER_GB_STORAGE_MONTH": usd_per_disk,
|
322
|
+
}
|
323
|
+
_write_env_vars(env_path, updates)
|
324
|
+
print(f"Updated pricing in {env_path}")
|
325
|
+
# Immediately reflect in current process settings as well
|
326
|
+
settings.PRICE_USD_PER_CORE_MONTH = usd_per_core
|
327
|
+
settings.PRICE_USD_PER_GB_RAM_MONTH = usd_per_mem
|
328
|
+
settings.PRICE_USD_PER_GB_STORAGE_MONTH = usd_per_disk
|
329
|
+
|
330
|
+
from .utils.pricing import fetch_glm_usd_price, update_glm_unit_prices_from_usd
|
331
|
+
glm_usd = fetch_glm_usd_price()
|
332
|
+
if glm_usd:
|
333
|
+
# Coerce to Decimal for calculations if needed
|
334
|
+
from decimal import Decimal
|
335
|
+
if not isinstance(glm_usd, Decimal):
|
336
|
+
glm_usd = Decimal(str(glm_usd))
|
337
|
+
update_glm_unit_prices_from_usd(glm_usd)
|
338
|
+
print("Recalculated GLM prices due to updated USD configuration.")
|
339
|
+
_print_pricing_examples(glm_usd)
|
340
|
+
else:
|
341
|
+
print("Warning: could not fetch GLM/USD; GLM unit prices not recalculated.")
|
342
|
+
print("Tip: run 'golem-provider pricing show' when online to verify pricing with USD examples.")
|