request-vm-on-golem 0.1.40__tar.gz → 0.1.41__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.
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/PKG-INFO +59 -1
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/README.md +58 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/pyproject.toml +1 -1
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/cli/commands.py +86 -2
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/config.py +18 -0
- request_vm_on_golem-0.1.41/requestor/payments/blockchain_service.py +148 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/provider/client.py +21 -10
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/vm_service.py +48 -4
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/__init__.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/api/main.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/cli/__init__.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/db/__init__.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/db/sqlite.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/errors.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/provider/__init__.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/run.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/__init__.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/database_service.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/provider_service.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/ssh_service.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/ssh/__init__.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/ssh/manager.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/utils/logging.py +0 -0
- {request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/utils/spinner.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: request-vm-on-golem
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.41
|
4
4
|
Summary: VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network
|
5
5
|
Keywords: golem,vm,cloud,decentralized,cli
|
6
6
|
Author: Phillip Jensen
|
@@ -119,6 +119,64 @@ The SSH connection process:
|
|
119
119
|
2. The provider's proxy system forwards your SSH connection to the VM
|
120
120
|
3. All traffic is securely routed through the allocated port
|
121
121
|
|
122
|
+
## Streaming Payments (Polygon GLM)
|
123
|
+
|
124
|
+
This requestor integrates with an on‑chain StreamPayment contract to enable “pay‑as‑you‑go” rentals.
|
125
|
+
|
126
|
+
Flow:
|
127
|
+
|
128
|
+
1. Fetch provider info:
|
129
|
+
- `GET http://{provider}:7466/api/v1/provider/info` → `provider_id`, `stream_payment_address`, `glm_token_address`.
|
130
|
+
2. Compute `ratePerSecond` from provider pricing and requested VM resources.
|
131
|
+
3. Ensure `deposit >= ratePerSecond * 3600` (≥ 1 hour runway recommended/minimum).
|
132
|
+
4. Create a stream (approve + `createStream(GLM, provider_id, deposit, ratePerSecond)`), capture `stream_id`.
|
133
|
+
5. Create VM: `POST /api/v1/vms` with `stream_id` included.
|
134
|
+
6. Top‑up over time with `topUp(stream_id, amount)` to extend stopTime and keep the VM running indefinitely.
|
135
|
+
7. On stop/destroy: the requestor will best‑effort `withdraw` / `terminate` to settle.
|
136
|
+
|
137
|
+
CLI helpers
|
138
|
+
|
139
|
+
- Open a stream for a planned VM (computes rate from provider pricing):
|
140
|
+
|
141
|
+
```bash
|
142
|
+
poetry run golem vm stream open \
|
143
|
+
--provider-id 0xProvider \
|
144
|
+
--cpu 2 --memory 4 --storage 20 \
|
145
|
+
--hours 1
|
146
|
+
# prints { stream_id, rate_per_second_wei, deposit_wei }
|
147
|
+
```
|
148
|
+
|
149
|
+
- Top up an existing stream:
|
150
|
+
|
151
|
+
```bash
|
152
|
+
# Add 3 hours at prior rate
|
153
|
+
poetry run golem vm stream topup --stream-id 123 --hours 3
|
154
|
+
|
155
|
+
# Or specify exact GLM amount
|
156
|
+
poetry run golem vm stream topup --stream-id 123 --glm 25.0
|
157
|
+
```
|
158
|
+
|
159
|
+
- Create a VM and attach an existing stream:
|
160
|
+
|
161
|
+
```bash
|
162
|
+
poetry run golem vm create my-vm \
|
163
|
+
--provider-id 0xProvider \
|
164
|
+
--cpu 2 --memory 4 --storage 20 \
|
165
|
+
--stream-id 123
|
166
|
+
```
|
167
|
+
|
168
|
+
Environment (env prefix `GOLEM_REQUESTOR_`):
|
169
|
+
|
170
|
+
- `polygon_rpc_url` — Polygon PoS RPC URL
|
171
|
+
- `stream_payment_address` — StreamPayment address
|
172
|
+
- `glm_token_address` — GLM ERC20 address
|
173
|
+
- `provider_eth_address` — optional helper for development; in production always use `/provider/info`
|
174
|
+
|
175
|
+
Efficiency tips:
|
176
|
+
|
177
|
+
- Batch top‑ups (e.g., add several hours at once) to reduce on‑chain calls.
|
178
|
+
- Withdrawals are typically executed by providers; requestors don’t need to withdraw.
|
179
|
+
|
122
180
|
## Installation
|
123
181
|
|
124
182
|
```bash
|
@@ -80,6 +80,64 @@ The SSH connection process:
|
|
80
80
|
2. The provider's proxy system forwards your SSH connection to the VM
|
81
81
|
3. All traffic is securely routed through the allocated port
|
82
82
|
|
83
|
+
## Streaming Payments (Polygon GLM)
|
84
|
+
|
85
|
+
This requestor integrates with an on‑chain StreamPayment contract to enable “pay‑as‑you‑go” rentals.
|
86
|
+
|
87
|
+
Flow:
|
88
|
+
|
89
|
+
1. Fetch provider info:
|
90
|
+
- `GET http://{provider}:7466/api/v1/provider/info` → `provider_id`, `stream_payment_address`, `glm_token_address`.
|
91
|
+
2. Compute `ratePerSecond` from provider pricing and requested VM resources.
|
92
|
+
3. Ensure `deposit >= ratePerSecond * 3600` (≥ 1 hour runway recommended/minimum).
|
93
|
+
4. Create a stream (approve + `createStream(GLM, provider_id, deposit, ratePerSecond)`), capture `stream_id`.
|
94
|
+
5. Create VM: `POST /api/v1/vms` with `stream_id` included.
|
95
|
+
6. Top‑up over time with `topUp(stream_id, amount)` to extend stopTime and keep the VM running indefinitely.
|
96
|
+
7. On stop/destroy: the requestor will best‑effort `withdraw` / `terminate` to settle.
|
97
|
+
|
98
|
+
CLI helpers
|
99
|
+
|
100
|
+
- Open a stream for a planned VM (computes rate from provider pricing):
|
101
|
+
|
102
|
+
```bash
|
103
|
+
poetry run golem vm stream open \
|
104
|
+
--provider-id 0xProvider \
|
105
|
+
--cpu 2 --memory 4 --storage 20 \
|
106
|
+
--hours 1
|
107
|
+
# prints { stream_id, rate_per_second_wei, deposit_wei }
|
108
|
+
```
|
109
|
+
|
110
|
+
- Top up an existing stream:
|
111
|
+
|
112
|
+
```bash
|
113
|
+
# Add 3 hours at prior rate
|
114
|
+
poetry run golem vm stream topup --stream-id 123 --hours 3
|
115
|
+
|
116
|
+
# Or specify exact GLM amount
|
117
|
+
poetry run golem vm stream topup --stream-id 123 --glm 25.0
|
118
|
+
```
|
119
|
+
|
120
|
+
- Create a VM and attach an existing stream:
|
121
|
+
|
122
|
+
```bash
|
123
|
+
poetry run golem vm create my-vm \
|
124
|
+
--provider-id 0xProvider \
|
125
|
+
--cpu 2 --memory 4 --storage 20 \
|
126
|
+
--stream-id 123
|
127
|
+
```
|
128
|
+
|
129
|
+
Environment (env prefix `GOLEM_REQUESTOR_`):
|
130
|
+
|
131
|
+
- `polygon_rpc_url` — Polygon PoS RPC URL
|
132
|
+
- `stream_payment_address` — StreamPayment address
|
133
|
+
- `glm_token_address` — GLM ERC20 address
|
134
|
+
- `provider_eth_address` — optional helper for development; in production always use `/provider/info`
|
135
|
+
|
136
|
+
Efficiency tips:
|
137
|
+
|
138
|
+
- Batch top‑ups (e.g., add several hours at once) to reduce on‑chain calls.
|
139
|
+
- Withdrawals are typically executed by providers; requestors don’t need to withdraw.
|
140
|
+
|
83
141
|
## Installation
|
84
142
|
|
85
143
|
```bash
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "request-vm-on-golem"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.41"
|
4
4
|
description = "VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network"
|
5
5
|
authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
|
6
6
|
readme = "README.md"
|
@@ -173,9 +173,10 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
173
173
|
@click.option('--cpu', type=int, required=True, help='Number of CPU cores')
|
174
174
|
@click.option('--memory', type=int, required=True, help='Memory in GB')
|
175
175
|
@click.option('--storage', type=int, required=True, help='Disk in GB')
|
176
|
+
@click.option('--stream-id', type=int, default=None, help='Optional StreamPayment stream id to fund this VM')
|
176
177
|
@click.option('--yes', is_flag=True, help='Do not prompt for confirmation')
|
177
178
|
@async_command
|
178
|
-
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, yes: bool):
|
179
|
+
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, stream_id: int | None, yes: bool):
|
179
180
|
"""Create a new VM on a specific provider."""
|
180
181
|
try:
|
181
182
|
# Show configuration details
|
@@ -232,7 +233,8 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
232
233
|
memory=memory,
|
233
234
|
storage=storage,
|
234
235
|
provider_ip=provider_ip,
|
235
|
-
ssh_key=key_pair.public_key_content
|
236
|
+
ssh_key=key_pair.public_key_content,
|
237
|
+
stream_id=stream_id
|
236
238
|
)
|
237
239
|
|
238
240
|
# Get access info from config
|
@@ -281,6 +283,88 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
281
283
|
raise click.Abort()
|
282
284
|
|
283
285
|
|
286
|
+
@vm.group(name='stream')
|
287
|
+
def vm_stream():
|
288
|
+
"""Streaming payments helpers"""
|
289
|
+
pass
|
290
|
+
|
291
|
+
|
292
|
+
@vm_stream.command('open')
|
293
|
+
@click.option('--provider-id', required=True, help='Provider ID to use')
|
294
|
+
@click.option('--cpu', type=int, required=True, help='CPU cores for rate calc')
|
295
|
+
@click.option('--memory', type=int, required=True, help='Memory (GB) for rate calc')
|
296
|
+
@click.option('--storage', type=int, required=True, help='Storage (GB) for rate calc')
|
297
|
+
@click.option('--hours', type=int, default=1, help='Deposit coverage in hours (default 1)')
|
298
|
+
@async_command
|
299
|
+
async def stream_open(provider_id: str, cpu: int, memory: int, storage: int, hours: int):
|
300
|
+
"""Create a GLM stream for a planned VM rental."""
|
301
|
+
from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
302
|
+
try:
|
303
|
+
provider_service = ProviderService()
|
304
|
+
async with provider_service:
|
305
|
+
provider = await provider_service.verify_provider(provider_id)
|
306
|
+
est = provider_service.compute_estimate(provider, (cpu, memory, storage))
|
307
|
+
if not est or est.get('glm_per_month') is None:
|
308
|
+
raise RequestorError('Provider does not advertise GLM pricing; cannot compute ratePerSecond')
|
309
|
+
glm_month = est['glm_per_month']
|
310
|
+
glm_per_second = float(glm_month) / (730.0 * 3600.0)
|
311
|
+
rate_per_second_wei = int(glm_per_second * (10**18))
|
312
|
+
|
313
|
+
provider_ip = 'localhost' if config.environment == "development" else provider.get('ip_address')
|
314
|
+
if not provider_ip and config.environment == "production":
|
315
|
+
raise RequestorError("Provider IP address not found in advertisement")
|
316
|
+
provider_url = config.get_provider_url(provider_ip)
|
317
|
+
async with ProviderClient(provider_url) as client:
|
318
|
+
info = await client.get_provider_info()
|
319
|
+
recipient = info['provider_id']
|
320
|
+
|
321
|
+
deposit_wei = rate_per_second_wei * int(hours) * 3600
|
322
|
+
spc = StreamPaymentConfig(
|
323
|
+
rpc_url=config.polygon_rpc_url,
|
324
|
+
contract_address=config.stream_payment_address,
|
325
|
+
glm_token_address=config.glm_token_address,
|
326
|
+
private_key=config.ethereum_private_key,
|
327
|
+
)
|
328
|
+
sp = StreamPaymentClient(spc)
|
329
|
+
stream_id = sp.create_stream(recipient, deposit_wei, rate_per_second_wei)
|
330
|
+
click.echo(json.dumps({"stream_id": stream_id, "rate_per_second_wei": rate_per_second_wei, "deposit_wei": deposit_wei}, indent=2))
|
331
|
+
except Exception as e:
|
332
|
+
logger.error(f"Failed to open stream: {e}")
|
333
|
+
raise click.Abort()
|
334
|
+
|
335
|
+
|
336
|
+
@vm_stream.command('topup')
|
337
|
+
@click.option('--stream-id', type=int, required=True)
|
338
|
+
@click.option('--glm', type=float, required=False, help='GLM amount to add')
|
339
|
+
@click.option('--hours', type=int, required=False, help='Hours of coverage to add at prior rate')
|
340
|
+
@async_command
|
341
|
+
async def stream_topup(stream_id: int, glm: float | None, hours: int | None):
|
342
|
+
"""Top up a stream. Provide either --glm or --hours (using prior rate)."""
|
343
|
+
from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
344
|
+
try:
|
345
|
+
spc = StreamPaymentConfig(
|
346
|
+
rpc_url=config.polygon_rpc_url,
|
347
|
+
contract_address=config.stream_payment_address,
|
348
|
+
glm_token_address=config.glm_token_address,
|
349
|
+
private_key=config.ethereum_private_key,
|
350
|
+
)
|
351
|
+
sp = StreamPaymentClient(spc)
|
352
|
+
add_wei: int
|
353
|
+
if glm is not None:
|
354
|
+
add_wei = int(float(glm) * (10**18))
|
355
|
+
elif hours is not None:
|
356
|
+
# naive: use last known rate by reading on-chain stream
|
357
|
+
rate = sp.contract.functions.streams(int(stream_id)).call()[5] # ratePerSecond
|
358
|
+
add_wei = int(rate) * int(hours) * 3600
|
359
|
+
else:
|
360
|
+
raise RequestorError('Provide either --glm or --hours')
|
361
|
+
tx = sp.top_up(stream_id, add_wei)
|
362
|
+
click.echo(json.dumps({"stream_id": stream_id, "topped_up_wei": add_wei, "tx": tx}, indent=2))
|
363
|
+
except Exception as e:
|
364
|
+
logger.error(f"Failed to top up stream: {e}")
|
365
|
+
raise click.Abort()
|
366
|
+
|
367
|
+
|
284
368
|
@vm.command(name='ssh')
|
285
369
|
@click.argument('name')
|
286
370
|
@async_command
|
@@ -94,6 +94,24 @@ class RequestorConfig(BaseSettings):
|
|
94
94
|
description="Private key for Golem Base"
|
95
95
|
)
|
96
96
|
|
97
|
+
# Polygon / Payments
|
98
|
+
polygon_rpc_url: str = Field(
|
99
|
+
default="https://polygon-rpc.com",
|
100
|
+
description="Polygon PoS RPC URL for GLM payments"
|
101
|
+
)
|
102
|
+
stream_payment_address: str = Field(
|
103
|
+
default="0x0000000000000000000000000000000000000000",
|
104
|
+
description="Deployed StreamPayment contract address"
|
105
|
+
)
|
106
|
+
glm_token_address: str = Field(
|
107
|
+
default="0x0000000000000000000000000000000000000000",
|
108
|
+
description="GLM ERC20 token address on target network"
|
109
|
+
)
|
110
|
+
provider_eth_address: str = Field(
|
111
|
+
default="",
|
112
|
+
description="Optional provider Ethereum address for test/dev streaming"
|
113
|
+
)
|
114
|
+
|
97
115
|
# Base Directory
|
98
116
|
base_dir: Path = Field(
|
99
117
|
default_factory=lambda: Path.home() / ".golem" / "requestor",
|
@@ -0,0 +1,148 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Optional, Any, Dict
|
5
|
+
|
6
|
+
from web3 import Web3
|
7
|
+
from eth_account import Account
|
8
|
+
|
9
|
+
|
10
|
+
STREAM_PAYMENT_ABI = [
|
11
|
+
{
|
12
|
+
"inputs": [
|
13
|
+
{"internalType": "address", "name": "token", "type": "address"},
|
14
|
+
{"internalType": "address", "name": "recipient", "type": "address"},
|
15
|
+
{"internalType": "uint256", "name": "deposit", "type": "uint256"},
|
16
|
+
{"internalType": "uint128", "name": "ratePerSecond", "type": "uint128"},
|
17
|
+
],
|
18
|
+
"name": "createStream",
|
19
|
+
"outputs": [{"internalType": "uint256", "name": "streamId", "type": "uint256"}],
|
20
|
+
"stateMutability": "nonpayable",
|
21
|
+
"type": "function",
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"inputs": [{"internalType": "uint256", "name": "streamId", "type": "uint256"}],
|
25
|
+
"name": "withdraw",
|
26
|
+
"outputs": [],
|
27
|
+
"stateMutability": "nonpayable",
|
28
|
+
"type": "function",
|
29
|
+
},
|
30
|
+
{
|
31
|
+
"inputs": [{"internalType": "uint256", "name": "streamId", "type": "uint256"}],
|
32
|
+
"name": "terminate",
|
33
|
+
"outputs": [],
|
34
|
+
"stateMutability": "nonpayable",
|
35
|
+
"type": "function",
|
36
|
+
},
|
37
|
+
{
|
38
|
+
"anonymous": False,
|
39
|
+
"inputs": [
|
40
|
+
{"indexed": True, "internalType": "uint256", "name": "streamId", "type": "uint256"},
|
41
|
+
{"indexed": True, "internalType": "address", "name": "sender", "type": "address"},
|
42
|
+
{"indexed": True, "internalType": "address", "name": "recipient", "type": "address"},
|
43
|
+
{"indexed": False, "internalType": "address", "name": "token", "type": "address"},
|
44
|
+
{"indexed": False, "internalType": "uint256", "name": "deposit", "type": "uint256"},
|
45
|
+
{"indexed": False, "internalType": "uint256", "name": "ratePerSecond", "type": "uint256"},
|
46
|
+
{"indexed": False, "internalType": "uint256", "name": "startTime", "type": "uint256"},
|
47
|
+
{"indexed": False, "internalType": "uint256", "name": "stopTime", "type": "uint256"},
|
48
|
+
],
|
49
|
+
"name": "StreamCreated",
|
50
|
+
"type": "event",
|
51
|
+
},
|
52
|
+
]
|
53
|
+
|
54
|
+
ERC20_ABI = [
|
55
|
+
{
|
56
|
+
"name": "approve",
|
57
|
+
"type": "function",
|
58
|
+
"stateMutability": "nonpayable",
|
59
|
+
"inputs": [
|
60
|
+
{"name": "spender", "type": "address"},
|
61
|
+
{"name": "amount", "type": "uint256"},
|
62
|
+
],
|
63
|
+
"outputs": [{"name": "", "type": "bool"}],
|
64
|
+
}
|
65
|
+
]
|
66
|
+
|
67
|
+
|
68
|
+
@dataclass
|
69
|
+
class StreamPaymentConfig:
|
70
|
+
rpc_url: str
|
71
|
+
contract_address: str
|
72
|
+
glm_token_address: str
|
73
|
+
private_key: str
|
74
|
+
|
75
|
+
|
76
|
+
class StreamPaymentClient:
|
77
|
+
def __init__(self, cfg: StreamPaymentConfig):
|
78
|
+
self.web3 = Web3(Web3.HTTPProvider(cfg.rpc_url))
|
79
|
+
self.account = Account.from_key(cfg.private_key)
|
80
|
+
self.web3.eth.default_account = self.account.address
|
81
|
+
|
82
|
+
self.contract = self.web3.eth.contract(
|
83
|
+
address=Web3.to_checksum_address(cfg.contract_address), abi=STREAM_PAYMENT_ABI
|
84
|
+
)
|
85
|
+
self.erc20 = self.web3.eth.contract(
|
86
|
+
address=Web3.to_checksum_address(cfg.glm_token_address), abi=ERC20_ABI
|
87
|
+
)
|
88
|
+
|
89
|
+
def _send(self, fn) -> Dict[str, Any]:
|
90
|
+
tx = fn.build_transaction(
|
91
|
+
{
|
92
|
+
"from": self.account.address,
|
93
|
+
"nonce": self.web3.eth.get_transaction_count(self.account.address),
|
94
|
+
}
|
95
|
+
)
|
96
|
+
# In production, sign and send raw; in tests, Account may be a dummy without signer
|
97
|
+
if hasattr(self.account, "sign_transaction"):
|
98
|
+
signed = self.account.sign_transaction(tx)
|
99
|
+
tx_hash = self.web3.eth.send_raw_transaction(signed.rawTransaction)
|
100
|
+
else:
|
101
|
+
tx_hash = self.web3.eth.send_transaction(tx)
|
102
|
+
receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
|
103
|
+
return {"transactionHash": tx_hash.hex(), "status": receipt.status, "logs": receipt.logs}
|
104
|
+
|
105
|
+
def create_stream(self, provider_address: str, deposit_wei: int, rate_per_second_wei: int) -> int:
|
106
|
+
# 1) Approve deposit for the StreamPayment contract
|
107
|
+
approve = self.erc20.functions.approve(self.contract.address, int(deposit_wei))
|
108
|
+
self._send(approve)
|
109
|
+
|
110
|
+
# 2) Create stream
|
111
|
+
fn = self.contract.functions.createStream(
|
112
|
+
self.erc20.address,
|
113
|
+
Web3.to_checksum_address(provider_address),
|
114
|
+
int(deposit_wei),
|
115
|
+
int(rate_per_second_wei),
|
116
|
+
)
|
117
|
+
receipt = self._send(fn)
|
118
|
+
|
119
|
+
# Try to parse StreamCreated event for streamId
|
120
|
+
try:
|
121
|
+
for log in receipt["logs"]:
|
122
|
+
# very naive filter: topic0 = keccak256(StreamCreated(...))
|
123
|
+
# When ABI is attached to contract, use contract.events
|
124
|
+
ev = self.contract.events.StreamCreated().process_log(log)
|
125
|
+
return int(ev["args"]["streamId"])
|
126
|
+
except Exception:
|
127
|
+
pass
|
128
|
+
# As a fallback, cannot easily fetch return value from a tx; caller should query later
|
129
|
+
raise RuntimeError("create_stream: could not parse streamId from receipt")
|
130
|
+
|
131
|
+
def withdraw(self, stream_id: int) -> str:
|
132
|
+
fn = self.contract.functions.withdraw(int(stream_id))
|
133
|
+
receipt = self._send(fn)
|
134
|
+
return receipt["transactionHash"]
|
135
|
+
|
136
|
+
def terminate(self, stream_id: int) -> str:
|
137
|
+
fn = self.contract.functions.terminate(int(stream_id))
|
138
|
+
receipt = self._send(fn)
|
139
|
+
return receipt["transactionHash"]
|
140
|
+
|
141
|
+
def top_up(self, stream_id: int, amount_wei: int) -> str:
|
142
|
+
# Approve first
|
143
|
+
approve = self.erc20.functions.approve(self.contract.address, int(amount_wei))
|
144
|
+
self._send(approve)
|
145
|
+
# Top up
|
146
|
+
fn = self.contract.functions.topUp(int(stream_id), int(amount_wei))
|
147
|
+
receipt = self._send(fn)
|
148
|
+
return receipt["transactionHash"]
|
@@ -21,26 +21,37 @@ class ProviderClient:
|
|
21
21
|
cpu: int,
|
22
22
|
memory: int,
|
23
23
|
storage: int,
|
24
|
-
ssh_key: str
|
24
|
+
ssh_key: str,
|
25
|
+
stream_id: int | None = None,
|
25
26
|
) -> Dict:
|
26
27
|
"""Create a VM on the provider."""
|
28
|
+
payload = {
|
29
|
+
"name": name,
|
30
|
+
"resources": {
|
31
|
+
"cpu": cpu,
|
32
|
+
"memory": memory,
|
33
|
+
"storage": storage
|
34
|
+
},
|
35
|
+
"ssh_key": ssh_key
|
36
|
+
}
|
37
|
+
if stream_id is not None:
|
38
|
+
payload["stream_id"] = int(stream_id)
|
27
39
|
async with self.session.post(
|
28
40
|
f"{self.provider_url}/api/v1/vms",
|
29
|
-
json=
|
30
|
-
"name": name,
|
31
|
-
"resources": {
|
32
|
-
"cpu": cpu,
|
33
|
-
"memory": memory,
|
34
|
-
"storage": storage
|
35
|
-
},
|
36
|
-
"ssh_key": ssh_key
|
37
|
-
}
|
41
|
+
json=payload
|
38
42
|
) as response:
|
39
43
|
if not response.ok:
|
40
44
|
error_text = await response.text()
|
41
45
|
raise Exception(f"Failed to create VM: {error_text}")
|
42
46
|
return await response.json()
|
43
47
|
|
48
|
+
async def get_provider_info(self) -> Dict:
|
49
|
+
async with self.session.get(f"{self.provider_url}/api/v1/provider/info") as response:
|
50
|
+
if not response.ok:
|
51
|
+
error_text = await response.text()
|
52
|
+
raise Exception(f"Failed to fetch provider info: {error_text}")
|
53
|
+
return await response.json()
|
54
|
+
|
44
55
|
async def add_ssh_key(self, vm_id: str, key: str) -> None:
|
45
56
|
"""Add SSH key to VM."""
|
46
57
|
async with self.session.post(
|
@@ -14,11 +14,13 @@ class VMService:
|
|
14
14
|
self,
|
15
15
|
db_service: DatabaseService,
|
16
16
|
ssh_service: SSHService,
|
17
|
-
provider_client: Optional[ProviderClient] = None
|
17
|
+
provider_client: Optional[ProviderClient] = None,
|
18
|
+
blockchain_client: Optional[object] = None,
|
18
19
|
):
|
19
20
|
self.db = db_service
|
20
21
|
self.ssh_service = ssh_service
|
21
22
|
self.provider_client = provider_client
|
23
|
+
self.blockchain_client = blockchain_client
|
22
24
|
|
23
25
|
async def create_vm(
|
24
26
|
self,
|
@@ -27,7 +29,8 @@ class VMService:
|
|
27
29
|
memory: int,
|
28
30
|
storage: int,
|
29
31
|
provider_ip: str,
|
30
|
-
ssh_key: str
|
32
|
+
ssh_key: str,
|
33
|
+
stream_id: int | None = None,
|
31
34
|
) -> Dict:
|
32
35
|
"""Create a new VM with validation and error handling."""
|
33
36
|
try:
|
@@ -42,18 +45,42 @@ class VMService:
|
|
42
45
|
cpu=cpu,
|
43
46
|
memory=memory,
|
44
47
|
storage=storage,
|
45
|
-
ssh_key=ssh_key
|
48
|
+
ssh_key=ssh_key,
|
49
|
+
stream_id=stream_id
|
46
50
|
)
|
47
51
|
|
48
52
|
# Get VM access info
|
49
53
|
access_info = await self.provider_client.get_vm_access(vm['id'])
|
50
54
|
|
55
|
+
# Optionally create a GLM stream (if configured)
|
56
|
+
from ..config import config as app_config
|
57
|
+
stream_id = None
|
58
|
+
try:
|
59
|
+
if (
|
60
|
+
self.blockchain_client
|
61
|
+
and app_config.stream_payment_address != "0x0000000000000000000000000000000000000000"
|
62
|
+
and app_config.glm_token_address != "0x0000000000000000000000000000000000000000"
|
63
|
+
and app_config.provider_eth_address
|
64
|
+
):
|
65
|
+
# Simple heuristic: deposit for 1 hour at a nominal rate
|
66
|
+
rate_per_second = 10**18 # 1 GLM / second (example)
|
67
|
+
deposit = rate_per_second * 3600 # 1 hour worth
|
68
|
+
stream_id = self.blockchain_client.create_stream(
|
69
|
+
app_config.provider_eth_address,
|
70
|
+
deposit,
|
71
|
+
rate_per_second,
|
72
|
+
)
|
73
|
+
except Exception:
|
74
|
+
# Do not fail VM creation if streaming setup fails
|
75
|
+
stream_id = None
|
76
|
+
|
51
77
|
# Save VM details to database
|
52
78
|
config = {
|
53
79
|
'cpu': cpu,
|
54
80
|
'memory': memory,
|
55
81
|
'storage': storage,
|
56
|
-
'ssh_port': access_info['ssh_port']
|
82
|
+
'ssh_port': access_info['ssh_port'],
|
83
|
+
**({"stream_id": stream_id} if stream_id is not None else {}),
|
57
84
|
}
|
58
85
|
await self.db.save_vm(
|
59
86
|
name=name,
|
@@ -88,6 +115,15 @@ class VMService:
|
|
88
115
|
if "Not Found" not in str(e):
|
89
116
|
raise
|
90
117
|
|
118
|
+
# Attempt to terminate stream if present
|
119
|
+
try:
|
120
|
+
stream_id = vm.get('config', {}).get('stream_id')
|
121
|
+
if stream_id is not None and self.blockchain_client:
|
122
|
+
self.blockchain_client.terminate(stream_id)
|
123
|
+
except Exception:
|
124
|
+
# Best-effort: do not block deletion on chain failure
|
125
|
+
pass
|
126
|
+
|
91
127
|
# Remove from database
|
92
128
|
await self.db.delete_vm(name)
|
93
129
|
|
@@ -125,6 +161,14 @@ class VMService:
|
|
125
161
|
# Update status in database
|
126
162
|
await self.db.update_vm_status(name, "stopped")
|
127
163
|
|
164
|
+
# Best-effort withdraw on stop
|
165
|
+
try:
|
166
|
+
stream_id = vm.get('config', {}).get('stream_id')
|
167
|
+
if stream_id is not None and self.blockchain_client:
|
168
|
+
self.blockchain_client.withdraw(stream_id)
|
169
|
+
except Exception:
|
170
|
+
pass
|
171
|
+
|
128
172
|
except Exception as e:
|
129
173
|
raise VMError(f"Failed to stop VM: {str(e)}")
|
130
174
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/database_service.py
RENAMED
File without changes
|
{request_vm_on_golem-0.1.40 → request_vm_on_golem-0.1.41}/requestor/services/provider_service.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|