request-vm-on-golem 0.1.39__tar.gz → 0.1.40__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.39 → request_vm_on_golem-0.1.40}/PKG-INFO +3 -3
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/README.md +2 -2
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/pyproject.toml +2 -1
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/cli/commands.py +32 -7
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/provider_service.py +93 -2
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/vm_service.py +1 -1
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/ssh/manager.py +10 -2
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/__init__.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/api/main.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/cli/__init__.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/config.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/db/__init__.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/db/sqlite.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/errors.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/provider/__init__.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/provider/client.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/run.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/__init__.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/database_service.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/ssh_service.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/ssh/__init__.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/utils/logging.py +0 -0
- {request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/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.40
|
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
|
@@ -175,7 +175,7 @@ Example output:
|
|
175
175
|
────────────────────────────────────────────────
|
176
176
|
🌍 Available Providers (3 total)
|
177
177
|
────────────────────────────────────────────────
|
178
|
-
Provider ID Country CPU Memory
|
178
|
+
Provider ID Country CPU Memory Disk
|
179
179
|
provider-1 🌍 SE 💻 4 🧠 8GB 💾 40GB
|
180
180
|
provider-2 🌍 US 💻 8 🧠 16GB 💾 80GB
|
181
181
|
provider-3 🌍 DE 💻 2 🧠 4GB 💾 20GB
|
@@ -206,7 +206,7 @@ Example output:
|
|
206
206
|
VM Details
|
207
207
|
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
208
208
|
🏷️ Name : my-webserver
|
209
|
-
💻 Resources : 2 CPU, 4GB RAM, 20GB
|
209
|
+
💻 Resources : 2 CPU, 4GB RAM, 20GB Disk
|
210
210
|
🟢 Status : running
|
211
211
|
|
212
212
|
Connection Details
|
@@ -136,7 +136,7 @@ Example output:
|
|
136
136
|
────────────────────────────────────────────────
|
137
137
|
🌍 Available Providers (3 total)
|
138
138
|
────────────────────────────────────────────────
|
139
|
-
Provider ID Country CPU Memory
|
139
|
+
Provider ID Country CPU Memory Disk
|
140
140
|
provider-1 🌍 SE 💻 4 🧠 8GB 💾 40GB
|
141
141
|
provider-2 🌍 US 💻 8 🧠 16GB 💾 80GB
|
142
142
|
provider-3 🌍 DE 💻 2 🧠 4GB 💾 20GB
|
@@ -167,7 +167,7 @@ Example output:
|
|
167
167
|
VM Details
|
168
168
|
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
169
169
|
🏷️ Name : my-webserver
|
170
|
-
💻 Resources : 2 CPU, 4GB RAM, 20GB
|
170
|
+
💻 Resources : 2 CPU, 4GB RAM, 20GB Disk
|
171
171
|
🟢 Status : running
|
172
172
|
|
173
173
|
Connection Details
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "request-vm-on-golem"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.40"
|
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"
|
@@ -44,6 +44,7 @@ web3 = "==7.13.0"
|
|
44
44
|
pytest = "^7.0.0"
|
45
45
|
pytest-asyncio = "^0.18.0"
|
46
46
|
pytest-cov = "^3.0.0"
|
47
|
+
httpx = "^0.23.0"
|
47
48
|
black = "^22.3.0"
|
48
49
|
isort = "^5.10.1"
|
49
50
|
mypy = "^0.950"
|
@@ -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,10 @@ 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('--yes', is_flag=True, help='Do not prompt for confirmation')
|
167
177
|
@async_command
|
168
|
-
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int):
|
178
|
+
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, yes: bool):
|
169
179
|
"""Create a new VM on a specific provider."""
|
170
180
|
try:
|
171
181
|
# Show configuration details
|
@@ -173,7 +183,7 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
173
183
|
click.echo(click.style(" VM Configuration", fg="blue", bold=True))
|
174
184
|
click.echo("─" * 60)
|
175
185
|
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
|
186
|
+
click.echo(f" Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB Disk', fg='cyan')}")
|
177
187
|
click.echo("─" * 60 + "\n")
|
178
188
|
|
179
189
|
# Now start the deployment with spinner
|
@@ -191,6 +201,21 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
191
201
|
if not provider_ip and config.environment == "production":
|
192
202
|
raise RequestorError("Provider IP address not found in advertisement")
|
193
203
|
|
204
|
+
# Before proceeding, show estimated monthly price and confirm
|
205
|
+
provider_service.estimate_spec = (cpu, memory, storage)
|
206
|
+
est_row = await provider_service.format_provider_row(provider, colorize=False)
|
207
|
+
# Columns: ... [7]=USD/core/mo, [8]=USD/GB RAM/mo, [9]=USD/GB Disk/mo, [10]=Est. $/mo, [11]=Est. GLM/mo
|
208
|
+
est_usd = est_row[10]
|
209
|
+
est_glm = est_row[11]
|
210
|
+
price_str = f"~${est_usd}/mo" if est_usd != '—' else "(no USD pricing)"
|
211
|
+
if est_glm != '—':
|
212
|
+
price_str += f" (~{est_glm} GLM/mo)"
|
213
|
+
click.echo(click.style(f" 💵 Estimated Monthly Cost: {price_str}", fg='yellow', bold=True))
|
214
|
+
if not yes:
|
215
|
+
if not click.confirm("Proceed with VM creation?", default=True):
|
216
|
+
logger.warning("Creation cancelled by user")
|
217
|
+
return
|
218
|
+
|
194
219
|
# Setup SSH
|
195
220
|
ssh_service = SSHService(config.ssh_key_dir)
|
196
221
|
key_pair = await ssh_service.get_key_pair()
|
@@ -222,7 +247,7 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
|
|
222
247
|
click.echo(click.style(" VM Details", fg="blue", bold=True))
|
223
248
|
click.echo(" " + "┈" * 25)
|
224
249
|
click.echo(f" 🏷️ Name : {click.style(name, fg='cyan')}")
|
225
|
-
click.echo(f" 💻 Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB
|
250
|
+
click.echo(f" 💻 Resources : {click.style(f'{cpu} CPU, {memory}GB RAM, {storage}GB Disk', fg='cyan')}")
|
226
251
|
click.echo(f" 🟢 Status : {click.style('running', fg='green')}")
|
227
252
|
|
228
253
|
# Connection Details Section
|
@@ -341,7 +366,7 @@ async def info_vm(name: str, as_json: bool):
|
|
341
366
|
"SSH Port",
|
342
367
|
"CPU",
|
343
368
|
"Memory (GB)",
|
344
|
-
"
|
369
|
+
"Disk (GB)",
|
345
370
|
]
|
346
371
|
|
347
372
|
row = [
|
{request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/provider_service.py
RENAMED
@@ -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
|
]
|
@@ -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
|
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
|
File without changes
|
{request_vm_on_golem-0.1.39 → request_vm_on_golem-0.1.40}/requestor/services/database_service.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|