request-vm-on-golem 0.1.50__py3-none-any.whl → 0.1.51__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.50.dist-info → request_vm_on_golem-0.1.51.dist-info}/METADATA +16 -1
- {request_vm_on_golem-0.1.50.dist-info → request_vm_on_golem-0.1.51.dist-info}/RECORD +9 -8
- requestor/api/main.py +9 -1
- requestor/cli/commands.py +30 -3
- requestor/config.py +17 -0
- requestor/payments/monitor.py +126 -0
- requestor/services/vm_service.py +2 -2
- {request_vm_on_golem-0.1.50.dist-info → request_vm_on_golem-0.1.51.dist-info}/WHEEL +0 -0
- {request_vm_on_golem-0.1.50.dist-info → request_vm_on_golem-0.1.51.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.51
|
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
|
@@ -171,6 +171,16 @@ poetry run golem vm stream status my-vm
|
|
171
171
|
poetry run golem vm stream inspect --stream-id 123
|
172
172
|
```
|
173
173
|
|
174
|
+
- Stopping or destroying a VM ends the stream:
|
175
|
+
|
176
|
+
```bash
|
177
|
+
# Stop VM and terminate payment stream (best-effort)
|
178
|
+
poetry run golem vm stop my-vm
|
179
|
+
|
180
|
+
# Destroy VM and terminate stream
|
181
|
+
poetry run golem vm destroy my-vm
|
182
|
+
```
|
183
|
+
|
174
184
|
- Create a VM and attach an existing stream (no auto-streams are created by the requestor):
|
175
185
|
|
176
186
|
```bash
|
@@ -195,6 +205,11 @@ Efficiency tips:
|
|
195
205
|
- Withdrawals are typically executed by providers; requestors don’t need to withdraw.
|
196
206
|
- The CLI `vm stream open` will prefer the provider’s advertised contract/token addresses to prevent mismatches.
|
197
207
|
|
208
|
+
Monitoring and auto top-up:
|
209
|
+
|
210
|
+
- The requestor API runs a background monitor that keeps each running VM’s stream funded with at least 1 hour runway (configurable). It checks every 30s and tops up to the target runway.
|
211
|
+
- Configure via env (prefix `GOLEM_REQUESTOR_`): `stream_monitor_enabled` (default true), `stream_monitor_interval_seconds` (default 30), `stream_min_remaining_seconds` (default 3600), `stream_topup_target_seconds` (default 3600).
|
212
|
+
|
198
213
|
## Faucet (L2 ETH)
|
199
214
|
|
200
215
|
- Request L2 test ETH to cover stream transactions:
|
@@ -1,13 +1,14 @@
|
|
1
1
|
requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
|
2
|
-
requestor/api/main.py,sha256=
|
2
|
+
requestor/api/main.py,sha256=CTnaM7KyBtDwVlyclYbNDy-nGi5_xt9GTcGusRasDVY,2493
|
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=N1Jj1Yy5AX-eOHLROlkBsm-CTXzn9--eOrB1dZZpCiA,47357
|
5
|
+
requestor/config.py,sha256=g6fhGxWmKxJMqkoaB1RDhMRPn45hqmuqIRmItBoInlg,8835
|
6
6
|
requestor/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
|
7
7
|
requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
|
8
8
|
requestor/db/sqlite.py,sha256=l5pWbx2qlHuar1N_a0B9tVnmumLJY1w5rp3yZ7jmsC0,4146
|
9
9
|
requestor/errors.py,sha256=wVpHBuYgQx5pTe_SamugfK-k768noikY1RxvPOjQGko,665
|
10
10
|
requestor/payments/blockchain_service.py,sha256=CACvZH2ZstutX7f0L_PXl8K_V5WlIkxNYIaeJuhP5I0,7500
|
11
|
+
requestor/payments/monitor.py,sha256=JtSnh2plFf-f8sJU-bkOpadhoK_R82_ULwkDRmBYSbc,6012
|
11
12
|
requestor/provider/__init__.py,sha256=fmW23aYUVciF8-gmBZkG-PLhn22upmcDzdPfAOLHG6g,103
|
12
13
|
requestor/provider/client.py,sha256=pfJymufYR13W4kfykHZSVvs6ikRUE5AdHp0W0DB17AE,4130
|
13
14
|
requestor/run.py,sha256=GqOG6n34szt8Sp3SEqjRV6huWm737yCN6YnBqoiwI-U,1785
|
@@ -16,12 +17,12 @@ requestor/services/__init__.py,sha256=1qSn_6RMn0KB0A7LCnY2IW6_tC3HBQsdfkFeV-h94e
|
|
16
17
|
requestor/services/database_service.py,sha256=GlSrzzzd7PSYQJNup00sxkB-B2PMr1__04K8k5QSWvs,2996
|
17
18
|
requestor/services/provider_service.py,sha256=eb4t6tkcw9VzJev2sfawT1KvVc5TxQnb1pgYgoQZcM4,15000
|
18
19
|
requestor/services/ssh_service.py,sha256=tcOCtk2SlB9Uuv-P2ghR22e7BJ9kigQh5b4zSGdPFns,4280
|
19
|
-
requestor/services/vm_service.py,sha256=
|
20
|
+
requestor/services/vm_service.py,sha256=1EUypRbCykdQTVJf4gYiNzkZNk66T3df32Vc51HkSMI,7983
|
20
21
|
requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
|
21
22
|
requestor/ssh/manager.py,sha256=3jQtbbK7CVC2yD1zCO88jGXh2fBcuv3CzWEqDLuaQVk,9758
|
22
23
|
requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
|
23
24
|
requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
|
24
|
-
request_vm_on_golem-0.1.
|
25
|
-
request_vm_on_golem-0.1.
|
26
|
-
request_vm_on_golem-0.1.
|
27
|
-
request_vm_on_golem-0.1.
|
25
|
+
request_vm_on_golem-0.1.51.dist-info/METADATA,sha256=0SYRZbBOrAmZyg_6wtjwvGAW0nc6EWcXShblUUvyRsA,15027
|
26
|
+
request_vm_on_golem-0.1.51.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
27
|
+
request_vm_on_golem-0.1.51.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
|
28
|
+
request_vm_on_golem-0.1.51.dist-info/RECORD,,
|
requestor/api/main.py
CHANGED
@@ -5,11 +5,13 @@ from contextlib import asynccontextmanager
|
|
5
5
|
from ..services.database_service import DatabaseService
|
6
6
|
from ..config import config
|
7
7
|
from ..errors import DatabaseError
|
8
|
+
from ..payments.monitor import RequestorStreamMonitor
|
8
9
|
|
9
10
|
logger = logging.getLogger(__name__)
|
10
11
|
|
11
12
|
# Global variable to hold the database service instance
|
12
13
|
db_service: DatabaseService = None
|
14
|
+
stream_monitor: RequestorStreamMonitor | None = None
|
13
15
|
|
14
16
|
@asynccontextmanager
|
15
17
|
async def lifespan(app: FastAPI):
|
@@ -26,9 +28,15 @@ async def lifespan(app: FastAPI):
|
|
26
28
|
logger.error(f"Failed to initialize database during startup: {e}")
|
27
29
|
# Depending on requirements, you might want to prevent the app from starting
|
28
30
|
# raise RuntimeError(f"Database initialization failed: {e}") from e
|
31
|
+
# Start requestor stream monitor
|
32
|
+
global stream_monitor
|
33
|
+
stream_monitor = RequestorStreamMonitor(db_service)
|
34
|
+
stream_monitor.start()
|
29
35
|
yield
|
30
36
|
# Shutdown: Cleanup (if needed)
|
31
37
|
logger.info("Shutting down API.")
|
38
|
+
if stream_monitor:
|
39
|
+
await stream_monitor.stop()
|
32
40
|
# No explicit cleanup needed for aiosqlite connection usually
|
33
41
|
|
34
42
|
app = FastAPI(lifespan=lifespan)
|
@@ -56,4 +64,4 @@ async def list_vms():
|
|
56
64
|
# Example of another endpoint (can be removed if not needed)
|
57
65
|
@app.get("/")
|
58
66
|
async def read_root():
|
59
|
-
return {"message": "Golem Requestor API"}
|
67
|
+
return {"message": "Golem Requestor API"}
|
requestor/cli/commands.py
CHANGED
@@ -743,7 +743,16 @@ async def destroy_vm(name: str):
|
|
743
743
|
# Initialize VM service
|
744
744
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
745
745
|
async with ProviderClient(provider_url) as client:
|
746
|
-
|
746
|
+
# Initialize blockchain client for stream termination on destroy
|
747
|
+
from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
748
|
+
spc = StreamPaymentConfig(
|
749
|
+
rpc_url=config.polygon_rpc_url,
|
750
|
+
contract_address=config.stream_payment_address,
|
751
|
+
glm_token_address=config.glm_token_address,
|
752
|
+
private_key=config.ethereum_private_key,
|
753
|
+
)
|
754
|
+
sp_client = StreamPaymentClient(spc)
|
755
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client, sp_client)
|
747
756
|
await vm_service.destroy_vm(name)
|
748
757
|
|
749
758
|
# Show fancy success message
|
@@ -798,7 +807,16 @@ async def purge_vms(force: bool):
|
|
798
807
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
799
808
|
|
800
809
|
async with ProviderClient(provider_url) as client:
|
801
|
-
|
810
|
+
# Initialize blockchain client for stream termination on purge
|
811
|
+
from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
812
|
+
spc = StreamPaymentConfig(
|
813
|
+
rpc_url=config.polygon_rpc_url,
|
814
|
+
contract_address=config.stream_payment_address,
|
815
|
+
glm_token_address=config.glm_token_address,
|
816
|
+
private_key=config.ethereum_private_key,
|
817
|
+
)
|
818
|
+
sp_client = StreamPaymentClient(spc)
|
819
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client, sp_client)
|
802
820
|
await vm_service.destroy_vm(vm['name'])
|
803
821
|
results['success'].append((vm['name'], 'Destroyed successfully'))
|
804
822
|
|
@@ -921,7 +939,16 @@ async def stop_vm(name: str):
|
|
921
939
|
# Initialize VM service
|
922
940
|
provider_url = config.get_provider_url(vm['provider_ip'])
|
923
941
|
async with ProviderClient(provider_url) as client:
|
924
|
-
|
942
|
+
# Initialize blockchain client for stream termination on stop
|
943
|
+
from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
944
|
+
spc = StreamPaymentConfig(
|
945
|
+
rpc_url=config.polygon_rpc_url,
|
946
|
+
contract_address=config.stream_payment_address,
|
947
|
+
glm_token_address=config.glm_token_address,
|
948
|
+
private_key=config.ethereum_private_key,
|
949
|
+
)
|
950
|
+
sp_client = StreamPaymentClient(spc)
|
951
|
+
vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client, sp_client)
|
925
952
|
await vm_service.stop_vm(name)
|
926
953
|
|
927
954
|
# Show fancy success message
|
requestor/config.py
CHANGED
@@ -113,6 +113,23 @@ class RequestorConfig(BaseSettings):
|
|
113
113
|
default="",
|
114
114
|
description="Token address (0x0 means native ETH). Defaults from l2.json"
|
115
115
|
)
|
116
|
+
# Stream monitor (auto top-up)
|
117
|
+
stream_monitor_enabled: bool = Field(
|
118
|
+
default=True,
|
119
|
+
description="Enable background monitor to auto top-up streams"
|
120
|
+
)
|
121
|
+
stream_monitor_interval_seconds: int = Field(
|
122
|
+
default=30,
|
123
|
+
description="How frequently to check and top up streams"
|
124
|
+
)
|
125
|
+
stream_min_remaining_seconds: int = Field(
|
126
|
+
default=3600,
|
127
|
+
description="Minimum remaining runway to maintain (seconds)"
|
128
|
+
)
|
129
|
+
stream_topup_target_seconds: int = Field(
|
130
|
+
default=3600,
|
131
|
+
description="Target runway after top-up (seconds)"
|
132
|
+
)
|
116
133
|
# Faucet settings (L2 payments)
|
117
134
|
l2_faucet_url: str = Field(
|
118
135
|
default="https://l2.holesky.golemdb.io/faucet",
|
@@ -0,0 +1,126 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from ..services.database_service import DatabaseService
|
5
|
+
from ..provider.client import ProviderClient
|
6
|
+
from ..config import config
|
7
|
+
from ..utils.logging import setup_logger
|
8
|
+
from .blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
9
|
+
|
10
|
+
|
11
|
+
class RequestorStreamMonitor:
|
12
|
+
def __init__(self, db: DatabaseService):
|
13
|
+
self.db = db
|
14
|
+
self._task: Optional[asyncio.Task] = None
|
15
|
+
self._logger = setup_logger(__name__)
|
16
|
+
self._sp = StreamPaymentClient(
|
17
|
+
StreamPaymentConfig(
|
18
|
+
rpc_url=config.polygon_rpc_url,
|
19
|
+
contract_address=config.stream_payment_address,
|
20
|
+
glm_token_address=config.glm_token_address,
|
21
|
+
private_key=config.ethereum_private_key,
|
22
|
+
)
|
23
|
+
)
|
24
|
+
|
25
|
+
def start(self):
|
26
|
+
if not config.stream_monitor_enabled:
|
27
|
+
return
|
28
|
+
self._logger.info(
|
29
|
+
f"⏱️ Requestor stream auto-topup enabled interval={config.stream_monitor_interval_seconds}s "
|
30
|
+
f"min_remaining={config.stream_min_remaining_seconds}s target={config.stream_topup_target_seconds}s"
|
31
|
+
)
|
32
|
+
self._task = asyncio.create_task(self._run(), name="requestor-stream-monitor")
|
33
|
+
|
34
|
+
async def stop(self):
|
35
|
+
if self._task:
|
36
|
+
self._task.cancel()
|
37
|
+
try:
|
38
|
+
await self._task
|
39
|
+
except asyncio.CancelledError:
|
40
|
+
pass
|
41
|
+
self._logger.info("Requestor stream auto-topup stopped")
|
42
|
+
|
43
|
+
async def _resolve_stream_id(self, vm: dict) -> Optional[int]:
|
44
|
+
# Prefer local DB recorded stream_id
|
45
|
+
sid = vm.get("config", {}).get("stream_id")
|
46
|
+
if isinstance(sid, int):
|
47
|
+
return sid
|
48
|
+
# Ask provider for mapping
|
49
|
+
try:
|
50
|
+
provider_url = config.get_provider_url(vm["provider_ip"])
|
51
|
+
async with ProviderClient(provider_url) as client:
|
52
|
+
status = await client.get_vm_stream_status(vm["vm_id"])
|
53
|
+
sid = status.get("stream_id")
|
54
|
+
self._logger.debug(f"Resolved stream for VM {vm['name']}: {sid}")
|
55
|
+
return int(sid) if sid is not None else None
|
56
|
+
except Exception as e:
|
57
|
+
self._logger.debug(f"Could not resolve stream for VM {vm['name']}: {e}")
|
58
|
+
return None
|
59
|
+
|
60
|
+
async def _run(self):
|
61
|
+
interval = max(int(config.stream_monitor_interval_seconds), 5)
|
62
|
+
min_remaining = max(int(config.stream_min_remaining_seconds), 0)
|
63
|
+
target_seconds = max(int(config.stream_topup_target_seconds), min_remaining)
|
64
|
+
while True:
|
65
|
+
try:
|
66
|
+
vms = await self.db.list_vms()
|
67
|
+
self._logger.debug(f"stream monitor tick: {len(vms)} VMs to check")
|
68
|
+
for vm in vms:
|
69
|
+
# Only manage running VMs
|
70
|
+
if vm.get("status") != "running":
|
71
|
+
self._logger.debug(f"skip VM {vm.get('name')} status={vm.get('status')}")
|
72
|
+
continue
|
73
|
+
stream_id = await self._resolve_stream_id(vm)
|
74
|
+
if stream_id is None:
|
75
|
+
self._logger.debug(f"skip VM {vm.get('name')} no stream mapped")
|
76
|
+
continue
|
77
|
+
# Read on-chain stream tuple via contract
|
78
|
+
try:
|
79
|
+
token, sender, recipient, startTime, stopTime, ratePerSecond, deposit, withdrawn, halted = (
|
80
|
+
self._sp.contract.functions.streams(int(stream_id)).call()
|
81
|
+
)
|
82
|
+
except Exception as e:
|
83
|
+
self._logger.warning(f"stream lookup failed for {stream_id}: {e}")
|
84
|
+
continue
|
85
|
+
if bool(halted):
|
86
|
+
# Respect terminated streams
|
87
|
+
self._logger.debug(f"skip stream {stream_id} halted=true")
|
88
|
+
continue
|
89
|
+
# Compute remaining seconds using chain time
|
90
|
+
try:
|
91
|
+
now = int(self._sp.web3.eth.get_block("latest")["timestamp"])
|
92
|
+
except Exception as e:
|
93
|
+
self._logger.warning(f"could not get chain time: {e}")
|
94
|
+
continue
|
95
|
+
remaining = max(int(stopTime) - now, 0)
|
96
|
+
self._logger.debug(
|
97
|
+
f"VM {vm.get('name')} stream {stream_id}: remaining={remaining}s rate={int(ratePerSecond)}"
|
98
|
+
)
|
99
|
+
if remaining < min_remaining:
|
100
|
+
# Top up to reach target_seconds of runway
|
101
|
+
deficit = max(target_seconds - remaining, 0)
|
102
|
+
add_wei = int(deficit) * int(ratePerSecond)
|
103
|
+
if add_wei <= 0:
|
104
|
+
continue
|
105
|
+
try:
|
106
|
+
self._logger.info(
|
107
|
+
f"⛽ topping up stream {stream_id} by {add_wei} wei to reach {target_seconds}s"
|
108
|
+
)
|
109
|
+
self._sp.top_up(int(stream_id), int(add_wei))
|
110
|
+
self._logger.success(
|
111
|
+
f"topped up stream {stream_id} (+{add_wei} wei); VM={vm.get('name')}"
|
112
|
+
)
|
113
|
+
except Exception as e:
|
114
|
+
# Ignore failures; will retry next tick
|
115
|
+
self._logger.warning(f"top-up failed for stream {stream_id}: {e}")
|
116
|
+
else:
|
117
|
+
self._logger.debug(
|
118
|
+
f"stream {stream_id} healthy (remaining={remaining}s >= {min_remaining}s)"
|
119
|
+
)
|
120
|
+
await asyncio.sleep(interval)
|
121
|
+
except asyncio.CancelledError:
|
122
|
+
break
|
123
|
+
except Exception as e:
|
124
|
+
# Keep the monitor resilient
|
125
|
+
self._logger.error(f"requestor stream monitor error: {e}")
|
126
|
+
await asyncio.sleep(interval)
|
requestor/services/vm_service.py
CHANGED
@@ -142,11 +142,11 @@ class VMService:
|
|
142
142
|
# Update status in database
|
143
143
|
await self.db.update_vm_status(name, "stopped")
|
144
144
|
|
145
|
-
# Best-effort
|
145
|
+
# Best-effort terminate stream on stop (treat stop as end of agreement)
|
146
146
|
try:
|
147
147
|
stream_id = vm.get('config', {}).get('stream_id')
|
148
148
|
if stream_id is not None and self.blockchain_client:
|
149
|
-
self.blockchain_client.
|
149
|
+
self.blockchain_client.terminate(stream_id)
|
150
150
|
except Exception:
|
151
151
|
pass
|
152
152
|
|
File without changes
|
{request_vm_on_golem-0.1.50.dist-info → request_vm_on_golem-0.1.51.dist-info}/entry_points.txt
RENAMED
File without changes
|