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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: request-vm-on-golem
3
- Version: 0.1.50
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=7utCzFNbh5Ol-vsBWeSwT4lXeHD7zdA-GFZuS3rHMWc,2180
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=JfDvDYNvji1uIUF-0fHEHZxLzHWl-NslBdG1Yw2sFuY,45748
5
- requestor/config.py,sha256=2ayNJzvIIoU0jMAVqbs-yfG4H63W_uALLScBG4EjUOw,8241
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=eQ2pPMpYlfPVbVFrkFElsRO5swPq-2XZEfuvxagyHDk,7941
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.50.dist-info/METADATA,sha256=EWV0bEfyWQeyBldHWHx3nUgOmZmD_5b5K6Btyp6VQt4,14363
25
- request_vm_on_golem-0.1.50.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
26
- request_vm_on_golem-0.1.50.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
27
- request_vm_on_golem-0.1.50.dist-info/RECORD,,
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
- vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
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
- vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
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
- vm_service = VMService(db_service, SSHService(config.ssh_key_dir), client)
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)
@@ -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 withdraw on stop
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.withdraw(stream_id)
149
+ self.blockchain_client.terminate(stream_id)
150
150
  except Exception:
151
151
  pass
152
152