request-vm-on-golem 0.1.39__py3-none-any.whl → 0.1.41__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.
- {request_vm_on_golem-0.1.39.dist-info → request_vm_on_golem-0.1.41.dist-info}/METADATA +61 -3
- {request_vm_on_golem-0.1.39.dist-info → request_vm_on_golem-0.1.41.dist-info}/RECORD +11 -10
- requestor/cli/commands.py +117 -8
- requestor/config.py +18 -0
- requestor/payments/blockchain_service.py +148 -0
- requestor/provider/client.py +21 -10
- requestor/services/provider_service.py +93 -2
- requestor/services/vm_service.py +49 -5
- requestor/ssh/manager.py +10 -2
- {request_vm_on_golem-0.1.39.dist-info → request_vm_on_golem-0.1.41.dist-info}/WHEEL +0 -0
- {request_vm_on_golem-0.1.39.dist-info → request_vm_on_golem-0.1.41.dist-info}/entry_points.txt +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
|
@@ -175,7 +233,7 @@ Example output:
|
|
175
233
|
────────────────────────────────────────────────
|
176
234
|
🌍 Available Providers (3 total)
|
177
235
|
────────────────────────────────────────────────
|
178
|
-
Provider ID Country CPU Memory
|
236
|
+
Provider ID Country CPU Memory Disk
|
179
237
|
provider-1 🌍 SE 💻 4 🧠 8GB 💾 40GB
|
180
238
|
provider-2 🌍 US 💻 8 🧠 16GB 💾 80GB
|
181
239
|
provider-3 🌍 DE 💻 2 🧠 4GB 💾 20GB
|
@@ -206,7 +264,7 @@ Example output:
|
|
206
264
|
VM Details
|
207
265
|
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
208
266
|
🏷️ Name : my-webserver
|
209
|
-
💻 Resources : 2 CPU, 4GB RAM, 20GB
|
267
|
+
💻 Resources : 2 CPU, 4GB RAM, 20GB Disk
|
210
268
|
🟢 Status : running
|
211
269
|
|
212
270
|
Connection Details
|
@@ -1,24 +1,25 @@
|
|
1
1
|
requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
|
2
2
|
requestor/api/main.py,sha256=7utCzFNbh5Ol-vsBWeSwT4lXeHD7zdA-GFZuS3rHMWc,2180
|
3
3
|
requestor/cli/__init__.py,sha256=e3E4oEGxmGj-STPtFkQwg_qIWhR0JAiAQdw3G1hXciU,37
|
4
|
-
requestor/cli/commands.py,sha256=
|
5
|
-
requestor/config.py,sha256=
|
4
|
+
requestor/cli/commands.py,sha256=b9_Vy30rf3hpoBb_9Gu9z4mPWF3_oBjUjd-5pX2bVvU,33277
|
5
|
+
requestor/config.py,sha256=suWN2-4AL0Pphu2t-_WiTKIFG-0_cJjWwR_CIOW1c24,5320
|
6
6
|
requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
|
7
7
|
requestor/db/sqlite.py,sha256=l5pWbx2qlHuar1N_a0B9tVnmumLJY1w5rp3yZ7jmsC0,4146
|
8
8
|
requestor/errors.py,sha256=wVpHBuYgQx5pTe_SamugfK-k768noikY1RxvPOjQGko,665
|
9
|
+
requestor/payments/blockchain_service.py,sha256=w9ONdKVf2E7BCEgDRiTeEqwdNTr8SqZiXHKdV5JH2ZQ,5757
|
9
10
|
requestor/provider/__init__.py,sha256=fmW23aYUVciF8-gmBZkG-PLhn22upmcDzdPfAOLHG6g,103
|
10
|
-
requestor/provider/client.py,sha256=
|
11
|
+
requestor/provider/client.py,sha256=WXCm-1bytcgsuHEZzpg7RjjDOTuaXC9cj0Mrm7e6DSw,3676
|
11
12
|
requestor/run.py,sha256=GqOG6n34szt8Sp3SEqjRV6huWm737yCN6YnBqoiwI-U,1785
|
12
13
|
requestor/services/__init__.py,sha256=1qSn_6RMn0KB0A7LCnY2IW6_tC3HBQsdfkFeV-h94eM,172
|
13
14
|
requestor/services/database_service.py,sha256=GlSrzzzd7PSYQJNup00sxkB-B2PMr1__04K8k5QSWvs,2996
|
14
|
-
requestor/services/provider_service.py,sha256=
|
15
|
+
requestor/services/provider_service.py,sha256=auUc5XSHWbtOzyLHFqJ4RF337rCNYThzb7TjvUaK_uo,14542
|
15
16
|
requestor/services/ssh_service.py,sha256=tcOCtk2SlB9Uuv-P2ghR22e7BJ9kigQh5b4zSGdPFns,4280
|
16
|
-
requestor/services/vm_service.py,sha256=
|
17
|
+
requestor/services/vm_service.py,sha256=UPPj7aD0b7nCyNH6I-b35xdsqAkRUiQlpQBVGK4B7ms,8858
|
17
18
|
requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
|
18
|
-
requestor/ssh/manager.py,sha256=
|
19
|
+
requestor/ssh/manager.py,sha256=3jQtbbK7CVC2yD1zCO88jGXh2fBcuv3CzWEqDLuaQVk,9758
|
19
20
|
requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
|
20
21
|
requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
|
21
|
-
request_vm_on_golem-0.1.
|
22
|
-
request_vm_on_golem-0.1.
|
23
|
-
request_vm_on_golem-0.1.
|
24
|
-
request_vm_on_golem-0.1.
|
22
|
+
request_vm_on_golem-0.1.41.dist-info/METADATA,sha256=znwTKJHh21R9DsDgeHeaKFwxZFFC95vOTj-kScsc2Tc,11924
|
23
|
+
request_vm_on_golem-0.1.41.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
24
|
+
request_vm_on_golem-0.1.41.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
|
25
|
+
request_vm_on_golem-0.1.41.dist-info/RECORD,,
|
requestor/cli/commands.py
CHANGED
@@ -90,7 +90,7 @@ def vm():
|
|
90
90
|
@vm.command(name='providers')
|
91
91
|
@click.option('--cpu', type=int, help='Minimum CPU cores required')
|
92
92
|
@click.option('--memory', type=int, help='Minimum memory (GB) required')
|
93
|
-
@click.option('--storage', type=int, help='Minimum
|
93
|
+
@click.option('--storage', type=int, help='Minimum disk (GB) required')
|
94
94
|
@click.option('--country', help='Preferred provider country')
|
95
95
|
@click.option('--driver', type=click.Choice(['central', 'golem-base']), default=None, help='Discovery driver to use')
|
96
96
|
@click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
|
@@ -106,7 +106,7 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
106
106
|
if memory:
|
107
107
|
logger.detail(f"Memory: {memory}GB+")
|
108
108
|
if storage:
|
109
|
-
logger.detail(f"
|
109
|
+
logger.detail(f"Disk: {storage}GB+")
|
110
110
|
if country:
|
111
111
|
logger.detail(f"Country: {country}")
|
112
112
|
|
@@ -117,6 +117,9 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
117
117
|
# Initialize provider service
|
118
118
|
provider_service = ProviderService()
|
119
119
|
async with provider_service:
|
120
|
+
# If a full spec is provided, enable per-provider estimate display
|
121
|
+
if cpu and memory and storage:
|
122
|
+
provider_service.estimate_spec = (cpu, memory, storage)
|
120
123
|
providers = await provider_service.find_providers(
|
121
124
|
cpu=cpu,
|
122
125
|
memory=memory,
|
@@ -129,6 +132,12 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
129
132
|
logger.warning("No providers found matching criteria")
|
130
133
|
return {"providers": []}
|
131
134
|
|
135
|
+
# If JSON requested and full spec provided, include estimates per provider
|
136
|
+
if as_json and cpu and memory and storage:
|
137
|
+
for p in providers:
|
138
|
+
est = provider_service.compute_estimate(p, (cpu, memory, storage))
|
139
|
+
if est is not None:
|
140
|
+
p['estimate'] = est
|
132
141
|
result = {"providers": providers}
|
133
142
|
|
134
143
|
if as_json:
|
@@ -163,9 +172,11 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
163
172
|
@click.option('--provider-id', required=True, help='Provider ID to use')
|
164
173
|
@click.option('--cpu', type=int, required=True, help='Number of CPU cores')
|
165
174
|
@click.option('--memory', type=int, required=True, help='Memory in GB')
|
166
|
-
@click.option('--storage', type=int, required=True, help='
|
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')
|
177
|
+
@click.option('--yes', is_flag=True, help='Do not prompt for confirmation')
|
167
178
|
@async_command
|
168
|
-
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int):
|
179
|
+
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, stream_id: int | None, yes: bool):
|
169
180
|
"""Create a new VM on a specific provider."""
|
170
181
|
try:
|
171
182
|
# Show configuration details
|
@@ -173,7 +184,7 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
173
184
|
click.echo(click.style(" VM Configuration", fg="blue", bold=True))
|
174
185
|
click.echo("─" * 60)
|
175
186
|
click.echo(f" Provider : {click.style(provider_id, fg='cyan')}")
|
176
|
-
click.echo(f" Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB
|
187
|
+
click.echo(f" Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB Disk', fg='cyan')}")
|
177
188
|
click.echo("─" * 60 + "\n")
|
178
189
|
|
179
190
|
# Now start the deployment with spinner
|
@@ -191,6 +202,21 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
191
202
|
if not provider_ip and config.environment == "production":
|
192
203
|
raise RequestorError("Provider IP address not found in advertisement")
|
193
204
|
|
205
|
+
# Before proceeding, show estimated monthly price and confirm
|
206
|
+
provider_service.estimate_spec = (cpu, memory, storage)
|
207
|
+
est_row = await provider_service.format_provider_row(provider, colorize=False)
|
208
|
+
# Columns: ... [7]=USD/core/mo, [8]=USD/GB RAM/mo, [9]=USD/GB Disk/mo, [10]=Est. $/mo, [11]=Est. GLM/mo
|
209
|
+
est_usd = est_row[10]
|
210
|
+
est_glm = est_row[11]
|
211
|
+
price_str = f"~${est_usd}/mo" if est_usd != '—' else "(no USD pricing)"
|
212
|
+
if est_glm != '—':
|
213
|
+
price_str += f" (~{est_glm} GLM/mo)"
|
214
|
+
click.echo(click.style(f" 💵 Estimated Monthly Cost: {price_str}", fg='yellow', bold=True))
|
215
|
+
if not yes:
|
216
|
+
if not click.confirm("Proceed with VM creation?", default=True):
|
217
|
+
logger.warning("Creation cancelled by user")
|
218
|
+
return
|
219
|
+
|
194
220
|
# Setup SSH
|
195
221
|
ssh_service = SSHService(config.ssh_key_dir)
|
196
222
|
key_pair = await ssh_service.get_key_pair()
|
@@ -207,7 +233,8 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
207
233
|
memory=memory,
|
208
234
|
storage=storage,
|
209
235
|
provider_ip=provider_ip,
|
210
|
-
ssh_key=key_pair.public_key_content
|
236
|
+
ssh_key=key_pair.public_key_content,
|
237
|
+
stream_id=stream_id
|
211
238
|
)
|
212
239
|
|
213
240
|
# Get access info from config
|
@@ -222,7 +249,7 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
222
249
|
click.echo(click.style(" VM Details", fg="blue", bold=True))
|
223
250
|
click.echo(" " + "┈" * 25)
|
224
251
|
click.echo(f" 🏷️ Name : {click.style(name, fg='cyan')}")
|
225
|
-
click.echo(f" 💻 Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB
|
252
|
+
click.echo(f" 💻 Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB Disk', fg='cyan')}")
|
226
253
|
click.echo(f" 🟢 Status : {click.style('running', fg='green')}")
|
227
254
|
|
228
255
|
# Connection Details Section
|
@@ -256,6 +283,88 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
256
283
|
raise click.Abort()
|
257
284
|
|
258
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
|
+
|
259
368
|
@vm.command(name='ssh')
|
260
369
|
@click.argument('name')
|
261
370
|
@async_command
|
@@ -341,7 +450,7 @@ async def info_vm(name: str, as_json: bool):
|
|
341
450
|
"SSH Port",
|
342
451
|
"CPU",
|
343
452
|
"Memory (GB)",
|
344
|
-
"
|
453
|
+
"Disk (GB)",
|
345
454
|
]
|
346
455
|
|
347
456
|
row = [
|
requestor/config.py
CHANGED
@@ -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"]
|
requestor/provider/client.py
CHANGED
@@ -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(
|
@@ -15,6 +15,39 @@ class ProviderService:
|
|
15
15
|
def __init__(self):
|
16
16
|
self.session = None
|
17
17
|
self.golem_base_client = None
|
18
|
+
# Optional spec (cpu, memory, storage) to compute estimates for display
|
19
|
+
self.estimate_spec: Optional[tuple[int, int, int]] = None
|
20
|
+
|
21
|
+
def compute_estimate(self, provider: Dict, spec: tuple[int, int, int]) -> Optional[Dict]:
|
22
|
+
"""Compute estimated pricing for a given spec, if provider has pricing.
|
23
|
+
|
24
|
+
Returns dict with usd_per_month, glm_per_month (if GLM per-unit available),
|
25
|
+
and usd_per_hour, or None if insufficient pricing data.
|
26
|
+
"""
|
27
|
+
pricing = provider.get('pricing') or {}
|
28
|
+
usd_core = pricing.get('usd_per_core_month')
|
29
|
+
usd_ram = pricing.get('usd_per_gb_ram_month')
|
30
|
+
usd_storage = pricing.get('usd_per_gb_storage_month')
|
31
|
+
if usd_core is None or usd_ram is None or usd_storage is None:
|
32
|
+
return None
|
33
|
+
cpu, mem, sto = spec
|
34
|
+
try:
|
35
|
+
usd_per_month = float(usd_core) * cpu + float(usd_ram) * mem + float(usd_storage) * sto
|
36
|
+
glm_core = pricing.get('glm_per_core_month')
|
37
|
+
glm_ram = pricing.get('glm_per_gb_ram_month')
|
38
|
+
glm_storage = pricing.get('glm_per_gb_storage_month')
|
39
|
+
glm_per_month = None
|
40
|
+
if glm_core is not None and glm_ram is not None and glm_storage is not None:
|
41
|
+
glm_per_month = float(glm_core) * cpu + float(glm_ram) * mem + float(glm_storage) * sto
|
42
|
+
usd_per_hour = usd_per_month / 730.0
|
43
|
+
# Round for display consistency
|
44
|
+
return {
|
45
|
+
'usd_per_month': round(usd_per_month, 4),
|
46
|
+
'usd_per_hour': round(usd_per_hour, 6),
|
47
|
+
'glm_per_month': round(glm_per_month, 8) if glm_per_month is not None else None,
|
48
|
+
}
|
49
|
+
except Exception:
|
50
|
+
return None
|
18
51
|
|
19
52
|
async def __aenter__(self):
|
20
53
|
self.session = aiohttp.ClientSession()
|
@@ -91,6 +124,14 @@ class ProviderService:
|
|
91
124
|
'memory': int(annotations.get('golem_memory', 0)),
|
92
125
|
'storage': int(annotations.get('golem_storage', 0)),
|
93
126
|
},
|
127
|
+
'pricing': {
|
128
|
+
'usd_per_core_month': annotations.get('golem_price_usd_core_month'),
|
129
|
+
'usd_per_gb_ram_month': annotations.get('golem_price_usd_ram_gb_month'),
|
130
|
+
'usd_per_gb_storage_month': annotations.get('golem_price_usd_storage_gb_month'),
|
131
|
+
'glm_per_core_month': annotations.get('golem_price_glm_core_month'),
|
132
|
+
'glm_per_gb_ram_month': annotations.get('golem_price_glm_ram_gb_month'),
|
133
|
+
'glm_per_gb_storage_month': annotations.get('golem_price_glm_storage_gb_month'),
|
134
|
+
},
|
94
135
|
'created_at_block': metadata.expires_at_block - (config.advertisement_interval * 2)
|
95
136
|
}
|
96
137
|
if provider['provider_id']:
|
@@ -225,6 +266,31 @@ class ProviderService:
|
|
225
266
|
|
226
267
|
updated_at_str = await self._format_block_timestamp(provider.get('created_at_block', 0))
|
227
268
|
|
269
|
+
pricing = provider.get('pricing') or {}
|
270
|
+
usd_core = pricing.get('usd_per_core_month')
|
271
|
+
usd_ram = pricing.get('usd_per_gb_ram_month')
|
272
|
+
usd_storage = pricing.get('usd_per_gb_storage_month')
|
273
|
+
|
274
|
+
# Precompute estimates if a spec is set and pricing available
|
275
|
+
est_usd = '—'
|
276
|
+
est_glm = '—'
|
277
|
+
est_hr_usd = '—'
|
278
|
+
if self.estimate_spec and all(p is not None for p in (usd_core, usd_ram, usd_storage)):
|
279
|
+
spec_cpu, spec_mem, spec_sto = self.estimate_spec
|
280
|
+
try:
|
281
|
+
est_usd_val = (float(usd_core) * spec_cpu) + (float(usd_ram) * spec_mem) + (float(usd_storage) * spec_sto)
|
282
|
+
est_usd = round(est_usd_val, 4)
|
283
|
+
est_hr_usd = round(est_usd_val / 730.0, 6)
|
284
|
+
# If GLM per-unit is present, compute GLM estimate as well
|
285
|
+
glm_core = pricing.get('glm_per_core_month')
|
286
|
+
glm_ram = pricing.get('glm_per_gb_ram_month')
|
287
|
+
glm_storage = pricing.get('glm_per_gb_storage_month')
|
288
|
+
if all(x is not None for x in (glm_core, glm_ram, glm_storage)):
|
289
|
+
est_glm_val = (float(glm_core) * spec_cpu) + (float(glm_ram) * spec_mem) + (float(glm_storage) * spec_sto)
|
290
|
+
est_glm = round(est_glm_val, 8)
|
291
|
+
except Exception:
|
292
|
+
pass
|
293
|
+
|
228
294
|
row = [
|
229
295
|
provider['provider_id'],
|
230
296
|
provider['provider_name'],
|
@@ -233,18 +299,38 @@ class ProviderService:
|
|
233
299
|
provider['resources']['cpu'],
|
234
300
|
provider['resources']['memory'],
|
235
301
|
provider['resources']['storage'],
|
302
|
+
usd_core if usd_core is not None else '—',
|
303
|
+
usd_ram if usd_ram is not None else '—',
|
304
|
+
usd_storage if usd_storage is not None else '—',
|
305
|
+
est_usd,
|
306
|
+
est_glm,
|
236
307
|
updated_at_str
|
237
308
|
]
|
238
309
|
|
239
310
|
if colorize:
|
240
311
|
# Format Provider ID
|
241
|
-
|
312
|
+
id_txt = style(row[0], fg="yellow")
|
313
|
+
if est_hr_usd != '—':
|
314
|
+
id_txt += style(f" (~${est_hr_usd}/hr)", fg="yellow")
|
315
|
+
row[0] = id_txt
|
242
316
|
|
243
317
|
# Format resources with icons and colors
|
244
318
|
row[4] = style(f"💻 {row[4]}", fg="cyan", bold=True)
|
245
319
|
row[5] = style(f"🧠 {row[5]}", fg="cyan", bold=True)
|
246
320
|
row[6] = style(f"💾 {row[6]}", fg="cyan", bold=True)
|
247
321
|
|
322
|
+
# Format pricing with currency markers
|
323
|
+
if usd_core != '—':
|
324
|
+
row[7] = style(f"${row[7]}/mo", fg="magenta")
|
325
|
+
if usd_ram != '—':
|
326
|
+
row[8] = style(f"${row[8]}/GB/mo", fg="magenta")
|
327
|
+
if usd_storage != '—':
|
328
|
+
row[9] = style(f"${row[9]}/GB/mo", fg="magenta")
|
329
|
+
if est_usd != '—':
|
330
|
+
row[10] = style(f"~${row[10]}/mo", fg="yellow", bold=True)
|
331
|
+
if est_glm != '—':
|
332
|
+
row[11] = style(f"~{row[11]} GLM/mo", fg="yellow")
|
333
|
+
|
248
334
|
# Format location info
|
249
335
|
row[3] = style(f"🌍 {row[3]}", fg="green", bold=True)
|
250
336
|
|
@@ -260,6 +346,11 @@ class ProviderService:
|
|
260
346
|
"Country",
|
261
347
|
"CPU",
|
262
348
|
"Memory (GB)",
|
263
|
-
"
|
349
|
+
"Disk (GB)",
|
350
|
+
"USD/core/mo",
|
351
|
+
"USD/GB RAM/mo",
|
352
|
+
"USD/GB Disk/mo",
|
353
|
+
"Est. $/mo",
|
354
|
+
"Est. GLM/mo",
|
264
355
|
"Updated"
|
265
356
|
]
|
requestor/services/vm_service.py
CHANGED
@@ -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
|
|
@@ -195,7 +239,7 @@ class VMService:
|
|
195
239
|
"SSH Port",
|
196
240
|
"CPU",
|
197
241
|
"Memory (GB)",
|
198
|
-
"
|
242
|
+
"Disk (GB)",
|
199
243
|
"Connect Command",
|
200
244
|
"Created"
|
201
245
|
]
|
requestor/ssh/manager.py
CHANGED
@@ -53,9 +53,17 @@ class SSHKeyManager:
|
|
53
53
|
|
54
54
|
# Create Golem directory if needed
|
55
55
|
self.golem_dir.mkdir(parents=True, exist_ok=True)
|
56
|
-
# Secure directory permissions (on Unix-like systems)
|
56
|
+
# Secure directory permissions (on Unix-like systems). If the directory
|
57
|
+
# is a system path (e.g., "/tmp") or not owned/permission-changeable
|
58
|
+
# by the current user, ignore the error to avoid test and runtime failures.
|
57
59
|
if os.name == 'posix':
|
58
|
-
|
60
|
+
try:
|
61
|
+
os.chmod(self.golem_dir, 0o700)
|
62
|
+
except PermissionError:
|
63
|
+
logger.warning(
|
64
|
+
"Could not set permissions on %s; continuing without chmod",
|
65
|
+
self.golem_dir,
|
66
|
+
)
|
59
67
|
|
60
68
|
async def get_key_pair(self) -> KeyPair:
|
61
69
|
"""Get the SSH key pair to use.
|
File without changes
|
{request_vm_on_golem-0.1.39.dist-info → request_vm_on_golem-0.1.41.dist-info}/entry_points.txt
RENAMED
File without changes
|